@enrichlayer/el-linear 1.34.1 → 1.35.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 +2 -1
- package/dist/commands/issue-id.js +5 -4
- package/dist/commands/project-updates.d.ts +2 -0
- package/dist/commands/project-updates.js +116 -0
- package/dist/main.js +8 -2
- package/dist/queries/project-updates-types.d.ts +52 -0
- package/dist/queries/project-updates-types.js +8 -0
- package/dist/queries/project-updates.d.ts +3 -0
- package/dist/queries/project-updates.js +49 -0
- package/dist/utils/formatters/summary.d.ts +3 -1
- package/dist/utils/formatters/summary.js +46 -0
- package/dist/utils/output.js +42 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -445,6 +445,7 @@ el-linear <command> --help # detailed help for one command
|
|
|
445
445
|
| Comments | `comments {list, read, create, update, delete}` |
|
|
446
446
|
| Labels | `labels {list, create, retire, restore}` |
|
|
447
447
|
| Projects | `projects {list, add-team, remove-team}` |
|
|
448
|
+
| Project updates | `project-updates {create, list, read}` (post a status update to a project's Updates feed) |
|
|
448
449
|
| Cycles | `cycles {list, read}` |
|
|
449
450
|
| Documents | `documents {list, read, create, update, delete}` |
|
|
450
451
|
| Releases | `releases {list, read, create, pipelines}` |
|
|
@@ -578,7 +579,7 @@ el-linear projects list --format summary --fields name,state,progress,lead,teams
|
|
|
578
579
|
# ...
|
|
579
580
|
```
|
|
580
581
|
|
|
581
|
-
Unrecognized field names are reported as a `_warnings:` line appended after the summary block (`fields_unprojectable: --format summary on issues list does not project foo, bar; ...`) — same signal scripts get on the JSON path. Resources whose summary formatter doesn't yet wire `--fields` (cycles, milestones, comments, teams, labels, users, documents, templates, attachments, releases, search results) emit the same warning and render their default summary.
|
|
582
|
+
Unrecognized field names are reported as a `_warnings:` line appended after the summary block (`fields_unprojectable: --format summary on issues list does not project foo, bar; ...`) — same signal scripts get on the JSON path. Resources whose summary formatter doesn't yet wire `--fields` (cycles, milestones, project updates, comments, teams, labels, users, documents, templates, attachments, releases, search results) emit the same warning and render their default summary.
|
|
582
583
|
|
|
583
584
|
### Windowed metadata (`WindowedMeta`)
|
|
584
585
|
|
|
@@ -22,11 +22,12 @@ import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
|
22
22
|
// elsewhere in the workspace. Any addition here is the single point of
|
|
23
23
|
// truth that all skills rely on.
|
|
24
24
|
//
|
|
25
|
-
// Accepted prefixes (DEV-4777 adds bug/spike; DEV-4660 added codex;
|
|
26
|
-
// mirror tools-repo DEV-4417):
|
|
27
|
-
// feature | fix | chore | refactor | dev
|
|
25
|
+
// Accepted prefixes (DEV-4777 adds bug/spike; DEV-4660 added codex; DEV-5342
|
|
26
|
+
// adds the feat short-form alias; all mirror tools-repo DEV-4417/DEV-5334):
|
|
27
|
+
// feature | feat | fix | chore | refactor | dev — SOP-canonical + Linear-CLI direct
|
|
28
28
|
// bug | spike — sanctioned Linear type labels
|
|
29
29
|
// codex — Codex-authored branches (codex/<TEAM>-<N>-slug)
|
|
30
|
+
// feat — short-form alias for feature (DEV-5342)
|
|
30
31
|
// New authoring surfaces (Codex, future agent prefixes) and sanctioned Linear
|
|
31
32
|
// type labels (bug, spike) get first-class issue detection so commit guards,
|
|
32
33
|
// MR descriptions, and session handoff don't go dark on a branch a human
|
|
@@ -34,7 +35,7 @@ import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
|
34
35
|
//
|
|
35
36
|
// Mirror of cli/el-git/src/commands/context.ts:BRANCH_RE in the
|
|
36
37
|
// vertical-int/tools repo. If you change one, change the other.
|
|
37
|
-
const BRANCH_RE = /^(?:feature|fix|chore|refactor|bug|spike|dev|codex)[-/]([A-Z]{2,4})-(\d+)(?:[-/](.*))?$/i;
|
|
38
|
+
const BRANCH_RE = /^(?:feature|feat|fix|chore|refactor|bug|spike|dev|codex)[-/]([A-Z]{2,4})-(\d+)(?:[-/](.*))?$/i;
|
|
38
39
|
export function parseBranchName(branch) {
|
|
39
40
|
const m = branch.match(BRANCH_RE);
|
|
40
41
|
if (!m) {
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { CREATE_PROJECT_UPDATE_MUTATION, GET_PROJECT_UPDATE_BY_ID_QUERY, LIST_PROJECT_UPDATES_QUERY, } from "../queries/project-updates.js";
|
|
3
|
+
import { notFoundError } from "../utils/error-messages.js";
|
|
4
|
+
import { createGraphQLService } from "../utils/graphql-service.js";
|
|
5
|
+
import { createLinearService } from "../utils/linear-service.js";
|
|
6
|
+
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
7
|
+
import { getRootOpts } from "../utils/root-opts.js";
|
|
8
|
+
import { parsePositiveInt } from "../utils/validators.js";
|
|
9
|
+
const VALID_HEALTH = [
|
|
10
|
+
"onTrack",
|
|
11
|
+
"atRisk",
|
|
12
|
+
"offTrack",
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the update body from `--body` / `--body-file` (mutually exclusive,
|
|
16
|
+
* exactly one required) — mirrors the comments-command contract so file-sourced
|
|
17
|
+
* bodies sidestep shell-quoting traps for markdown/tables/backticks.
|
|
18
|
+
*/
|
|
19
|
+
function resolveBody(options) {
|
|
20
|
+
if (options.body && options.bodyFile) {
|
|
21
|
+
throw new Error("--body and --body-file are mutually exclusive — pass one or the other");
|
|
22
|
+
}
|
|
23
|
+
if (options.bodyFile) {
|
|
24
|
+
return readFileSync(options.bodyFile, "utf-8");
|
|
25
|
+
}
|
|
26
|
+
if (options.body) {
|
|
27
|
+
return options.body;
|
|
28
|
+
}
|
|
29
|
+
throw new Error("Either --body or --body-file is required");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Validate an optional `--health` value against Linear's
|
|
33
|
+
* `ProjectUpdateHealthType` enum. Returns undefined when the flag is omitted
|
|
34
|
+
* (Linear then defaults the health), throws on an unknown value.
|
|
35
|
+
*/
|
|
36
|
+
function resolveHealth(value) {
|
|
37
|
+
if (value === undefined) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
if (!VALID_HEALTH.includes(value)) {
|
|
41
|
+
throw new Error(`Invalid --health "${value}" — expected one of: ${VALID_HEALTH.join(", ")}`);
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
async function handleCreateProjectUpdate(options, command) {
|
|
46
|
+
// Validate local input first so bad --body/--health fails with zero network.
|
|
47
|
+
const body = resolveBody(options);
|
|
48
|
+
const health = resolveHealth(options.health);
|
|
49
|
+
const rootOpts = getRootOpts(command);
|
|
50
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
51
|
+
const linearService = await createLinearService(rootOpts);
|
|
52
|
+
const projectId = await linearService.resolveProjectId(options.project);
|
|
53
|
+
const input = { projectId, body };
|
|
54
|
+
if (health !== undefined) {
|
|
55
|
+
input.health = health;
|
|
56
|
+
}
|
|
57
|
+
if (options.diffHidden) {
|
|
58
|
+
input.isDiffHidden = true;
|
|
59
|
+
}
|
|
60
|
+
const result = await graphQLService.rawRequest(CREATE_PROJECT_UPDATE_MUTATION, { input });
|
|
61
|
+
if (!result.projectUpdateCreate.success ||
|
|
62
|
+
!result.projectUpdateCreate.projectUpdate) {
|
|
63
|
+
throw new Error(`Failed to create project update on project "${options.project}"`);
|
|
64
|
+
}
|
|
65
|
+
outputSuccess(result.projectUpdateCreate.projectUpdate);
|
|
66
|
+
}
|
|
67
|
+
async function handleListProjectUpdates(options, command) {
|
|
68
|
+
const rootOpts = getRootOpts(command);
|
|
69
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
70
|
+
const linearService = await createLinearService(rootOpts);
|
|
71
|
+
const projectId = await linearService.resolveProjectId(options.project);
|
|
72
|
+
const result = await graphQLService.rawRequest(LIST_PROJECT_UPDATES_QUERY, {
|
|
73
|
+
projectId,
|
|
74
|
+
first: parsePositiveInt(options.limit, "--limit"),
|
|
75
|
+
});
|
|
76
|
+
if (!result.project) {
|
|
77
|
+
throw notFoundError("Project", options.project);
|
|
78
|
+
}
|
|
79
|
+
const nodes = result.project.projectUpdates.nodes;
|
|
80
|
+
outputSuccess({ data: nodes, meta: { count: nodes.length } });
|
|
81
|
+
}
|
|
82
|
+
async function handleReadProjectUpdate(updateId, _options, command) {
|
|
83
|
+
const rootOpts = getRootOpts(command);
|
|
84
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
85
|
+
const result = await graphQLService.rawRequest(GET_PROJECT_UPDATE_BY_ID_QUERY, { id: updateId });
|
|
86
|
+
if (!result.projectUpdate) {
|
|
87
|
+
throw notFoundError("Project update", updateId);
|
|
88
|
+
}
|
|
89
|
+
outputSuccess(result.projectUpdate);
|
|
90
|
+
}
|
|
91
|
+
export function setupProjectUpdatesCommands(program) {
|
|
92
|
+
const projectUpdates = program
|
|
93
|
+
.command("project-updates")
|
|
94
|
+
.description("Project update (status post) operations");
|
|
95
|
+
projectUpdates.action(() => projectUpdates.help());
|
|
96
|
+
projectUpdates
|
|
97
|
+
.command("create")
|
|
98
|
+
.description("Post a status update to a project (appears in the project's Updates feed).")
|
|
99
|
+
.requiredOption("--project <project>", "project name or ID")
|
|
100
|
+
.option("--body <body>", "update body markdown (inline)")
|
|
101
|
+
.option("--body-file <path>", "read update body from file")
|
|
102
|
+
.option("--health <health>", "project health: onTrack | atRisk | offTrack (omit to leave unset)")
|
|
103
|
+
.option("--diff-hidden", "hide the progress diff on the update")
|
|
104
|
+
.option("-q, --quiet", "print one confirmation line (health url) instead of the full JSON")
|
|
105
|
+
.action(handleAsyncCommand(handleCreateProjectUpdate));
|
|
106
|
+
projectUpdates
|
|
107
|
+
.command("list")
|
|
108
|
+
.description("List status updates posted to a project (newest first).")
|
|
109
|
+
.requiredOption("--project <project>", "project name or ID")
|
|
110
|
+
.option("-l, --limit <number>", "limit results", "50")
|
|
111
|
+
.action(handleAsyncCommand(handleListProjectUpdates));
|
|
112
|
+
projectUpdates
|
|
113
|
+
.command("read <updateId>")
|
|
114
|
+
.description("Get a single project update by its ID.")
|
|
115
|
+
.action(handleAsyncCommand(handleReadProjectUpdate));
|
|
116
|
+
}
|
package/dist/main.js
CHANGED
|
@@ -20,6 +20,7 @@ import { setupIssuesCommands } from "./commands/issues.js";
|
|
|
20
20
|
import { setupLabelsCommands } from "./commands/labels.js";
|
|
21
21
|
import { setupProfileCommands } from "./commands/profile.js";
|
|
22
22
|
import { setupProjectMilestonesCommands } from "./commands/project-milestones.js";
|
|
23
|
+
import { setupProjectUpdatesCommands } from "./commands/project-updates.js";
|
|
23
24
|
import { setupProjectsCommands } from "./commands/projects.js";
|
|
24
25
|
import { setupReadShortcut } from "./commands/read-shortcut.js";
|
|
25
26
|
import { setupRefsCommands } from "./commands/refs.js";
|
|
@@ -65,9 +66,13 @@ program
|
|
|
65
66
|
.option("--profile <name>", "named profile (under ~/.config/el-linear/profiles/<name>/) for this invocation. Overrides EL_LINEAR_PROFILE env + the on-disk active-profile marker.")
|
|
66
67
|
.option("--json", "output as JSON (default, accepted for compatibility)")
|
|
67
68
|
.option("--format <kind>", "output format: json (default, structured envelope) or summary (human-readable)", "json")
|
|
68
|
-
.option("--raw", "strip { data, meta } wrapper from list output — emit the array directly"
|
|
69
|
+
.option("--raw", "strip { data, meta } wrapper from list output — emit the array directly. " +
|
|
70
|
+
"Warnings (e.g. --fields fields_unresolved) can't ride on a bare array, so " +
|
|
71
|
+
"they are written to stderr prefixed `_warnings: `, keeping stdout a pure JSON array")
|
|
69
72
|
.option("--jq <filter>", "apply a jq filter to the JSON output")
|
|
70
|
-
.option("--fields <fields>", "filter output to specific fields (comma-separated)"
|
|
73
|
+
.option("--fields <fields>", "filter output to specific fields (comma-separated). Unresolved fields are " +
|
|
74
|
+
"emitted as null plus a `fields_unresolved:` warning — in the JSON " +
|
|
75
|
+
"envelope's `_warnings`, or on stderr when output is a bare array (with --raw)")
|
|
71
76
|
.option("--no-cache", "bypass the on-disk cache for `teams list` / `labels list` / `projects list`");
|
|
72
77
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
73
78
|
const rootOpts = actionCommand.optsWithGlobals();
|
|
@@ -143,6 +148,7 @@ setupReleasesCommands(program);
|
|
|
143
148
|
setupProjectsCommands(program);
|
|
144
149
|
setupCyclesCommands(program);
|
|
145
150
|
setupProjectMilestonesCommands(program);
|
|
151
|
+
setupProjectUpdatesCommands(program);
|
|
146
152
|
setupEmbedsCommands(program);
|
|
147
153
|
setupTeamsCommands(program);
|
|
148
154
|
setupTemplatesCommands(program);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed response shapes for the queries in `./project-updates.ts`.
|
|
3
|
+
* See `./issues-types.ts` for the rationale (ALL-937).
|
|
4
|
+
*
|
|
5
|
+
* All three queries (create / list / read) select the same
|
|
6
|
+
* `PROJECT_UPDATE_FRAGMENT`, so one node shape covers every consumer.
|
|
7
|
+
*/
|
|
8
|
+
export type ProjectUpdateHealth = "onTrack" | "atRisk" | "offTrack";
|
|
9
|
+
interface ProjectUpdateProjectRef {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
}
|
|
13
|
+
interface ProjectUpdateUserRef {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
displayName: string | null;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Mirrors `PROJECT_UPDATE_FRAGMENT` — the full selection set shared by the
|
|
20
|
+
* create mutation, the project-scoped list, and the by-id read.
|
|
21
|
+
*/
|
|
22
|
+
export interface ProjectUpdateNode {
|
|
23
|
+
id: string;
|
|
24
|
+
body: string | null;
|
|
25
|
+
health: ProjectUpdateHealth | null;
|
|
26
|
+
url: string | null;
|
|
27
|
+
slugId: string | null;
|
|
28
|
+
createdAt: string;
|
|
29
|
+
updatedAt: string;
|
|
30
|
+
editedAt: string | null;
|
|
31
|
+
user: ProjectUpdateUserRef | null;
|
|
32
|
+
project: ProjectUpdateProjectRef | null;
|
|
33
|
+
}
|
|
34
|
+
export interface CreateProjectUpdateResponse {
|
|
35
|
+
projectUpdateCreate: {
|
|
36
|
+
success: boolean;
|
|
37
|
+
projectUpdate: ProjectUpdateNode | null;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export interface ListProjectUpdatesResponse {
|
|
41
|
+
project: {
|
|
42
|
+
id: string;
|
|
43
|
+
name: string;
|
|
44
|
+
projectUpdates: {
|
|
45
|
+
nodes: ProjectUpdateNode[];
|
|
46
|
+
};
|
|
47
|
+
} | null;
|
|
48
|
+
}
|
|
49
|
+
export interface GetProjectUpdateByIdResponse {
|
|
50
|
+
projectUpdate: ProjectUpdateNode | null;
|
|
51
|
+
}
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed response shapes for the queries in `./project-updates.ts`.
|
|
3
|
+
* See `./issues-types.ts` for the rationale (ALL-937).
|
|
4
|
+
*
|
|
5
|
+
* All three queries (create / list / read) select the same
|
|
6
|
+
* `PROJECT_UPDATE_FRAGMENT`, so one node shape covers every consumer.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const CREATE_PROJECT_UPDATE_MUTATION = "\n mutation ProjectUpdateCreate($input: ProjectUpdateCreateInput!) {\n projectUpdateCreate(input: $input) {\n success\n projectUpdate {\n \n id\n body\n health\n url\n slugId\n createdAt\n updatedAt\n editedAt\n user {\n id\n name\n displayName\n }\n project {\n id\n name\n }\n\n }\n }\n }\n";
|
|
2
|
+
export declare const LIST_PROJECT_UPDATES_QUERY = "\n query ListProjectUpdates($projectId: String!, $first: Int!) {\n project(id: $projectId) {\n id\n name\n projectUpdates(first: $first, orderBy: createdAt) {\n nodes {\n \n id\n body\n health\n url\n slugId\n createdAt\n updatedAt\n editedAt\n user {\n id\n name\n displayName\n }\n project {\n id\n name\n }\n\n }\n }\n }\n }\n";
|
|
3
|
+
export declare const GET_PROJECT_UPDATE_BY_ID_QUERY = "\n query GetProjectUpdate($id: String!) {\n projectUpdate(id: $id) {\n \n id\n body\n health\n url\n slugId\n createdAt\n updatedAt\n editedAt\n user {\n id\n name\n displayName\n }\n project {\n id\n name\n }\n\n }\n }\n";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const PROJECT_UPDATE_FRAGMENT = `
|
|
2
|
+
id
|
|
3
|
+
body
|
|
4
|
+
health
|
|
5
|
+
url
|
|
6
|
+
slugId
|
|
7
|
+
createdAt
|
|
8
|
+
updatedAt
|
|
9
|
+
editedAt
|
|
10
|
+
user {
|
|
11
|
+
id
|
|
12
|
+
name
|
|
13
|
+
displayName
|
|
14
|
+
}
|
|
15
|
+
project {
|
|
16
|
+
id
|
|
17
|
+
name
|
|
18
|
+
}
|
|
19
|
+
`;
|
|
20
|
+
export const CREATE_PROJECT_UPDATE_MUTATION = `
|
|
21
|
+
mutation ProjectUpdateCreate($input: ProjectUpdateCreateInput!) {
|
|
22
|
+
projectUpdateCreate(input: $input) {
|
|
23
|
+
success
|
|
24
|
+
projectUpdate {
|
|
25
|
+
${PROJECT_UPDATE_FRAGMENT}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
`;
|
|
30
|
+
export const LIST_PROJECT_UPDATES_QUERY = `
|
|
31
|
+
query ListProjectUpdates($projectId: String!, $first: Int!) {
|
|
32
|
+
project(id: $projectId) {
|
|
33
|
+
id
|
|
34
|
+
name
|
|
35
|
+
projectUpdates(first: $first, orderBy: createdAt) {
|
|
36
|
+
nodes {
|
|
37
|
+
${PROJECT_UPDATE_FRAGMENT}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
`;
|
|
43
|
+
export const GET_PROJECT_UPDATE_BY_ID_QUERY = `
|
|
44
|
+
query GetProjectUpdate($id: String!) {
|
|
45
|
+
projectUpdate(id: $id) {
|
|
46
|
+
${PROJECT_UPDATE_FRAGMENT}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
@@ -30,6 +30,8 @@ export declare function formatCycleSummary(cycle: Record<string, unknown>): stri
|
|
|
30
30
|
export declare function formatCycleList(cycles: unknown[]): string;
|
|
31
31
|
export declare function formatMilestoneSummary(milestone: Record<string, unknown>): string;
|
|
32
32
|
export declare function formatMilestoneList(milestones: unknown[]): string;
|
|
33
|
+
export declare function formatProjectUpdateSummary(update: Record<string, unknown>): string;
|
|
34
|
+
export declare function formatProjectUpdateList(updates: unknown[]): string;
|
|
33
35
|
export declare function formatTeamList(teams: unknown[]): string;
|
|
34
36
|
export declare function formatLabelList(labels: unknown[]): string;
|
|
35
37
|
/**
|
|
@@ -68,7 +70,7 @@ export declare function formatSearchResultList(results: unknown[]): string;
|
|
|
68
70
|
* the full payload. Lists fall back to a simple bulleted list.
|
|
69
71
|
*/
|
|
70
72
|
export declare function formatGenericSummary(value: unknown): string;
|
|
71
|
-
export type ResourceKind = "issue" | "issue-list" | "project" | "project-list" | "comment" | "comment-list" | "cycle" | "cycle-list" | "milestone" | "milestone-list" | "team-list" | "label-list" | "user" | "user-list" | "document" | "document-list" | "template" | "template-list" | "attachment-list" | "release" | "release-list" | "search-result-list" | "relation-list" | "issue-relation-list" | "empty-list" | "generic";
|
|
73
|
+
export type ResourceKind = "issue" | "issue-list" | "project" | "project-list" | "comment" | "comment-list" | "cycle" | "cycle-list" | "milestone" | "milestone-list" | "project-update" | "project-update-list" | "team-list" | "label-list" | "user" | "user-list" | "document" | "document-list" | "template" | "template-list" | "attachment-list" | "release" | "release-list" | "search-result-list" | "relation-list" | "issue-relation-list" | "empty-list" | "generic";
|
|
72
74
|
/**
|
|
73
75
|
* Heuristic — used by the central `outputSuccess` path which doesn't know
|
|
74
76
|
* which command produced the payload. Looks at the shape of the data to
|
|
@@ -655,6 +655,35 @@ export function formatMilestoneList(milestones) {
|
|
|
655
655
|
{ header: "PROJECT", minWidth: 7, extract: (m) => getName(m.project) },
|
|
656
656
|
], { emptyText: "(no milestones)", itemNoun: "milestone" });
|
|
657
657
|
}
|
|
658
|
+
// ── project updates ────────────────────────────────────────────
|
|
659
|
+
export function formatProjectUpdateSummary(update) {
|
|
660
|
+
const headerLine = `project-update ${s(update.id)}`;
|
|
661
|
+
const fields = [
|
|
662
|
+
{ label: "Health", value: s(update.health) },
|
|
663
|
+
{ label: "Project", value: getName(update.project) },
|
|
664
|
+
{ label: "Author", value: getName(update.user) },
|
|
665
|
+
{ label: "Created", value: s(update.createdAt) },
|
|
666
|
+
{ label: "URL", value: s(update.url) },
|
|
667
|
+
];
|
|
668
|
+
const header = renderHeader(fields);
|
|
669
|
+
const body = clipDescription(update.body);
|
|
670
|
+
const parts = [headerLine, header];
|
|
671
|
+
if (body)
|
|
672
|
+
parts.push("", body);
|
|
673
|
+
return parts.filter((p) => p !== "").join("\n");
|
|
674
|
+
}
|
|
675
|
+
export function formatProjectUpdateList(updates) {
|
|
676
|
+
return renderTable(updates.map((raw) => asObj(raw) ?? {}), [
|
|
677
|
+
{ header: "HEALTH", minWidth: 6, extract: (u) => s(u.health) },
|
|
678
|
+
{ header: "AUTHOR", minWidth: 6, extract: (u) => getName(u.user) },
|
|
679
|
+
{
|
|
680
|
+
header: "CREATED",
|
|
681
|
+
minWidth: 7,
|
|
682
|
+
extract: (u) => s(u.createdAt).slice(0, 10),
|
|
683
|
+
},
|
|
684
|
+
{ header: "URL", minWidth: 3, maxWidth: 60, extract: (u) => s(u.url) },
|
|
685
|
+
], { emptyText: "(no project updates)", itemNoun: "project update" });
|
|
686
|
+
}
|
|
658
687
|
// ── teams ──────────────────────────────────────────────────────
|
|
659
688
|
export function formatTeamList(teams) {
|
|
660
689
|
return renderTable(teams.map((raw) => asObj(raw) ?? {}), [
|
|
@@ -1036,6 +1065,10 @@ export function inferKindFromPayload(value) {
|
|
|
1036
1065
|
"progress" in obj &&
|
|
1037
1066
|
("state" in obj || "lead" in obj || "teams" in obj))
|
|
1038
1067
|
return "project";
|
|
1068
|
+
// Project updates carry body + createdAt + user like comments; `health` is
|
|
1069
|
+
// the distinguishing field, so this must precede the comment check.
|
|
1070
|
+
if ("body" in obj && "health" in obj)
|
|
1071
|
+
return "project-update";
|
|
1039
1072
|
if ("body" in obj && "createdAt" in obj && "user" in obj)
|
|
1040
1073
|
return "comment";
|
|
1041
1074
|
if (("number" in obj || "isActive" in obj) &&
|
|
@@ -1079,6 +1112,10 @@ function inferListKind(items) {
|
|
|
1079
1112
|
}
|
|
1080
1113
|
return "issue-list";
|
|
1081
1114
|
}
|
|
1115
|
+
// Project-update rows carry body + createdAt like comments; `health` is the
|
|
1116
|
+
// distinguishing field, so this must precede the comment-list check.
|
|
1117
|
+
if ("body" in sample && "health" in sample)
|
|
1118
|
+
return "project-update-list";
|
|
1082
1119
|
if ("body" in sample && "createdAt" in sample)
|
|
1083
1120
|
return "comment-list";
|
|
1084
1121
|
if ("progress" in sample && "name" in sample) {
|
|
@@ -1168,6 +1205,10 @@ export function dispatch(kind, payload, fields) {
|
|
|
1168
1205
|
return formatMilestoneSummary((obj ?? {}));
|
|
1169
1206
|
case "milestone-list":
|
|
1170
1207
|
return formatMilestoneList(list ?? []);
|
|
1208
|
+
case "project-update":
|
|
1209
|
+
return formatProjectUpdateSummary((obj ?? {}));
|
|
1210
|
+
case "project-update-list":
|
|
1211
|
+
return formatProjectUpdateList(list ?? []);
|
|
1171
1212
|
case "team-list":
|
|
1172
1213
|
return formatTeamList(list ?? []);
|
|
1173
1214
|
case "label-list":
|
|
@@ -1230,6 +1271,11 @@ export function formatLine(payload) {
|
|
|
1230
1271
|
if (kind === "comment" && obj) {
|
|
1231
1272
|
return `comment ${s(obj.id)}`;
|
|
1232
1273
|
}
|
|
1274
|
+
if (kind === "project-update" && obj) {
|
|
1275
|
+
// create doesn't carry an identifier/title — health + url are the
|
|
1276
|
+
// stable handles a caller needs (matches the summary header style).
|
|
1277
|
+
return `${s(obj.health)} ${s(obj.url)}`;
|
|
1278
|
+
}
|
|
1233
1279
|
if (kind === "relation-list") {
|
|
1234
1280
|
return formatRelationLine(payload);
|
|
1235
1281
|
}
|
package/dist/utils/output.js
CHANGED
|
@@ -78,6 +78,12 @@ function filterFields(obj, fields, unresolved) {
|
|
|
78
78
|
const perItem = new Set();
|
|
79
79
|
const projected = filterFields(item, fields, perItem);
|
|
80
80
|
for (const field of fields) {
|
|
81
|
+
// A primitive item short-circuits: filterFields returns it
|
|
82
|
+
// unchanged and records NOTHING in perItem, so every field counts
|
|
83
|
+
// as resolved here — a mixed array holding one primitive suppresses
|
|
84
|
+
// the unresolved warning for all fields. Acceptable: a primitive
|
|
85
|
+
// item can't meaningfully be projected, and heterogeneous lists
|
|
86
|
+
// legitimately have per-item gaps.
|
|
81
87
|
if (!perItem.has(field)) {
|
|
82
88
|
resolvedSomewhere.add(field);
|
|
83
89
|
}
|
|
@@ -208,20 +214,19 @@ export function outputSuccess(data) {
|
|
|
208
214
|
}
|
|
209
215
|
else if (output !== null && typeof output === "object") {
|
|
210
216
|
const obj = output;
|
|
211
|
-
if (
|
|
212
|
-
output = {
|
|
213
|
-
...obj,
|
|
214
|
-
data: filterFields(obj.data, fieldsFilter, unresolved),
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
else if (obj.data !== null &&
|
|
217
|
+
if (obj.data !== null &&
|
|
218
218
|
obj.data !== undefined &&
|
|
219
219
|
typeof obj.data === "object") {
|
|
220
|
-
// Envelope with
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
220
|
+
// Envelope with a `data` payload — project inside `data`,
|
|
221
|
+
// whether it's an array (list envelope) or an object (e.g.
|
|
222
|
+
// el-git context). Arrays ARE objects, so this single
|
|
223
|
+
// `typeof === "object"` check subsumes the former separate
|
|
224
|
+
// `Array.isArray(obj.data)` arm (DEV-5339 collapsed the two
|
|
225
|
+
// byte-identical branches); `filterFields` dispatches on
|
|
226
|
+
// array-vs-object internally. Paths are relative to `data` for
|
|
227
|
+
// every envelope shape. Before DEV-5323 the object case fell
|
|
228
|
+
// through to root filtering, so `--fields branch,issueId` on an
|
|
229
|
+
// envelope returned `{}`.
|
|
225
230
|
output = {
|
|
226
231
|
...obj,
|
|
227
232
|
data: filterFields(obj.data, fieldsFilter, unresolved),
|
|
@@ -261,11 +266,31 @@ export function outputSuccess(data) {
|
|
|
261
266
|
// fields_unprojectable surfaced from a prior dispatch) and embed
|
|
262
267
|
// them as `_warnings` on the envelope.
|
|
263
268
|
const warnings = [...drainWarnings(), ...drainSummaryFieldWarnings()];
|
|
264
|
-
if (warnings.length > 0
|
|
265
|
-
output !== null &&
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
+
if (warnings.length > 0) {
|
|
270
|
+
if (output !== null &&
|
|
271
|
+
typeof output === "object" &&
|
|
272
|
+
!Array.isArray(output)) {
|
|
273
|
+
output = { ...output, _warnings: warnings };
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
// Bare-array (or primitive / null) output has no envelope object to
|
|
277
|
+
// carry `_warnings`. This is the DEV-5339 fix: previously the buffer
|
|
278
|
+
// was drained above but only re-embedded for object output, so a
|
|
279
|
+
// warning was silently dropped on a top-level array payload AND on
|
|
280
|
+
// `--raw` (which unwraps { data: [...] } to a bare array *before*
|
|
281
|
+
// this point) — losing exactly the DEV-5323 `fields_unresolved:`
|
|
282
|
+
// fail-visible signal on the `--raw` form our own CLAUDE.md
|
|
283
|
+
// recommends to agents. Route each warning to STDERR prefixed
|
|
284
|
+
// `_warnings: ` so the signal always reaches the consumer while
|
|
285
|
+
// stdout stays a pure JSON array (safe to pipe to `jq`). Mirrors the
|
|
286
|
+
// summary path's `_warnings:` line convention — that path uses
|
|
287
|
+
// logger.info because its stdout is already human text; here stdout
|
|
288
|
+
// must remain machine-parseable JSON, so we use logger.error
|
|
289
|
+
// (stderr).
|
|
290
|
+
for (const w of warnings) {
|
|
291
|
+
logger.error(`_warnings: ${w}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
269
294
|
}
|
|
270
295
|
if (jqFilter) {
|
|
271
296
|
const json = JSON.stringify(output);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enrichlayer/el-linear",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.35.0",
|
|
4
4
|
"description": "A pragmatic CLI for Linear.app — deterministic team/label/member resolution, structured issue validation, configurable term enforcement, and a GraphQL escape hatch.",
|
|
5
5
|
"main": "dist/main.js",
|
|
6
6
|
"types": "dist/main.d.ts",
|