@aaronshaf/plane 0.1.7 → 0.1.10
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 +1 -1
- package/src/commands/cycles.ts +86 -63
- package/src/commands/intake.ts +64 -44
- package/src/commands/issue.ts +386 -301
- package/src/commands/issues.ts +58 -45
- package/src/commands/labels.ts +6 -2
- package/src/commands/modules.ts +108 -74
- package/src/commands/pages.ts +42 -30
- package/src/config.ts +1 -1
- package/src/output.ts +12 -9
- package/src/resolve.ts +17 -5
- package/tests/api.test.ts +12 -8
- package/tests/cycles-extended.test.ts +15 -15
- package/tests/intake.test.ts +12 -12
- package/tests/issue-activity.test.ts +8 -16
- package/tests/issue-commands.test.ts +212 -278
- package/tests/issue-comments-worklogs.test.ts +31 -38
- package/tests/issue-links.test.ts +20 -23
- package/tests/modules.test.ts +17 -23
- package/tests/output.test.ts +11 -0
- package/tests/pages.test.ts +8 -8
package/src/commands/issues.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command, Options, Args } from "@effect/cli";
|
|
2
|
-
import { Console, Effect } from "effect";
|
|
2
|
+
import { Console, Effect, Option } from "effect";
|
|
3
3
|
import { api, decodeOrFail } from "../api.js";
|
|
4
4
|
import { IssuesResponseSchema } from "../config.js";
|
|
5
5
|
import { formatIssue } from "../format.js";
|
|
@@ -29,6 +29,62 @@ const priorityOption = Options.optional(
|
|
|
29
29
|
Options.choice("priority", ["urgent", "high", "medium", "low", "none"]),
|
|
30
30
|
).pipe(Options.withDescription("Filter by priority"));
|
|
31
31
|
|
|
32
|
+
export function issuesListHandler({
|
|
33
|
+
project,
|
|
34
|
+
state,
|
|
35
|
+
assignee,
|
|
36
|
+
priority,
|
|
37
|
+
}: {
|
|
38
|
+
project: string;
|
|
39
|
+
state: Option.Option<string>;
|
|
40
|
+
assignee: Option.Option<string>;
|
|
41
|
+
priority: Option.Option<string>;
|
|
42
|
+
}) {
|
|
43
|
+
return Effect.gen(function* () {
|
|
44
|
+
const { key, id } = yield* resolveProject(project);
|
|
45
|
+
const raw = yield* api.get(`projects/${id}/issues/?order_by=sequence_id`);
|
|
46
|
+
const { results } = yield* decodeOrFail(IssuesResponseSchema, raw);
|
|
47
|
+
|
|
48
|
+
let filtered = results;
|
|
49
|
+
|
|
50
|
+
if (state._tag === "Some") {
|
|
51
|
+
filtered = filtered.filter((i) => {
|
|
52
|
+
const s = i.state as State | string;
|
|
53
|
+
if (typeof s !== "object") return false;
|
|
54
|
+
const val = state.value.toLowerCase();
|
|
55
|
+
return s.group === val || s.name.toLowerCase() === val;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (assignee._tag === "Some") {
|
|
60
|
+
const isUuid =
|
|
61
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
|
|
62
|
+
assignee.value,
|
|
63
|
+
);
|
|
64
|
+
const memberId = isUuid
|
|
65
|
+
? assignee.value
|
|
66
|
+
: yield* getMemberId(assignee.value);
|
|
67
|
+
filtered = filtered.filter(
|
|
68
|
+
(i) => Array.isArray(i.assignees) && i.assignees.includes(memberId),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (priority._tag === "Some") {
|
|
73
|
+
filtered = filtered.filter((i) => i.priority === priority.value);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (jsonMode) {
|
|
77
|
+
yield* Console.log(JSON.stringify(filtered, null, 2));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (xmlMode) {
|
|
81
|
+
yield* Console.log(toXml(filtered));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
yield* Console.log(filtered.map((i) => formatIssue(i, key)).join("\n"));
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
32
88
|
export const issuesList = Command.make(
|
|
33
89
|
"list",
|
|
34
90
|
{
|
|
@@ -37,50 +93,7 @@ export const issuesList = Command.make(
|
|
|
37
93
|
priority: priorityOption,
|
|
38
94
|
project: projectArg,
|
|
39
95
|
},
|
|
40
|
-
|
|
41
|
-
Effect.gen(function* () {
|
|
42
|
-
const { key, id } = yield* resolveProject(project);
|
|
43
|
-
const raw = yield* api.get(`projects/${id}/issues/?order_by=sequence_id`);
|
|
44
|
-
const { results } = yield* decodeOrFail(IssuesResponseSchema, raw);
|
|
45
|
-
|
|
46
|
-
let filtered = results;
|
|
47
|
-
|
|
48
|
-
if (state._tag === "Some") {
|
|
49
|
-
filtered = filtered.filter((i) => {
|
|
50
|
-
const s = i.state as State | string;
|
|
51
|
-
if (typeof s !== "object") return false;
|
|
52
|
-
const val = state.value.toLowerCase();
|
|
53
|
-
return s.group === val || s.name.toLowerCase() === val;
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (assignee._tag === "Some") {
|
|
58
|
-
const isUuid =
|
|
59
|
-
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
|
|
60
|
-
assignee.value,
|
|
61
|
-
);
|
|
62
|
-
const memberId = isUuid
|
|
63
|
-
? assignee.value
|
|
64
|
-
: yield* getMemberId(assignee.value);
|
|
65
|
-
filtered = filtered.filter(
|
|
66
|
-
(i) => Array.isArray(i.assignees) && i.assignees.includes(memberId),
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (priority._tag === "Some") {
|
|
71
|
-
filtered = filtered.filter((i) => i.priority === priority.value);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (jsonMode) {
|
|
75
|
-
yield* Console.log(JSON.stringify(filtered, null, 2));
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
if (xmlMode) {
|
|
79
|
-
yield* Console.log(toXml(filtered));
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
yield* Console.log(filtered.map((i) => formatIssue(i, key)).join("\n"));
|
|
83
|
-
}),
|
|
96
|
+
issuesListHandler,
|
|
84
97
|
).pipe(
|
|
85
98
|
Command.withDescription(
|
|
86
99
|
"List issues for a project ordered by sequence ID. Each line shows: REF [state-group] state-name title",
|
package/src/commands/labels.ts
CHANGED
|
@@ -53,8 +53,12 @@ export const labelsCreate = Command.make(
|
|
|
53
53
|
({ project, name, color }) =>
|
|
54
54
|
Effect.gen(function* () {
|
|
55
55
|
const { id } = yield* resolveProject(project);
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
interface LabelPayload {
|
|
57
|
+
name: string;
|
|
58
|
+
color?: string;
|
|
59
|
+
}
|
|
60
|
+
const body: LabelPayload = { name };
|
|
61
|
+
if (color._tag === "Some") body.color = color.value;
|
|
58
62
|
const raw = yield* api.post(`projects/${id}/labels/`, body);
|
|
59
63
|
const label = yield* decodeOrFail(LabelSchema, raw);
|
|
60
64
|
yield* Console.log(`Created label: ${label.name} (${label.id})`);
|
package/src/commands/modules.ts
CHANGED
|
@@ -18,32 +18,35 @@ const moduleIdArg = Args.text({ name: "module-id" }).pipe(
|
|
|
18
18
|
|
|
19
19
|
// --- modules list ---
|
|
20
20
|
|
|
21
|
+
export function modulesListHandler({ project }: { project: string }) {
|
|
22
|
+
return Effect.gen(function* () {
|
|
23
|
+
const { id } = yield* resolveProject(project);
|
|
24
|
+
const raw = yield* api.get(`projects/${id}/modules/`);
|
|
25
|
+
const { results } = yield* decodeOrFail(ModulesResponseSchema, raw);
|
|
26
|
+
if (jsonMode) {
|
|
27
|
+
yield* Console.log(JSON.stringify(results, null, 2));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (xmlMode) {
|
|
31
|
+
yield* Console.log(toXml(results));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (results.length === 0) {
|
|
35
|
+
yield* Console.log("No modules found");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const lines = results.map((m) => {
|
|
39
|
+
const status = (m.status ?? "?").padEnd(12);
|
|
40
|
+
return `${m.id} ${status} ${m.name}`;
|
|
41
|
+
});
|
|
42
|
+
yield* Console.log(lines.join("\n"));
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
21
46
|
export const modulesList = Command.make(
|
|
22
47
|
"list",
|
|
23
48
|
{ project: projectArg },
|
|
24
|
-
|
|
25
|
-
Effect.gen(function* () {
|
|
26
|
-
const { id } = yield* resolveProject(project);
|
|
27
|
-
const raw = yield* api.get(`projects/${id}/modules/`);
|
|
28
|
-
const { results } = yield* decodeOrFail(ModulesResponseSchema, raw);
|
|
29
|
-
if (jsonMode) {
|
|
30
|
-
yield* Console.log(JSON.stringify(results, null, 2));
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
if (xmlMode) {
|
|
34
|
-
yield* Console.log(toXml(results));
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
if (results.length === 0) {
|
|
38
|
-
yield* Console.log("No modules found");
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
const lines = results.map((m) => {
|
|
42
|
-
const status = (m.status ?? "?").padEnd(12);
|
|
43
|
-
return `${m.id} ${status} ${m.name}`;
|
|
44
|
-
});
|
|
45
|
-
yield* Console.log(lines.join("\n"));
|
|
46
|
-
}),
|
|
49
|
+
modulesListHandler,
|
|
47
50
|
).pipe(
|
|
48
51
|
Command.withDescription(
|
|
49
52
|
"List modules for a project. Shows module UUID, status, and name.\n\nExample:\n plane modules list PROJ",
|
|
@@ -52,37 +55,46 @@ export const modulesList = Command.make(
|
|
|
52
55
|
|
|
53
56
|
// --- modules issues list ---
|
|
54
57
|
|
|
58
|
+
export function moduleIssuesListHandler({
|
|
59
|
+
project,
|
|
60
|
+
moduleId,
|
|
61
|
+
}: {
|
|
62
|
+
project: string;
|
|
63
|
+
moduleId: string;
|
|
64
|
+
}) {
|
|
65
|
+
return Effect.gen(function* () {
|
|
66
|
+
const { key, id } = yield* resolveProject(project);
|
|
67
|
+
const raw = yield* api.get(
|
|
68
|
+
`projects/${id}/modules/${moduleId}/module-issues/`,
|
|
69
|
+
);
|
|
70
|
+
const { results } = yield* decodeOrFail(ModuleIssuesResponseSchema, raw);
|
|
71
|
+
if (jsonMode) {
|
|
72
|
+
yield* Console.log(JSON.stringify(results, null, 2));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (xmlMode) {
|
|
76
|
+
yield* Console.log(toXml(results));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (results.length === 0) {
|
|
80
|
+
yield* Console.log("No issues in module");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const lines = results.map((mi) => {
|
|
84
|
+
if (mi.issue_detail) {
|
|
85
|
+
const seq = String(mi.issue_detail.sequence_id).padStart(3, " ");
|
|
86
|
+
return `${key}-${seq} ${mi.issue_detail.name} (${mi.id})`;
|
|
87
|
+
}
|
|
88
|
+
return `${mi.issue} (module-issue: ${mi.id})`;
|
|
89
|
+
});
|
|
90
|
+
yield* Console.log(lines.join("\n"));
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
55
94
|
export const moduleIssuesList = Command.make(
|
|
56
95
|
"list",
|
|
57
96
|
{ project: projectArg, moduleId: moduleIdArg },
|
|
58
|
-
|
|
59
|
-
Effect.gen(function* () {
|
|
60
|
-
const { key, id } = yield* resolveProject(project);
|
|
61
|
-
const raw = yield* api.get(
|
|
62
|
-
`projects/${id}/modules/${moduleId}/module-issues/`,
|
|
63
|
-
);
|
|
64
|
-
const { results } = yield* decodeOrFail(ModuleIssuesResponseSchema, raw);
|
|
65
|
-
if (jsonMode) {
|
|
66
|
-
yield* Console.log(JSON.stringify(results, null, 2));
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
if (xmlMode) {
|
|
70
|
-
yield* Console.log(toXml(results));
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
if (results.length === 0) {
|
|
74
|
-
yield* Console.log("No issues in module");
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
const lines = results.map((mi) => {
|
|
78
|
-
if (mi.issue_detail) {
|
|
79
|
-
const seq = String(mi.issue_detail.sequence_id).padStart(3, " ");
|
|
80
|
-
return `${key}-${seq} ${mi.issue_detail.name} (${mi.id})`;
|
|
81
|
-
}
|
|
82
|
-
return `${mi.issue} (module-issue: ${mi.id})`;
|
|
83
|
-
});
|
|
84
|
-
yield* Console.log(lines.join("\n"));
|
|
85
|
-
}),
|
|
97
|
+
moduleIssuesListHandler,
|
|
86
98
|
).pipe(
|
|
87
99
|
Command.withDescription(
|
|
88
100
|
"List issues in a module.\n\nExample:\n plane modules issues list PROJ <module-id>",
|
|
@@ -95,22 +107,33 @@ const issueRefArg = Args.text({ name: "ref" }).pipe(
|
|
|
95
107
|
Args.withDescription("Issue reference to add (e.g. PROJ-29)"),
|
|
96
108
|
);
|
|
97
109
|
|
|
110
|
+
export function moduleIssuesAddHandler({
|
|
111
|
+
project,
|
|
112
|
+
moduleId,
|
|
113
|
+
ref,
|
|
114
|
+
}: {
|
|
115
|
+
project: string;
|
|
116
|
+
moduleId: string;
|
|
117
|
+
ref: string;
|
|
118
|
+
}) {
|
|
119
|
+
return Effect.gen(function* () {
|
|
120
|
+
const { id: projectId } = yield* resolveProject(project);
|
|
121
|
+
const { seq } = yield* parseIssueRef(ref);
|
|
122
|
+
const issue = yield* findIssueBySeq(projectId, seq);
|
|
123
|
+
yield* api.post(
|
|
124
|
+
`projects/${projectId}/modules/${moduleId}/module-issues/`,
|
|
125
|
+
{
|
|
126
|
+
issues: [issue.id],
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
yield* Console.log(`Added ${ref} to module ${moduleId}`);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
98
133
|
export const moduleIssuesAdd = Command.make(
|
|
99
134
|
"add",
|
|
100
135
|
{ project: projectArg, moduleId: moduleIdArg, ref: issueRefArg },
|
|
101
|
-
|
|
102
|
-
Effect.gen(function* () {
|
|
103
|
-
const { id: projectId } = yield* resolveProject(project);
|
|
104
|
-
const { seq } = yield* parseIssueRef(ref);
|
|
105
|
-
const issue = yield* findIssueBySeq(projectId, seq);
|
|
106
|
-
yield* api.post(
|
|
107
|
-
`projects/${projectId}/modules/${moduleId}/module-issues/`,
|
|
108
|
-
{
|
|
109
|
-
issues: [issue.id],
|
|
110
|
-
},
|
|
111
|
-
);
|
|
112
|
-
yield* Console.log(`Added ${ref} to module ${moduleId}`);
|
|
113
|
-
}),
|
|
136
|
+
moduleIssuesAddHandler,
|
|
114
137
|
).pipe(
|
|
115
138
|
Command.withDescription(
|
|
116
139
|
"Add an issue to a module.\n\nExample:\n plane modules issues add PROJ <module-id> PROJ-29",
|
|
@@ -125,6 +148,26 @@ const moduleIssueIdArg = Args.text({ name: "module-issue-id" }).pipe(
|
|
|
125
148
|
),
|
|
126
149
|
);
|
|
127
150
|
|
|
151
|
+
export function moduleIssuesRemoveHandler({
|
|
152
|
+
project,
|
|
153
|
+
moduleId,
|
|
154
|
+
moduleIssueId,
|
|
155
|
+
}: {
|
|
156
|
+
project: string;
|
|
157
|
+
moduleId: string;
|
|
158
|
+
moduleIssueId: string;
|
|
159
|
+
}) {
|
|
160
|
+
return Effect.gen(function* () {
|
|
161
|
+
const { id } = yield* resolveProject(project);
|
|
162
|
+
yield* api.delete(
|
|
163
|
+
`projects/${id}/modules/${moduleId}/module-issues/${moduleIssueId}/`,
|
|
164
|
+
);
|
|
165
|
+
yield* Console.log(
|
|
166
|
+
`Removed module-issue ${moduleIssueId} from module ${moduleId}`,
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
128
171
|
export const moduleIssuesRemove = Command.make(
|
|
129
172
|
"remove",
|
|
130
173
|
{
|
|
@@ -132,16 +175,7 @@ export const moduleIssuesRemove = Command.make(
|
|
|
132
175
|
moduleId: moduleIdArg,
|
|
133
176
|
moduleIssueId: moduleIssueIdArg,
|
|
134
177
|
},
|
|
135
|
-
|
|
136
|
-
Effect.gen(function* () {
|
|
137
|
-
const { id } = yield* resolveProject(project);
|
|
138
|
-
yield* api.delete(
|
|
139
|
-
`projects/${id}/modules/${moduleId}/module-issues/${moduleIssueId}/`,
|
|
140
|
-
);
|
|
141
|
-
yield* Console.log(
|
|
142
|
-
`Removed module-issue ${moduleIssueId} from module ${moduleId}`,
|
|
143
|
-
);
|
|
144
|
-
}),
|
|
178
|
+
moduleIssuesRemoveHandler,
|
|
145
179
|
).pipe(
|
|
146
180
|
Command.withDescription(
|
|
147
181
|
"Remove an issue from a module using the module-issue join ID.\n\nExample:\n plane modules issues remove PROJ <module-id> <module-issue-id>",
|
package/src/commands/pages.ts
CHANGED
|
@@ -11,32 +11,35 @@ const projectArg = Args.text({ name: "project" }).pipe(
|
|
|
11
11
|
|
|
12
12
|
// --- pages list ---
|
|
13
13
|
|
|
14
|
+
export function pagesListHandler({ project }: { project: string }) {
|
|
15
|
+
return Effect.gen(function* () {
|
|
16
|
+
const { id } = yield* resolveProject(project);
|
|
17
|
+
const raw = yield* api.get(`projects/${id}/pages/`);
|
|
18
|
+
const { results } = yield* decodeOrFail(PagesResponseSchema, raw);
|
|
19
|
+
if (jsonMode) {
|
|
20
|
+
yield* Console.log(JSON.stringify(results, null, 2));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (xmlMode) {
|
|
24
|
+
yield* Console.log(toXml(results));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (results.length === 0) {
|
|
28
|
+
yield* Console.log("No pages");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const lines = results.map((p) => {
|
|
32
|
+
const updated = (p.updated_at ?? p.created_at).slice(0, 10);
|
|
33
|
+
return `${p.id} ${updated} ${p.name}`;
|
|
34
|
+
});
|
|
35
|
+
yield* Console.log(lines.join("\n"));
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
14
39
|
export const pagesList = Command.make(
|
|
15
40
|
"list",
|
|
16
41
|
{ project: projectArg },
|
|
17
|
-
|
|
18
|
-
Effect.gen(function* () {
|
|
19
|
-
const { id } = yield* resolveProject(project);
|
|
20
|
-
const raw = yield* api.get(`projects/${id}/pages/`);
|
|
21
|
-
const { results } = yield* decodeOrFail(PagesResponseSchema, raw);
|
|
22
|
-
if (jsonMode) {
|
|
23
|
-
yield* Console.log(JSON.stringify(results, null, 2));
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
if (xmlMode) {
|
|
27
|
-
yield* Console.log(toXml(results));
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
if (results.length === 0) {
|
|
31
|
-
yield* Console.log("No pages");
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
const lines = results.map((p) => {
|
|
35
|
-
const updated = (p.updated_at ?? p.created_at).slice(0, 10);
|
|
36
|
-
return `${p.id} ${updated} ${p.name}`;
|
|
37
|
-
});
|
|
38
|
-
yield* Console.log(lines.join("\n"));
|
|
39
|
-
}),
|
|
42
|
+
pagesListHandler,
|
|
40
43
|
).pipe(
|
|
41
44
|
Command.withDescription(
|
|
42
45
|
"List pages for a project. Shows page UUID, last updated date, and title.\n\nExample:\n plane pages list PROJ",
|
|
@@ -49,16 +52,25 @@ const pageIdArg = Args.text({ name: "page-id" }).pipe(
|
|
|
49
52
|
Args.withDescription("Page UUID (from 'plane pages list')"),
|
|
50
53
|
);
|
|
51
54
|
|
|
55
|
+
export function pagesGetHandler({
|
|
56
|
+
project,
|
|
57
|
+
pageId,
|
|
58
|
+
}: {
|
|
59
|
+
project: string;
|
|
60
|
+
pageId: string;
|
|
61
|
+
}) {
|
|
62
|
+
return Effect.gen(function* () {
|
|
63
|
+
const { id } = yield* resolveProject(project);
|
|
64
|
+
const raw = yield* api.get(`projects/${id}/pages/${pageId}/`);
|
|
65
|
+
const page = yield* decodeOrFail(PageSchema, raw);
|
|
66
|
+
yield* Console.log(JSON.stringify(page, null, 2));
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
52
70
|
export const pagesGet = Command.make(
|
|
53
71
|
"get",
|
|
54
72
|
{ project: projectArg, pageId: pageIdArg },
|
|
55
|
-
|
|
56
|
-
Effect.gen(function* () {
|
|
57
|
-
const { id } = yield* resolveProject(project);
|
|
58
|
-
const raw = yield* api.get(`projects/${id}/pages/${pageId}/`);
|
|
59
|
-
const page = yield* decodeOrFail(PageSchema, raw);
|
|
60
|
-
yield* Console.log(JSON.stringify(page, null, 2));
|
|
61
|
-
}),
|
|
73
|
+
pagesGetHandler,
|
|
62
74
|
).pipe(
|
|
63
75
|
Command.withDescription(
|
|
64
76
|
"Print full JSON for a page including description_html.\n\nExample:\n plane pages get PROJ <page-id>",
|
package/src/config.ts
CHANGED
|
@@ -18,7 +18,7 @@ export const IssueSchema = Schema.Struct({
|
|
|
18
18
|
state: Schema.Union(Schema.String, StateSchema),
|
|
19
19
|
assignees: Schema.optional(Schema.NullOr(Schema.Array(Schema.String))),
|
|
20
20
|
description_html: Schema.optional(Schema.NullOr(Schema.String)),
|
|
21
|
-
estimate_point: Schema.optional(Schema.NullOr(Schema.
|
|
21
|
+
estimate_point: Schema.optional(Schema.NullOr(Schema.String)),
|
|
22
22
|
});
|
|
23
23
|
export type Issue = typeof IssueSchema.Type;
|
|
24
24
|
|
package/src/output.ts
CHANGED
|
@@ -15,25 +15,28 @@ function escapeXml(val: unknown): string {
|
|
|
15
15
|
.replace(/"/g, """);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
function toXmlItem(obj:
|
|
19
|
-
|
|
20
|
-
return `<${tag}>${escapeXml(obj)}</${tag}>`;
|
|
21
|
-
}
|
|
22
|
-
const attrs = Object.entries(obj as Record<string, unknown>)
|
|
18
|
+
function toXmlItem(obj: Record<string, unknown>, tag = "item"): string {
|
|
19
|
+
const attrs = Object.entries(obj)
|
|
23
20
|
.filter(([, v]) => v === null || typeof v !== "object")
|
|
24
21
|
.map(([k, v]) => `${k}="${escapeXml(v)}"`)
|
|
25
22
|
.join(" ");
|
|
26
|
-
const children = Object.entries(obj
|
|
23
|
+
const children = Object.entries(obj)
|
|
27
24
|
.filter(([, v]) => v !== null && typeof v === "object")
|
|
28
25
|
.map(([k, v]) =>
|
|
29
26
|
Array.isArray(v)
|
|
30
|
-
? `<${k}>${v
|
|
31
|
-
|
|
27
|
+
? `<${k}>${v
|
|
28
|
+
.map((i) =>
|
|
29
|
+
typeof i === "object" && i !== null
|
|
30
|
+
? toXmlItem(i as Record<string, unknown>)
|
|
31
|
+
: `<item>${escapeXml(i)}</item>`,
|
|
32
|
+
)
|
|
33
|
+
.join("")}</${k}>`
|
|
34
|
+
: toXmlItem(v as Record<string, unknown>, k),
|
|
32
35
|
)
|
|
33
36
|
.join("");
|
|
34
37
|
return `<${tag}${attrs ? " " + attrs : ""}>${children}</${tag}>`;
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
export function toXml(results: readonly unknown[]): string {
|
|
38
|
-
return `<results>\n${results.map((r) => " " + toXmlItem(r)).join("\n")}\n</results>`;
|
|
41
|
+
return `<results>\n${results.map((r) => " " + toXmlItem(r as Record<string, unknown>)).join("\n")}\n</results>`;
|
|
39
42
|
}
|
package/src/resolve.ts
CHANGED
|
@@ -7,12 +7,13 @@ import {
|
|
|
7
7
|
ProjectsResponseSchema,
|
|
8
8
|
StatesResponseSchema,
|
|
9
9
|
} from "./config.js";
|
|
10
|
+
import type { Issue } from "./config.js";
|
|
10
11
|
|
|
11
12
|
// Cache project list within a process invocation
|
|
12
13
|
let _projectCache: Record<string, string> | null = null;
|
|
13
14
|
|
|
14
15
|
/** Clear the project cache — for use in tests only */
|
|
15
|
-
export function _clearProjectCache() {
|
|
16
|
+
export function _clearProjectCache(): void {
|
|
16
17
|
_projectCache = null;
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -66,7 +67,10 @@ export function parseIssueRef(
|
|
|
66
67
|
);
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
export function findIssueBySeq(
|
|
70
|
+
export function findIssueBySeq(
|
|
71
|
+
projectId: string,
|
|
72
|
+
seq: number,
|
|
73
|
+
): Effect.Effect<Issue, Error> {
|
|
70
74
|
return Effect.gen(function* () {
|
|
71
75
|
const raw = yield* api.get(`projects/${projectId}/issues/`);
|
|
72
76
|
const { results } = yield* decodeOrFail(IssuesResponseSchema, raw);
|
|
@@ -76,7 +80,9 @@ export function findIssueBySeq(projectId: string, seq: number) {
|
|
|
76
80
|
});
|
|
77
81
|
}
|
|
78
82
|
|
|
79
|
-
export function getMemberId(
|
|
83
|
+
export function getMemberId(
|
|
84
|
+
nameEmailOrId: string,
|
|
85
|
+
): Effect.Effect<string, Error> {
|
|
80
86
|
return Effect.gen(function* () {
|
|
81
87
|
const results = yield* decodeOrFail(
|
|
82
88
|
MembersResponseSchema,
|
|
@@ -97,7 +103,10 @@ export function getMemberId(nameEmailOrId: string) {
|
|
|
97
103
|
});
|
|
98
104
|
}
|
|
99
105
|
|
|
100
|
-
export function getStateId(
|
|
106
|
+
export function getStateId(
|
|
107
|
+
projectId: string,
|
|
108
|
+
nameOrGroup: string,
|
|
109
|
+
): Effect.Effect<string, Error> {
|
|
101
110
|
return Effect.gen(function* () {
|
|
102
111
|
const raw = yield* api.get(`projects/${projectId}/states/`);
|
|
103
112
|
const { results } = yield* decodeOrFail(StatesResponseSchema, raw);
|
|
@@ -111,7 +120,10 @@ export function getStateId(projectId: string, nameOrGroup: string) {
|
|
|
111
120
|
});
|
|
112
121
|
}
|
|
113
122
|
|
|
114
|
-
export function getLabelId(
|
|
123
|
+
export function getLabelId(
|
|
124
|
+
projectId: string,
|
|
125
|
+
name: string,
|
|
126
|
+
): Effect.Effect<string, Error> {
|
|
115
127
|
return Effect.gen(function* () {
|
|
116
128
|
const raw = yield* api.get(`projects/${projectId}/labels/`);
|
|
117
129
|
const { results } = yield* decodeOrFail(LabelsResponseSchema, raw);
|
package/tests/api.test.ts
CHANGED
|
@@ -43,8 +43,10 @@ describe("api.get", () => {
|
|
|
43
43
|
}),
|
|
44
44
|
),
|
|
45
45
|
);
|
|
46
|
-
const result = await Effect.runPromise(api.get("projects/"))
|
|
47
|
-
|
|
46
|
+
const result = (await Effect.runPromise(api.get("projects/"))) as {
|
|
47
|
+
results: unknown[];
|
|
48
|
+
};
|
|
49
|
+
expect(result.results).toHaveLength(1);
|
|
48
50
|
});
|
|
49
51
|
|
|
50
52
|
it("strips trailing slash from PLANE_HOST", async () => {
|
|
@@ -54,8 +56,10 @@ describe("api.get", () => {
|
|
|
54
56
|
HttpResponse.json({ results: [] }),
|
|
55
57
|
),
|
|
56
58
|
);
|
|
57
|
-
const result = await Effect.runPromise(api.get("projects/"))
|
|
58
|
-
|
|
59
|
+
const result = (await Effect.runPromise(api.get("projects/"))) as {
|
|
60
|
+
results: unknown[];
|
|
61
|
+
};
|
|
62
|
+
expect(result.results).toHaveLength(0);
|
|
59
63
|
});
|
|
60
64
|
|
|
61
65
|
it("appends expand=state for issues/ paths", async () => {
|
|
@@ -103,7 +107,7 @@ describe("api.post", () => {
|
|
|
103
107
|
http.post(
|
|
104
108
|
`${BASE}/api/v1/workspaces/${WS}/projects/p1/issues/`,
|
|
105
109
|
async ({ request }) => {
|
|
106
|
-
const body = (await request.json()) as
|
|
110
|
+
const body = (await request.json()) as { name?: string };
|
|
107
111
|
return HttpResponse.json({
|
|
108
112
|
id: "new-issue",
|
|
109
113
|
sequence_id: 99,
|
|
@@ -116,7 +120,7 @@ describe("api.post", () => {
|
|
|
116
120
|
);
|
|
117
121
|
const result = (await Effect.runPromise(
|
|
118
122
|
api.post("projects/p1/issues/", { name: "New Issue" }),
|
|
119
|
-
)) as
|
|
123
|
+
)) as { sequence_id: number; name: string };
|
|
120
124
|
expect(result.sequence_id).toBe(99);
|
|
121
125
|
expect(result.name).toBe("New Issue");
|
|
122
126
|
});
|
|
@@ -128,7 +132,7 @@ describe("api.patch", () => {
|
|
|
128
132
|
http.patch(
|
|
129
133
|
`${BASE}/api/v1/workspaces/${WS}/projects/p1/issues/i1/`,
|
|
130
134
|
async ({ request }) => {
|
|
131
|
-
const body = (await request.json()) as
|
|
135
|
+
const body = (await request.json()) as { priority?: string };
|
|
132
136
|
return HttpResponse.json({
|
|
133
137
|
id: "i1",
|
|
134
138
|
sequence_id: 1,
|
|
@@ -141,7 +145,7 @@ describe("api.patch", () => {
|
|
|
141
145
|
);
|
|
142
146
|
const result = (await Effect.runPromise(
|
|
143
147
|
api.patch("projects/p1/issues/i1/", { priority: "high" }),
|
|
144
|
-
)) as
|
|
148
|
+
)) as { priority: string };
|
|
145
149
|
expect(result.priority).toBe("high");
|
|
146
150
|
});
|
|
147
151
|
});
|