@aaronshaf/plane 0.1.3 → 0.1.5
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/scripts/check-coverage.ts +2 -2
- package/src/api.ts +67 -59
- package/src/app.ts +78 -0
- package/src/bin.ts +6 -71
- package/src/commands/cycles.ts +104 -85
- package/src/commands/init.ts +57 -55
- package/src/commands/intake.ts +82 -65
- package/src/commands/issue.ts +418 -314
- package/src/commands/issues.ts +51 -43
- package/src/commands/labels.ts +52 -43
- package/src/commands/members.ts +25 -19
- package/src/commands/modules.ts +136 -99
- package/src/commands/pages.ts +58 -49
- package/src/commands/projects.ts +28 -22
- package/src/commands/states.ts +31 -25
- package/src/config.ts +152 -154
- package/src/format.ts +15 -8
- package/src/output.ts +28 -28
- package/src/resolve.ts +66 -53
- package/tests/api.test.ts +178 -155
- package/tests/cycles-extended.test.ts +205 -162
- package/tests/format.test.ts +72 -54
- package/tests/helpers/mock-api.ts +16 -14
- package/tests/intake.test.ts +173 -139
- package/tests/issue-activity.test.ts +191 -158
- package/tests/issue-commands.test.ts +587 -304
- package/tests/issue-comments-worklogs.test.ts +337 -265
- package/tests/issue-links.test.ts +229 -193
- package/tests/modules.test.ts +283 -239
- package/tests/new-schemas.test.ts +203 -183
- package/tests/new-schemas2.test.ts +195 -183
- package/tests/output.test.ts +66 -64
- package/tests/pages.test.ts +122 -108
- package/tests/resolve.test.ts +186 -156
- package/tests/schemas.test.ts +215 -177
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -1,81 +1,89 @@
|
|
|
1
|
-
import { Effect, Schema } from "effect"
|
|
2
|
-
import * as fs from "node:fs"
|
|
3
|
-
import * as path from "node:path"
|
|
4
|
-
import * as os from "node:os"
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import * as os from "node:os";
|
|
5
5
|
|
|
6
|
-
const CONFIG_FILE = path.join(os.homedir(), ".config", "plane", "config.json")
|
|
6
|
+
const CONFIG_FILE = path.join(os.homedir(), ".config", "plane", "config.json");
|
|
7
7
|
|
|
8
|
-
function readConfigFile(): Partial<{
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
function readConfigFile(): Partial<{
|
|
9
|
+
token: string;
|
|
10
|
+
host: string;
|
|
11
|
+
workspace: string;
|
|
12
|
+
}> {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
|
|
15
|
+
} catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
function getConfig() {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const file = readConfigFile();
|
|
22
|
+
return {
|
|
23
|
+
token: process.env["PLANE_API_TOKEN"] ?? file.token ?? "",
|
|
24
|
+
host: (
|
|
25
|
+
process.env["PLANE_HOST"] ??
|
|
26
|
+
file.host ??
|
|
27
|
+
"https://plane.so"
|
|
28
|
+
).replace(/\/$/, ""),
|
|
29
|
+
workspace: process.env["PLANE_WORKSPACE"] ?? file.workspace ?? "",
|
|
30
|
+
};
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
function request(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
method: string,
|
|
35
|
+
path: string,
|
|
36
|
+
body?: unknown,
|
|
29
37
|
): Effect.Effect<unknown, Error> {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
return Effect.tryPromise({
|
|
39
|
+
try: async () => {
|
|
40
|
+
const { token, host, workspace } = getConfig();
|
|
41
|
+
let url = `${host}/api/v1/workspaces/${workspace}/${path}`;
|
|
34
42
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
// Always expand state on issue list/get calls (not intake-issues/ or cycle-issues/)
|
|
44
|
+
if (method === "GET" && /(?:^|\/)(issues\/)/.test(path)) {
|
|
45
|
+
url += url.includes("?") ? "&expand=state" : "?expand=state";
|
|
46
|
+
}
|
|
39
47
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
const headers: Record<string, string> = {
|
|
49
|
+
"X-Api-Key": token,
|
|
50
|
+
};
|
|
51
|
+
if (body !== undefined) {
|
|
52
|
+
headers["Content-Type"] = "application/json";
|
|
53
|
+
}
|
|
46
54
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
const res = await fetch(url, {
|
|
56
|
+
method,
|
|
57
|
+
headers,
|
|
58
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
59
|
+
});
|
|
52
60
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
const text = await res.text();
|
|
63
|
+
throw new Error(`HTTP ${res.status}: ${text}`);
|
|
64
|
+
}
|
|
57
65
|
|
|
58
|
-
|
|
59
|
-
|
|
66
|
+
// 204 No Content
|
|
67
|
+
if (res.status === 204) return null;
|
|
60
68
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
69
|
+
return res.json();
|
|
70
|
+
},
|
|
71
|
+
catch: (e) => (e instanceof Error ? e : new Error(String(e))),
|
|
72
|
+
});
|
|
65
73
|
}
|
|
66
74
|
|
|
67
75
|
export const api = {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
76
|
+
get: (path: string) => request("GET", path),
|
|
77
|
+
post: (path: string, body: unknown) => request("POST", path, body),
|
|
78
|
+
patch: (path: string, body: unknown) => request("PATCH", path, body),
|
|
79
|
+
delete: (path: string) => request("DELETE", path),
|
|
80
|
+
};
|
|
73
81
|
|
|
74
82
|
export function decodeOrFail<A, I>(
|
|
75
|
-
|
|
76
|
-
|
|
83
|
+
schema: Schema.Schema<A, I>,
|
|
84
|
+
data: unknown,
|
|
77
85
|
): Effect.Effect<A, Error> {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
86
|
+
return Schema.decodeUnknown(schema)(data).pipe(
|
|
87
|
+
Effect.mapError((e) => new Error(String(e))),
|
|
88
|
+
);
|
|
81
89
|
}
|
package/src/app.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Command } from "@effect/cli";
|
|
2
|
+
import { issue } from "./commands/issue.js";
|
|
3
|
+
import { issues } from "./commands/issues.js";
|
|
4
|
+
import { states } from "./commands/states.js";
|
|
5
|
+
import { labels } from "./commands/labels.js";
|
|
6
|
+
import { members } from "./commands/members.js";
|
|
7
|
+
import { cycles } from "./commands/cycles.js";
|
|
8
|
+
import { modules } from "./commands/modules.js";
|
|
9
|
+
import { intake } from "./commands/intake.js";
|
|
10
|
+
import { pages } from "./commands/pages.js";
|
|
11
|
+
import { projects } from "./commands/projects.js";
|
|
12
|
+
import { init } from "./commands/init.js";
|
|
13
|
+
|
|
14
|
+
const plane = Command.make("plane").pipe(
|
|
15
|
+
Command.withDescription(
|
|
16
|
+
`CLI for the Plane project management API. Useful for humans and AI agents/bots.
|
|
17
|
+
|
|
18
|
+
CONFIGURATION
|
|
19
|
+
Config file: ~/.config/plane/config.json (written by 'plane init')
|
|
20
|
+
Env vars: PLANE_API_TOKEN, PLANE_HOST, PLANE_WORKSPACE
|
|
21
|
+
Env vars take priority over the config file.
|
|
22
|
+
|
|
23
|
+
QUICK START
|
|
24
|
+
plane init Interactive setup — saves host/workspace/token
|
|
25
|
+
plane projects list List projects and their identifiers
|
|
26
|
+
plane issues list PROJ List issues for a project
|
|
27
|
+
plane issue get PROJ-29 Get full JSON for an issue
|
|
28
|
+
plane issue create PROJ "title" Create an issue
|
|
29
|
+
plane issue update --state done PROJ-29
|
|
30
|
+
plane issue comment PROJ-29 "text" Add a comment
|
|
31
|
+
|
|
32
|
+
CONCEPTS
|
|
33
|
+
Project identifier Short string shown by 'plane projects list' (e.g. ACME, WEB)
|
|
34
|
+
Issue ref Identifier + sequence number (e.g. ACME-29, WEB-5)
|
|
35
|
+
State groups backlog | unstarted | started | completed | cancelled
|
|
36
|
+
Priorities urgent | high | medium | low | none
|
|
37
|
+
|
|
38
|
+
ALL SUBCOMMANDS
|
|
39
|
+
init Set up config interactively
|
|
40
|
+
projects list List all projects
|
|
41
|
+
issues list List issues (supports --state filter)
|
|
42
|
+
issue get | create | update | delete | comment | activity |
|
|
43
|
+
link | comments | worklogs
|
|
44
|
+
cycles list | issues (list, add)
|
|
45
|
+
modules list | issues (list, add, remove)
|
|
46
|
+
intake list | accept | reject
|
|
47
|
+
pages list | get
|
|
48
|
+
states list List workflow states for a project
|
|
49
|
+
labels list List labels for a project
|
|
50
|
+
members list List members of a project
|
|
51
|
+
|
|
52
|
+
FOR AI AGENTS / BOTS
|
|
53
|
+
- Add --json to any list command for JSON output (array of objects)
|
|
54
|
+
- Add --xml to any list command for XML output
|
|
55
|
+
- 'plane issue get PROJ-N' always outputs full JSON
|
|
56
|
+
- Use PLANE_API_TOKEN / PLANE_HOST / PLANE_WORKSPACE env vars to avoid 'plane init'
|
|
57
|
+
- Full Plane REST API reference (180+ endpoints):
|
|
58
|
+
https://developers.plane.so/api-reference/introduction`,
|
|
59
|
+
),
|
|
60
|
+
Command.withSubcommands([
|
|
61
|
+
init,
|
|
62
|
+
projects,
|
|
63
|
+
issues,
|
|
64
|
+
issue,
|
|
65
|
+
states,
|
|
66
|
+
labels,
|
|
67
|
+
members,
|
|
68
|
+
cycles,
|
|
69
|
+
modules,
|
|
70
|
+
intake,
|
|
71
|
+
pages,
|
|
72
|
+
]),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
export const cli = Command.run(plane, {
|
|
76
|
+
name: "plane",
|
|
77
|
+
version: "0.1.0",
|
|
78
|
+
});
|
package/src/bin.ts
CHANGED
|
@@ -1,73 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { issue } from "./commands/issue.js"
|
|
5
|
-
import { issues } from "./commands/issues.js"
|
|
6
|
-
import { states } from "./commands/states.js"
|
|
7
|
-
import { labels } from "./commands/labels.js"
|
|
8
|
-
import { members } from "./commands/members.js"
|
|
9
|
-
import { cycles } from "./commands/cycles.js"
|
|
10
|
-
import { modules } from "./commands/modules.js"
|
|
11
|
-
import { intake } from "./commands/intake.js"
|
|
12
|
-
import { pages } from "./commands/pages.js"
|
|
13
|
-
import { projects } from "./commands/projects.js"
|
|
14
|
-
import { init } from "./commands/init.js"
|
|
15
|
-
|
|
16
|
-
const plane = Command.make("plane").pipe(
|
|
17
|
-
Command.withDescription(
|
|
18
|
-
`CLI for the Plane project management API. Useful for humans and AI agents/bots.
|
|
19
|
-
|
|
20
|
-
CONFIGURATION
|
|
21
|
-
Config file: ~/.config/plane/config.json (written by 'plane init')
|
|
22
|
-
Env vars: PLANE_API_TOKEN, PLANE_HOST, PLANE_WORKSPACE
|
|
23
|
-
Env vars take priority over the config file.
|
|
24
|
-
|
|
25
|
-
QUICK START
|
|
26
|
-
plane init Interactive setup — saves host/workspace/token
|
|
27
|
-
plane projects list List projects and their identifiers
|
|
28
|
-
plane issues list PROJ List issues for a project
|
|
29
|
-
plane issue get PROJ-29 Get full JSON for an issue
|
|
30
|
-
plane issue create PROJ "title" Create an issue
|
|
31
|
-
plane issue update --state done PROJ-29
|
|
32
|
-
plane issue comment PROJ-29 "text" Add a comment
|
|
33
|
-
|
|
34
|
-
CONCEPTS
|
|
35
|
-
Project identifier Short string shown by 'plane projects list' (e.g. ACME, WEB)
|
|
36
|
-
Issue ref Identifier + sequence number (e.g. ACME-29, WEB-5)
|
|
37
|
-
State groups backlog | unstarted | started | completed | cancelled
|
|
38
|
-
Priorities urgent | high | medium | low | none
|
|
39
|
-
|
|
40
|
-
ALL SUBCOMMANDS
|
|
41
|
-
init Set up config interactively
|
|
42
|
-
projects list List all projects
|
|
43
|
-
issues list List issues (supports --state filter)
|
|
44
|
-
issue get | create | update | delete | comment | activity |
|
|
45
|
-
link | comments | worklogs
|
|
46
|
-
cycles list | issues (list, add)
|
|
47
|
-
modules list | issues (list, add, remove)
|
|
48
|
-
intake list | accept | reject
|
|
49
|
-
pages list | get
|
|
50
|
-
states list List workflow states for a project
|
|
51
|
-
labels list List labels for a project
|
|
52
|
-
members list List members of a project
|
|
53
|
-
|
|
54
|
-
FOR AI AGENTS / BOTS
|
|
55
|
-
- Add --json to any list command for JSON output (array of objects)
|
|
56
|
-
- Add --xml to any list command for XML output
|
|
57
|
-
- 'plane issue get PROJ-N' always outputs full JSON
|
|
58
|
-
- Use PLANE_API_TOKEN / PLANE_HOST / PLANE_WORKSPACE env vars to avoid 'plane init'
|
|
59
|
-
- Full Plane REST API reference (180+ endpoints):
|
|
60
|
-
https://developers.plane.so/api-reference/introduction`,
|
|
61
|
-
),
|
|
62
|
-
Command.withSubcommands([init, projects, issues, issue, states, labels, members, cycles, modules, intake, pages]),
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
const cli = Command.run(plane, {
|
|
66
|
-
name: "plane",
|
|
67
|
-
version: "0.1.0",
|
|
68
|
-
})
|
|
1
|
+
import { NodeContext, NodeRuntime } from "@effect/platform-node";
|
|
2
|
+
import { Effect, Layer } from "effect";
|
|
3
|
+
import { cli } from "./app.js";
|
|
69
4
|
|
|
70
5
|
Effect.suspend(() => cli(process.argv)).pipe(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
)
|
|
6
|
+
Effect.provide(Layer.mergeAll(NodeContext.layer)),
|
|
7
|
+
NodeRuntime.runMain,
|
|
8
|
+
);
|
package/src/commands/cycles.ts
CHANGED
|
@@ -1,113 +1,132 @@
|
|
|
1
|
-
import { Command, Args } from "@effect/cli"
|
|
2
|
-
import { Console, Effect } from "effect"
|
|
3
|
-
import { api, decodeOrFail } from "../api.js"
|
|
4
|
-
import { CyclesResponseSchema, CycleIssuesResponseSchema } from "../config.js"
|
|
5
|
-
import { resolveProject, parseIssueRef, findIssueBySeq } from "../resolve.js"
|
|
6
|
-
import { jsonMode, xmlMode, toXml } from "../output.js"
|
|
1
|
+
import { Command, Args } from "@effect/cli";
|
|
2
|
+
import { Console, Effect } from "effect";
|
|
3
|
+
import { api, decodeOrFail } from "../api.js";
|
|
4
|
+
import { CyclesResponseSchema, CycleIssuesResponseSchema } from "../config.js";
|
|
5
|
+
import { resolveProject, parseIssueRef, findIssueBySeq } from "../resolve.js";
|
|
6
|
+
import { jsonMode, xmlMode, toXml } from "../output.js";
|
|
7
7
|
|
|
8
8
|
const projectArg = Args.text({ name: "project" }).pipe(
|
|
9
|
-
|
|
10
|
-
)
|
|
9
|
+
Args.withDescription("Project identifier (e.g. PROJ, WEB, OPS)"),
|
|
10
|
+
);
|
|
11
11
|
|
|
12
12
|
const cycleIdArg = Args.text({ name: "cycle-id" }).pipe(
|
|
13
|
-
|
|
14
|
-
)
|
|
13
|
+
Args.withDescription("Cycle UUID (from 'plane cycles list PROJECT')"),
|
|
14
|
+
);
|
|
15
15
|
|
|
16
16
|
// --- cycles list ---
|
|
17
17
|
|
|
18
|
-
export const cyclesList = Command.make(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
18
|
+
export const cyclesList = Command.make(
|
|
19
|
+
"list",
|
|
20
|
+
{ project: projectArg },
|
|
21
|
+
({ project }) =>
|
|
22
|
+
Effect.gen(function* () {
|
|
23
|
+
const { id } = yield* resolveProject(project);
|
|
24
|
+
const raw = yield* api.get(`projects/${id}/cycles/`);
|
|
25
|
+
const { results } = yield* decodeOrFail(CyclesResponseSchema, 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 cycles found");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const lines = results.map((c) => {
|
|
39
|
+
const start = c.start_date ?? "—";
|
|
40
|
+
const end = c.end_date ?? "—";
|
|
41
|
+
const status = (c.status ?? "?").padEnd(10);
|
|
42
|
+
return `${c.id} ${status} ${start} → ${end} ${c.name}`;
|
|
43
|
+
});
|
|
44
|
+
yield* Console.log(lines.join("\n"));
|
|
45
|
+
}),
|
|
37
46
|
).pipe(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
)
|
|
47
|
+
Command.withDescription(
|
|
48
|
+
"List cycles for a project. Shows cycle UUID, status, date range, and name.\n\nExample:\n plane cycles list PROJ",
|
|
49
|
+
),
|
|
50
|
+
);
|
|
42
51
|
|
|
43
52
|
// --- cycles issues list ---
|
|
44
53
|
|
|
45
54
|
export const cycleIssuesList = Command.make(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
55
|
+
"list",
|
|
56
|
+
{ project: projectArg, cycleId: cycleIdArg },
|
|
57
|
+
({ project, cycleId }) =>
|
|
58
|
+
Effect.gen(function* () {
|
|
59
|
+
const { key, id } = yield* resolveProject(project);
|
|
60
|
+
const raw = yield* api.get(
|
|
61
|
+
`projects/${id}/cycles/${cycleId}/cycle-issues/`,
|
|
62
|
+
);
|
|
63
|
+
const { results } = yield* decodeOrFail(CycleIssuesResponseSchema, raw);
|
|
64
|
+
if (jsonMode) {
|
|
65
|
+
yield* Console.log(JSON.stringify(results, null, 2));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (xmlMode) {
|
|
69
|
+
yield* Console.log(toXml(results));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (results.length === 0) {
|
|
73
|
+
yield* Console.log("No issues in cycle");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const lines = results.map((ci) => {
|
|
77
|
+
if (ci.issue_detail) {
|
|
78
|
+
const seq = String(ci.issue_detail.sequence_id).padStart(3, " ");
|
|
79
|
+
return `${key}-${seq} ${ci.issue_detail.name} (${ci.id})`;
|
|
80
|
+
}
|
|
81
|
+
return `${ci.issue} (cycle-issue: ${ci.id})`;
|
|
82
|
+
});
|
|
83
|
+
yield* Console.log(lines.join("\n"));
|
|
84
|
+
}),
|
|
68
85
|
).pipe(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
)
|
|
86
|
+
Command.withDescription(
|
|
87
|
+
"List issues in a cycle.\n\nExample:\n plane cycles issues list PROJ <cycle-id>",
|
|
88
|
+
),
|
|
89
|
+
);
|
|
73
90
|
|
|
74
91
|
// --- cycles issues add ---
|
|
75
92
|
|
|
76
93
|
const issueRefArg = Args.text({ name: "ref" }).pipe(
|
|
77
|
-
|
|
78
|
-
)
|
|
94
|
+
Args.withDescription("Issue reference to add (e.g. PROJ-29)"),
|
|
95
|
+
);
|
|
79
96
|
|
|
80
97
|
export const cycleIssuesAdd = Command.make(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
98
|
+
"add",
|
|
99
|
+
{ project: projectArg, cycleId: cycleIdArg, ref: issueRefArg },
|
|
100
|
+
({ project, cycleId, ref }) =>
|
|
101
|
+
Effect.gen(function* () {
|
|
102
|
+
const { id: projectId } = yield* resolveProject(project);
|
|
103
|
+
const { seq } = yield* parseIssueRef(ref);
|
|
104
|
+
const issue = yield* findIssueBySeq(projectId, seq);
|
|
105
|
+
yield* api.post(`projects/${projectId}/cycles/${cycleId}/cycle-issues/`, {
|
|
106
|
+
issues: [issue.id],
|
|
107
|
+
});
|
|
108
|
+
yield* Console.log(`Added ${ref} to cycle ${cycleId}`);
|
|
109
|
+
}),
|
|
93
110
|
).pipe(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
)
|
|
111
|
+
Command.withDescription(
|
|
112
|
+
"Add an issue to a cycle.\n\nExample:\n plane cycles issues add PROJ <cycle-id> PROJ-29",
|
|
113
|
+
),
|
|
114
|
+
);
|
|
98
115
|
|
|
99
116
|
// --- cycles issues (parent) ---
|
|
100
117
|
|
|
101
118
|
export const cycleIssues = Command.make("issues").pipe(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
)
|
|
119
|
+
Command.withDescription(
|
|
120
|
+
"Manage issues within a cycle. Subcommands: list, add",
|
|
121
|
+
),
|
|
122
|
+
Command.withSubcommands([cycleIssuesList, cycleIssuesAdd]),
|
|
123
|
+
);
|
|
105
124
|
|
|
106
125
|
// --- cycles (parent) ---
|
|
107
126
|
|
|
108
127
|
export const cycles = Command.make("cycles").pipe(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
)
|
|
128
|
+
Command.withDescription(
|
|
129
|
+
"Manage cycles (sprints). Subcommands: list, issues\n\nExamples:\n plane cycles list PROJ\n plane cycles issues list PROJ <cycle-id>\n plane cycles issues add PROJ <cycle-id> PROJ-29",
|
|
130
|
+
),
|
|
131
|
+
Command.withSubcommands([cyclesList, cycleIssues]),
|
|
132
|
+
);
|