@danypops/tickets 0.5.0 → 0.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/tickets",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Unified CLI, daemon, and TypeScript library for issue tracking across GitHub, GitLab, and Jira.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -295,6 +295,24 @@ export class JiraRepository {
295
295
  return result.jql;
296
296
  }
297
297
 
298
+ /**
299
+ * BoardFilterDiscoverable -- resolves a board's own real base scope: its saved
300
+ * filter's JQL, via `GET /rest/agile/1.0/board/{boardId}/configuration` (filter
301
+ * id) then `GET /rest/api/2/filter/{id}` (that filter's JQL). This is the actual
302
+ * project/query scope a board tracks -- assuming a board scopes to a single named
303
+ * project (e.g. matching a URL path segment) is a guess, not a fact; a project can
304
+ * be renamed or a board's filter can span multiple projects, so this is always
305
+ * resolved from Jira, never inferred from a project key string.
306
+ */
307
+ async discoverBoardFilterJql(boardId: number): Promise<string> {
308
+ const config = await this.call<{ filter?: { id?: string } }>(() => this.agileClient().board.getConfiguration({ boardId }));
309
+ const filterId = config?.filter?.id;
310
+ if (!filterId) throw new Error(`jira: board ${boardId} has no configured filter`);
311
+ const filter = await this.call<{ jql?: string }>(() => this.client.filters.getFilter({ id: Number(filterId) }));
312
+ if (!filter?.jql) throw new Error(`jira: filter ${filterId} (board ${boardId}'s own filter) has no JQL`);
313
+ return filter.jql;
314
+ }
315
+
298
316
  private agileClientInstance?: AgileClient;
299
317
 
300
318
  private agileClient(): AgileClient {
@@ -8,6 +8,7 @@ import type { Comment, CreateInput, Issue, ListFilter, UpdateInput } from "../do
8
8
  import { parseRef } from "../domain/issue.js";
9
9
  import type { Template } from "../domain/template.js";
10
10
  import {
11
+ hasBoardFilterDiscovery,
11
12
  hasBoardQuickFilterDiscovery,
12
13
  hasComments,
13
14
  hasFieldDiscovery,
@@ -133,4 +134,11 @@ export class TicketService {
133
134
  if (!hasBoardQuickFilterDiscovery(repo)) throw new NotSupportedError(backend, "board quick filter discovery");
134
135
  return repo.discoverBoardQuickFilterJql(boardId, quickFilterId);
135
136
  }
137
+
138
+ /** Resolves a Jira board's own real base scope (its saved filter's JQL) -- never assume a board tracks one named project. */
139
+ async discoverBoardFilterJql(backend: string, boardId: number): Promise<string> {
140
+ const repo = this.repo(backend);
141
+ if (!hasBoardFilterDiscovery(repo)) throw new NotSupportedError(backend, "board filter discovery");
142
+ return repo.discoverBoardFilterJql(boardId);
143
+ }
136
144
  }
package/src/cli/index.ts CHANGED
@@ -230,6 +230,15 @@ discoverCmd
230
230
  );
231
231
  });
232
232
 
233
+ discoverCmd
234
+ .command("board_filter")
235
+ .description("resolve a Jira board's own real base scope -- its saved filter's JQL -- instead of assuming it tracks one named project")
236
+ .requiredOption("-b, --backend <name>", "backend name")
237
+ .requiredOption("--board <id>", "board id", (v) => Number.parseInt(v, 10))
238
+ .action(async (opts) => {
239
+ await withClient((client) => client.call("discover.board_filter", { backend: opts.backend, boardId: opts.board }));
240
+ });
241
+
233
242
  const queryCmd = program.command("query").description("save and run named raw backend queries (Jira JQL) -- e.g. a board's sprint or backlog view");
234
243
 
235
244
  queryCmd
package/src/daemon/ops.ts CHANGED
@@ -30,6 +30,7 @@ export type TicketOperation =
30
30
  | "discover.statuses"
31
31
  | "discover.template"
32
32
  | "discover.board_quickfilter"
33
+ | "discover.board_filter"
33
34
  | "query.save"
34
35
  | "query.list"
