@dac-tech-team-tw/itsaplan-cli 0.1.0 → 0.3.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 +94 -49
- package/dist/cli.js +269 -89
- package/dist/commands.js +192 -25
- package/dist/config.d.ts +9 -0
- package/dist/config.js +62 -2
- package/dist/contracts.d.ts +1 -1
- package/dist/contracts.js +1 -1
- package/dist/initiatives.d.ts +16 -0
- package/dist/initiatives.js +241 -0
- package/dist/issues.d.ts +2 -0
- package/dist/issues.js +97 -6
- package/dist/output.d.ts +12 -0
- package/dist/output.js +261 -0
- package/dist/project-config.d.ts +2 -0
- package/dist/project-config.js +56 -0
- package/dist/writes.d.ts +3 -3
- package/dist/writes.js +6 -6
- package/package.json +2 -1
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { CliError } from "./contracts.js";
|
|
3
|
+
import { getProject, integer, me, object, objects, positive, projectRef, publicData, readBody } from "./issues.js";
|
|
4
|
+
import { applied, preview, verificationFailed, withWrite, writeFailure } from "./writes.js";
|
|
5
|
+
const statuses = ["proposed", "planned", "active", "completed", "canceled"];
|
|
6
|
+
const record = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
|
+
function unsupported(message) {
|
|
8
|
+
throw new CliError("UNSUPPORTED_RESPONSE", message);
|
|
9
|
+
}
|
|
10
|
+
function selectedRef(ctx, options) {
|
|
11
|
+
const ref = options.project ?? ctx.options.project ?? ctx.project;
|
|
12
|
+
return ref === undefined ? undefined : projectRef(ref);
|
|
13
|
+
}
|
|
14
|
+
async function selectedProject(ctx, options) {
|
|
15
|
+
const ref = selectedRef(ctx, options);
|
|
16
|
+
if (ref === undefined)
|
|
17
|
+
throw new CliError("PROJECT_REQUIRED", "Provide --project team.KEY or configure a project");
|
|
18
|
+
return getProject(ctx, ref);
|
|
19
|
+
}
|
|
20
|
+
function initiative(value, id) {
|
|
21
|
+
const item = object(value, "initiative");
|
|
22
|
+
if (!positive(item.id) || !positive(item.projectId))
|
|
23
|
+
unsupported("Initiative response is missing stable identifiers");
|
|
24
|
+
if (id !== undefined && item.id !== id)
|
|
25
|
+
unsupported("Initiative response has a different ID");
|
|
26
|
+
return item;
|
|
27
|
+
}
|
|
28
|
+
async function projectFor(ctx, options, item) {
|
|
29
|
+
let ref = selectedRef(ctx, options);
|
|
30
|
+
if (ref === undefined) {
|
|
31
|
+
const projects = objects(await ctx.client.request("/projects"), "projects");
|
|
32
|
+
const matches = projects.filter((project) => project.id === item.projectId);
|
|
33
|
+
if (matches.length !== 1 || typeof matches[0].ref !== "string")
|
|
34
|
+
unsupported("Cannot resolve the initiative project; provide --project team.KEY");
|
|
35
|
+
ref = matches[0].ref;
|
|
36
|
+
}
|
|
37
|
+
const setup = await getProject(ctx, ref);
|
|
38
|
+
if (setup.project.id !== item.projectId)
|
|
39
|
+
throw new CliError("PROJECT_CONFLICT", "The initiative does not belong to the selected project");
|
|
40
|
+
return setup;
|
|
41
|
+
}
|
|
42
|
+
function canonical(setup) {
|
|
43
|
+
return projectRef(String(setup.project.ref));
|
|
44
|
+
}
|
|
45
|
+
function validateStatus(value, multiple) {
|
|
46
|
+
const values = multiple ? value.split(",") : [value];
|
|
47
|
+
if (!values.length || values.some((entry) => !statuses.includes(entry)))
|
|
48
|
+
throw new CliError("INVALID_STATUS", `--status must be ${statuses.join("|")}${multiple ? " or a comma-separated subset" : ""}`);
|
|
49
|
+
}
|
|
50
|
+
function labelId(setup, input) {
|
|
51
|
+
const matches = setup.labels.filter((entry) => entry.name === input || String(entry.id) === input);
|
|
52
|
+
if (!matches.length)
|
|
53
|
+
throw new CliError("VALUE_NOT_FOUND", `Label ${JSON.stringify(input)} is not available in this project`);
|
|
54
|
+
if (matches.length !== 1)
|
|
55
|
+
throw new CliError("AMBIGUOUS_VALUE", `Label ${JSON.stringify(input)} is ambiguous`);
|
|
56
|
+
return Number(matches[0].id);
|
|
57
|
+
}
|
|
58
|
+
async function ownerId(ctx, setup, value) {
|
|
59
|
+
const input = value === "@me" ? String((await me(ctx)).id) : value.replace(/^@/, "");
|
|
60
|
+
const matches = setup.assignees.filter((entry) => entry.kind === "member" && (entry.userId === input || entry.username === input));
|
|
61
|
+
if (!matches.length)
|
|
62
|
+
throw new CliError("VALUE_NOT_FOUND", `Owner ${JSON.stringify(value)} is not available in this project`);
|
|
63
|
+
if (matches.length !== 1)
|
|
64
|
+
throw new CliError("AMBIGUOUS_VALUE", `Owner ${JSON.stringify(value)} is ambiguous`);
|
|
65
|
+
return String(matches[0].userId);
|
|
66
|
+
}
|
|
67
|
+
function signedDescription(body, actor, editing) {
|
|
68
|
+
let value = body;
|
|
69
|
+
if (actor.kind === "agent") {
|
|
70
|
+
value = value.trimEnd();
|
|
71
|
+
const footer = /(?:^|\r?\n[ \t]*\r?\n)— (?:by|last edited by) (?:Codex|Claude) via iap$/u;
|
|
72
|
+
while (footer.test(value))
|
|
73
|
+
value = value.replace(footer, "").trimEnd();
|
|
74
|
+
value = `${value}\n\n— ${editing ? "last edited by" : "by"} ${actor.name === "codex" ? "Codex" : "Claude"} via iap`;
|
|
75
|
+
}
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
function date(value, flag) {
|
|
79
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value) || Number.isNaN(Date.parse(`${value}T00:00:00Z`)) || new Date(`${value}T00:00:00Z`).toISOString().slice(0, 10) !== value)
|
|
80
|
+
throw new CliError("INVALID_DATE", `${flag} must be a valid YYYY-MM-DD date`);
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
async function fields(ctx, setup, options, actor, item) {
|
|
84
|
+
for (const [value, remove, flag] of [
|
|
85
|
+
[options.owner, options.removeOwner, "owner"],
|
|
86
|
+
[options.priority, options.removePriority, "priority"],
|
|
87
|
+
[options.startDate, options.removeStartDate, "start-date"],
|
|
88
|
+
[options.targetDate, options.removeTargetDate, "target-date"],
|
|
89
|
+
]) {
|
|
90
|
+
if (value !== undefined && remove)
|
|
91
|
+
throw new CliError("CONFLICTING_ARGUMENTS", `--${flag} and --remove-${flag} conflict`);
|
|
92
|
+
}
|
|
93
|
+
const body = {};
|
|
94
|
+
if (options.title !== undefined) {
|
|
95
|
+
if (!options.title.trim())
|
|
96
|
+
throw new CliError("INVALID_TITLE", "--title must not be empty");
|
|
97
|
+
body.title = options.title;
|
|
98
|
+
}
|
|
99
|
+
const description = await readBody(options);
|
|
100
|
+
if (description !== undefined)
|
|
101
|
+
body.description = signedDescription(description, actor, item !== undefined);
|
|
102
|
+
if (options.status !== undefined) {
|
|
103
|
+
validateStatus(options.status, false);
|
|
104
|
+
body.status = options.status;
|
|
105
|
+
}
|
|
106
|
+
if (options.owner !== undefined)
|
|
107
|
+
body.ownerUserId = await ownerId(ctx, setup, options.owner);
|
|
108
|
+
if (options.removeOwner)
|
|
109
|
+
body.ownerUserId = null;
|
|
110
|
+
if (options.priority !== undefined) {
|
|
111
|
+
if (!["urgent", "high", "medium", "low"].includes(options.priority))
|
|
112
|
+
throw new CliError("INVALID_PRIORITY", "--priority must be urgent|high|medium|low");
|
|
113
|
+
body.priority = options.priority;
|
|
114
|
+
}
|
|
115
|
+
if (options.removePriority)
|
|
116
|
+
body.priority = null;
|
|
117
|
+
if (options.startDate !== undefined)
|
|
118
|
+
body.startDate = date(options.startDate, "--start-date");
|
|
119
|
+
if (options.removeStartDate)
|
|
120
|
+
body.startDate = null;
|
|
121
|
+
if (options.targetDate !== undefined)
|
|
122
|
+
body.targetDate = date(options.targetDate, "--target-date");
|
|
123
|
+
if (options.removeTargetDate)
|
|
124
|
+
body.targetDate = null;
|
|
125
|
+
const add = [...(options.label ?? []), ...(options.addLabel ?? [])].map((name) => labelId(setup, name));
|
|
126
|
+
const remove = (options.removeLabel ?? []).map((name) => labelId(setup, name));
|
|
127
|
+
if (add.some((id) => remove.includes(id)))
|
|
128
|
+
throw new CliError("CONFLICTING_ARGUMENTS", "The same label cannot be added and removed");
|
|
129
|
+
if (add.length || remove.length) {
|
|
130
|
+
if (item && (!Array.isArray(item.labelIds) || !item.labelIds.every(positive)))
|
|
131
|
+
unsupported("Initiative response is missing labelIds; cannot preserve existing labels");
|
|
132
|
+
body.labelIds = [...new Set([...(item ? item.labelIds.filter((id) => !remove.includes(id)) : []), ...add])];
|
|
133
|
+
}
|
|
134
|
+
return body;
|
|
135
|
+
}
|
|
136
|
+
function matches(value, id, projectId, body) {
|
|
137
|
+
if (!record(value) || !positive(value.id) || value.id !== id || value.projectId !== projectId)
|
|
138
|
+
return false;
|
|
139
|
+
return Object.entries(body).every(([key, expected]) => {
|
|
140
|
+
const actual = value[key];
|
|
141
|
+
if (key === "labelIds" && Array.isArray(actual) && Array.isArray(expected))
|
|
142
|
+
return actual.every(positive) && isDeepStrictEqual([...actual].sort((a, b) => a - b), [...expected].sort((a, b) => a - b));
|
|
143
|
+
return isDeepStrictEqual(actual, expected);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
async function list(ctx, options) {
|
|
147
|
+
const page = integer(options.page ?? 1, "--page");
|
|
148
|
+
const pageSize = integer(options.limit ?? 25, "--limit", 100);
|
|
149
|
+
if (options.status !== undefined)
|
|
150
|
+
validateStatus(options.status, true);
|
|
151
|
+
if (options.sort !== undefined && !["title", "priority", "targetDate", "owner"].includes(options.sort))
|
|
152
|
+
throw new CliError("INVALID_SORT", "--sort must be title|priority|targetDate|owner");
|
|
153
|
+
if (options.dir !== undefined && !["asc", "desc"].includes(options.dir))
|
|
154
|
+
throw new CliError("INVALID_DIRECTION", "--dir must be asc|desc");
|
|
155
|
+
const setup = await selectedProject(ctx, options);
|
|
156
|
+
const response = object(await ctx.client.request(`/projects/${encodeURIComponent(canonical(setup))}/initiatives`, { query: { page, pageSize, status: options.status, search: options.search, sort: options.sort, dir: options.dir } }), "initiative page");
|
|
157
|
+
const items = objects(response.items, "initiative items");
|
|
158
|
+
if (!Number.isSafeInteger(response.total) || response.total < 0 || response.page !== page || response.pageSize !== pageSize || items.length > pageSize || items.some((item) => !positive(item.id) || item.projectId !== setup.project.id))
|
|
159
|
+
unsupported("Invalid initiative page or foreign initiative");
|
|
160
|
+
const total = response.total;
|
|
161
|
+
const offset = (page - 1) * pageSize;
|
|
162
|
+
if (!Number.isSafeInteger(offset))
|
|
163
|
+
unsupported("Initiative page offset is invalid");
|
|
164
|
+
const nextPage = offset + pageSize < total ? page + 1 : null;
|
|
165
|
+
return { data: { items: publicData(items), total, page, pageSize, nextPage }, meta: { scope: { projectId: setup.project.id, projectRef: canonical(setup) }, limit: pageSize, returned: items.length, completeness: page === 1 && items.length === total ? "complete" : "partial" } };
|
|
166
|
+
}
|
|
167
|
+
async function writeInitiative(ctx, action, identifier, options, actor) {
|
|
168
|
+
const editing = action === "edit";
|
|
169
|
+
if (!editing && identifier !== undefined)
|
|
170
|
+
throw new CliError("CONFLICTING_ARGUMENTS", "initiative create does not accept an ID");
|
|
171
|
+
let item;
|
|
172
|
+
let setup;
|
|
173
|
+
if (editing) {
|
|
174
|
+
if (identifier === undefined)
|
|
175
|
+
throw new CliError("INITIATIVE_REQUIRED", "Provide a numeric initiative ID");
|
|
176
|
+
const id = integer(identifier, "initiative ID");
|
|
177
|
+
item = initiative(await ctx.client.request(`/initiatives/${id}`), id);
|
|
178
|
+
setup = await projectFor(ctx, options, item);
|
|
179
|
+
}
|
|
180
|
+
else
|
|
181
|
+
setup = await selectedProject(ctx, options);
|
|
182
|
+
const body = await fields(ctx, setup, options, actor, item);
|
|
183
|
+
if (!editing && body.title === undefined)
|
|
184
|
+
throw new CliError("TITLE_REQUIRED", "Provide --title");
|
|
185
|
+
if (editing && Object.keys(body).length === 0)
|
|
186
|
+
throw new CliError("NO_CHANGES", "Provide at least one initiative edit field");
|
|
187
|
+
if (item)
|
|
188
|
+
for (const key of Object.keys(body))
|
|
189
|
+
if (!(key in item))
|
|
190
|
+
unsupported(`Initiative response is missing ${key}; cannot build a trustworthy diff`);
|
|
191
|
+
const path = editing ? `/initiatives/${item.id}` : `/projects/${encodeURIComponent(canonical(setup))}/initiatives`;
|
|
192
|
+
const method = editing ? "PATCH" : "POST";
|
|
193
|
+
const diff = Object.fromEntries(Object.entries(body).map(([key, value]) => [key, { before: item ? item[key] : null, after: value }]));
|
|
194
|
+
const data = { action, projectId: setup.project.id, projectRef: canonical(setup), ...(item ? { initiativeId: item.id } : {}), changed: false, diff };
|
|
195
|
+
if (options.dryRun)
|
|
196
|
+
return preview(ctx, actor, data, path, body, [], method);
|
|
197
|
+
let response;
|
|
198
|
+
try {
|
|
199
|
+
response = await ctx.client.request(path, { method, body });
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
return writeFailure(error, data, actor, "initiative");
|
|
203
|
+
}
|
|
204
|
+
data.changed = true;
|
|
205
|
+
if (!editing && record(response) && positive(response.id))
|
|
206
|
+
data.initiativeId = response.id;
|
|
207
|
+
if (matches(response, data.initiativeId, setup.project.id, body))
|
|
208
|
+
return applied(data, actor);
|
|
209
|
+
if (positive(data.initiativeId)) {
|
|
210
|
+
try {
|
|
211
|
+
if (matches(await ctx.client.request(`/initiatives/${data.initiativeId}`), data.initiativeId, setup.project.id, body))
|
|
212
|
+
return applied(data, actor);
|
|
213
|
+
}
|
|
214
|
+
catch { /* The accepted write retains its ID even if verification fails. */ }
|
|
215
|
+
}
|
|
216
|
+
return verificationFailed(data, actor, "initiative");
|
|
217
|
+
}
|
|
218
|
+
export async function runInitiative(ctx, action, initiativeId, options) {
|
|
219
|
+
if (action === "list") {
|
|
220
|
+
if (initiativeId !== undefined)
|
|
221
|
+
throw new CliError("CONFLICTING_ARGUMENTS", "initiative list does not accept an ID");
|
|
222
|
+
return list(ctx, options);
|
|
223
|
+
}
|
|
224
|
+
if (action === "view") {
|
|
225
|
+
if (initiativeId === undefined)
|
|
226
|
+
throw new CliError("INITIATIVE_REQUIRED", "Provide a numeric initiative ID");
|
|
227
|
+
const id = integer(initiativeId, "initiative ID");
|
|
228
|
+
const item = initiative(await ctx.client.request(`/initiatives/${id}`), id);
|
|
229
|
+
const setup = await projectFor(ctx, options, item);
|
|
230
|
+
return { data: { initiative: publicData(item), projectRef: canonical(setup) }, meta: { scope: { initiativeId: id, projectId: setup.project.id, projectRef: canonical(setup) } } };
|
|
231
|
+
}
|
|
232
|
+
if (action === "create" || action === "edit") {
|
|
233
|
+
const result = await withWrite(options, (actor) => writeInitiative(ctx, action, initiativeId, options, actor));
|
|
234
|
+
if (record(result.data)) {
|
|
235
|
+
const { initiativeId: id, projectId, projectRef: ref } = result.data;
|
|
236
|
+
result.meta = { ...result.meta, scope: { ...(id !== undefined ? { initiativeId: id } : {}), projectId, projectRef: ref } };
|
|
237
|
+
}
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
throw new CliError("INVALID_ARGUMENT", "Unknown initiative action");
|
|
241
|
+
}
|
package/dist/issues.d.ts
CHANGED
package/dist/issues.js
CHANGED
|
@@ -627,9 +627,12 @@ export async function runIssue(ctx, action, identifier, options) {
|
|
|
627
627
|
throw new CliError("UNSUPPORTED_FILTER_COMBINATION", "--search cannot be combined with --assignee");
|
|
628
628
|
if (options.search !== undefined && !options.search.trim())
|
|
629
629
|
throw new CliError("INVALID_SEARCH", "--search must not be empty");
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
630
|
+
const setup = options.assignee !== undefined || options.includeState
|
|
631
|
+
? await getProject(ctx, ref)
|
|
632
|
+
: undefined;
|
|
633
|
+
const assigneeUserId = options.assignee !== undefined && setup
|
|
634
|
+
? await assignee(ctx, setup, options.assignee)
|
|
635
|
+
: undefined;
|
|
633
636
|
const items = objects(await ctx.client.request(`/projects/${encodeURIComponent(ref)}/issues${options.search !== undefined ? "/search" : ""}`, {
|
|
634
637
|
query: {
|
|
635
638
|
limit,
|
|
@@ -642,14 +645,57 @@ export async function runIssue(ctx, action, identifier, options) {
|
|
|
642
645
|
typeof entry.identifier !== "string" ||
|
|
643
646
|
typeof entry.title !== "string"))
|
|
644
647
|
unsupported("List response is missing issue identifiers or titles");
|
|
648
|
+
let selectedItems = items;
|
|
649
|
+
if (options.includeState && setup) {
|
|
650
|
+
if (items.some((entry) => !positive(entry.columnId)))
|
|
651
|
+
unsupported("List response is missing issue state identifiers");
|
|
652
|
+
const columns = new Map(setup.columns.map(({ id, name, stateType }) => [
|
|
653
|
+
id,
|
|
654
|
+
{ id, name, stateType },
|
|
655
|
+
]));
|
|
656
|
+
selectedItems = items.map((entry) => ({
|
|
657
|
+
...entry,
|
|
658
|
+
state: columns.get(entry.columnId) ?? {
|
|
659
|
+
id: entry.columnId,
|
|
660
|
+
unavailable: true,
|
|
661
|
+
},
|
|
662
|
+
}));
|
|
663
|
+
}
|
|
645
664
|
return {
|
|
646
|
-
data: { items: publicData(
|
|
647
|
-
meta: {
|
|
665
|
+
data: { items: publicData(selectedItems), nextCursor: null },
|
|
666
|
+
meta: {
|
|
667
|
+
limit,
|
|
668
|
+
returned: items.length,
|
|
669
|
+
completeness: "unknown",
|
|
670
|
+
...(options.outputSelected
|
|
671
|
+
? {
|
|
672
|
+
scope: {
|
|
673
|
+
projectRef: setup?.project.ref ?? ref,
|
|
674
|
+
...(options.search !== undefined
|
|
675
|
+
? { search: options.search }
|
|
676
|
+
: {}),
|
|
677
|
+
...(options.assignee !== undefined
|
|
678
|
+
? { assignee: options.assignee, assigneeUserId }
|
|
679
|
+
: {}),
|
|
680
|
+
},
|
|
681
|
+
}
|
|
682
|
+
: {}),
|
|
683
|
+
},
|
|
648
684
|
};
|
|
649
685
|
}
|
|
650
686
|
if (!["resolve", "view", "activity", "comments"].includes(action))
|
|
651
687
|
throw new CliError("UNKNOWN_ACTION", `Unsupported issue action: ${action}`);
|
|
652
688
|
const { item, ref } = await resolve(ctx, identifier, options);
|
|
689
|
+
const selectedMeta = options.outputSelected
|
|
690
|
+
? {
|
|
691
|
+
scope: {
|
|
692
|
+
issueId: item.id,
|
|
693
|
+
identifier: item.identifier,
|
|
694
|
+
...(ref ? { projectRef: ref } : {}),
|
|
695
|
+
projectId: item.projectId,
|
|
696
|
+
},
|
|
697
|
+
}
|
|
698
|
+
: {};
|
|
653
699
|
if (action === "resolve")
|
|
654
700
|
return {
|
|
655
701
|
data: publicData({
|
|
@@ -660,6 +706,7 @@ export async function runIssue(ctx, action, identifier, options) {
|
|
|
660
706
|
sequenceNumber: item.sequenceNumber,
|
|
661
707
|
...(ref ? { projectRef: ref } : {}),
|
|
662
708
|
}),
|
|
709
|
+
...(options.outputSelected ? { meta: selectedMeta } : {}),
|
|
663
710
|
};
|
|
664
711
|
if (action === "activity" || action === "comments") {
|
|
665
712
|
const limit = options.limit === undefined ? 25 : integer(options.limit, "--limit", 100);
|
|
@@ -673,10 +720,27 @@ export async function runIssue(ctx, action, identifier, options) {
|
|
|
673
720
|
limit,
|
|
674
721
|
returned: page.items.length,
|
|
675
722
|
completeness: page.nextCursor === null ? "complete" : "partial",
|
|
723
|
+
...selectedMeta,
|
|
724
|
+
...(options.outputSelected
|
|
725
|
+
? {
|
|
726
|
+
read: {
|
|
727
|
+
entries: {
|
|
728
|
+
status: "read_page",
|
|
729
|
+
contentRead: true,
|
|
730
|
+
filter: action === "comments" ? "comments" : "all",
|
|
731
|
+
cursor: options.cursor ?? null,
|
|
732
|
+
nextCursor: encodeCursor(page.nextCursor),
|
|
733
|
+
},
|
|
734
|
+
attachments: { status: "not_read", contentRead: false },
|
|
735
|
+
},
|
|
736
|
+
}
|
|
737
|
+
: {}),
|
|
676
738
|
},
|
|
677
739
|
};
|
|
678
740
|
}
|
|
679
741
|
const setup = await setupFor(ctx, item, ref);
|
|
742
|
+
if (selectedMeta.scope)
|
|
743
|
+
selectedMeta.scope.projectRef = String(setup.project.ref);
|
|
680
744
|
const attachments = objects(await ctx.client.request(`/issues/${item.id}/attachments`), "attachments");
|
|
681
745
|
if (attachments.some((entry) => typeof entry.id !== "string" ||
|
|
682
746
|
typeof entry.filename !== "string" ||
|
|
@@ -685,6 +749,18 @@ export async function runIssue(ctx, action, identifier, options) {
|
|
|
685
749
|
unsupported("Invalid attachment metadata");
|
|
686
750
|
const data = object(enriched(item, setup), "enriched issue");
|
|
687
751
|
data.attachments = publicData(attachments);
|
|
752
|
+
const read = options.outputSelected
|
|
753
|
+
? {
|
|
754
|
+
read: {
|
|
755
|
+
comments: { status: "not_read" },
|
|
756
|
+
attachments: {
|
|
757
|
+
status: "metadata_only",
|
|
758
|
+
returned: attachments.length,
|
|
759
|
+
contentRead: false,
|
|
760
|
+
},
|
|
761
|
+
},
|
|
762
|
+
}
|
|
763
|
+
: {};
|
|
688
764
|
if (options.comments) {
|
|
689
765
|
const page = await feed(ctx, item.id, 25, undefined, true);
|
|
690
766
|
data.comments = {
|
|
@@ -694,16 +770,31 @@ export async function runIssue(ctx, action, identifier, options) {
|
|
|
694
770
|
return {
|
|
695
771
|
data,
|
|
696
772
|
meta: {
|
|
773
|
+
...selectedMeta,
|
|
697
774
|
comments: {
|
|
698
775
|
limit: 25,
|
|
699
776
|
returned: page.items.length,
|
|
700
777
|
completeness: page.nextCursor === null ? "complete" : "partial",
|
|
701
778
|
},
|
|
702
779
|
attachmentsContentRead: false,
|
|
780
|
+
...(options.outputSelected
|
|
781
|
+
? {
|
|
782
|
+
read: {
|
|
783
|
+
...read.read,
|
|
784
|
+
comments: {
|
|
785
|
+
status: "read_page",
|
|
786
|
+
nextCursor: encodeCursor(page.nextCursor),
|
|
787
|
+
},
|
|
788
|
+
},
|
|
789
|
+
}
|
|
790
|
+
: {}),
|
|
703
791
|
},
|
|
704
792
|
};
|
|
705
793
|
}
|
|
706
|
-
return {
|
|
794
|
+
return {
|
|
795
|
+
data,
|
|
796
|
+
meta: { attachmentsContentRead: false, ...selectedMeta, ...read },
|
|
797
|
+
};
|
|
707
798
|
}
|
|
708
799
|
// Shared resource workflows use the same identity, scope, input and outcome rules.
|
|
709
800
|
export { actorFor, readBody, me, integer, object, objects, positive, projectRef, publicData, getProject, resolve, setupFor, writeFailure, verificationFailed, };
|
package/dist/output.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Result } from "./contracts.js";
|
|
2
|
+
export type OutputKind = "project.list" | "project.view" | "issue.list" | "issue.view" | "issue.resolve" | "issue.comments" | "issue.activity";
|
|
3
|
+
export interface OutputOptions {
|
|
4
|
+
summary?: boolean;
|
|
5
|
+
fields?: string;
|
|
6
|
+
}
|
|
7
|
+
/** Validate against command schemas even when the eventual result is empty. */
|
|
8
|
+
export declare function validateOutput(kind: OutputKind, options: OutputOptions): void;
|
|
9
|
+
/** Only opt-in issue list state output needs project state lookup enrichment. */
|
|
10
|
+
export declare function needsIssueStates(kind: OutputKind, options: OutputOptions): boolean;
|
|
11
|
+
/** Shape data only: serialization, redaction and fetching belong to callers. */
|
|
12
|
+
export declare function projectOutput(kind: OutputKind, result: Result, options: OutputOptions): Result;
|