35
36
  | "query.remove"
@@ -57,6 +58,7 @@ export interface TicketOpInputs extends Record<TicketOperation, unknown> {
57
58
  "discover.statuses": { backend: string };
58
59
  "discover.template": { backend: string; project: string; issueType: string; sampleSize?: number };
59
60
  "discover.board_quickfilter": { backend: string; boardId: number; quickFilterId: number };
61
+ "discover.board_filter": { backend: string; boardId: number };
60
62
  "query.save": { name: string; backend: string; query: string; description?: string };
61
63
  "query.list": Record<string, never>;
62
64
  "query.remove": { name: string };
@@ -85,6 +87,7 @@ export interface TicketOpOutputs extends Record<TicketOperation, unknown> {
85
87
  "discover.statuses": { mappings: Record<string, string> };
86
88
  "discover.template": { template: Template | null };
87
89
  "discover.board_quickfilter": { jql: string };
90
+ "discover.board_filter": { jql: string };
88
91
  "query.save": { query: SavedQuery };
89
92
  "query.list": { queries: SavedQuery[] };
90
93
  "query.remove": { removed: boolean };
@@ -113,6 +116,7 @@ export const TICKET_OPERATIONS: TicketOperation[] = [
113
116
  "discover.statuses",
114
117
  "discover.template",
115
118
  "discover.board_quickfilter",
119
+ "discover.board_filter",
116
120
  "query.save",
117
121
  "query.list",
118
122
  "query.remove",
@@ -94,6 +94,7 @@ export const TICKET_OP_HANDLERS: { [Op in TicketOperation]: Handler<Op> } = {
94
94
  "discover.board_quickfilter": async (deps, input) => ({
95
95
  jql: await deps.service.discoverBoardQuickFilterJql(input.backend, input.boardId, input.quickFilterId),
96
96
  }),
97
+ "discover.board_filter": async (deps, input) => ({ jql: await deps.service.discoverBoardFilterJql(input.backend, input.boardId) }),
97
98
  "query.save": async (deps, input) => ({ query: deps.queries.save(input.name, input.backend, input.query, input.description) }),
98
99
  "query.list": async (deps) => ({ queries: deps.queries.list() }),
99
100
  "query.remove": async (deps, input) => ({ removed: deps.queries.remove(input.name) }),
@@ -88,3 +88,15 @@ export interface BoardQuickFilterDiscoverable {
88
88
  export function hasBoardQuickFilterDiscovery(repo: IssueRepository): repo is IssueRepository & BoardQuickFilterDiscoverable {
89
89
  return typeof (repo as Partial<BoardQuickFilterDiscoverable>).discoverBoardQuickFilterJql === "function";
90
90
  }
91
+
92
+ /**
93
+ * Optional capability — resolves a Jira board's own real base scope (its saved
94
+ * filter's JQL) rather than assuming a board tracks one named project. Jira only.
95
+ */
96
+ export interface BoardFilterDiscoverable {
97
+ discoverBoardFilterJql(boardId: number): Promise<string>;
98
+ }
99
+
100
+ export function hasBoardFilterDiscovery(repo: IssueRepository): repo is IssueRepository & BoardFilterDiscoverable {
101
+ return typeof (repo as Partial<BoardFilterDiscoverable>).discoverBoardFilterJql === "function";
102
+ }
@@ -112,6 +112,13 @@ const OPERATIONS: readonly OperationSpec[] = [
112
112
  properties: { backend: stringProp, boardId: numberProp, quickFilterId: numberProp },
113
113
  required: ["backend", "boardId", "quickFilterId"],
114
114
  },
115
+ {
116
+ action: "discover.board_filter",
117
+ description: "Resolves a Jira board's own real base scope -- its saved filter's JQL -- rather than assuming it tracks one named project.",
118
+ effect: "read",
119
+ properties: { backend: stringProp, boardId: numberProp },
120
+ required: ["backend", "boardId"],
121
+ },
115
122
  {
116
123
  action: "query.save",
117
124
  description: "Saves a raw backend query (Jira JQL) under a name, so it can be run again later without retyping it -- e.g. a board's sprint or backlog view.",