@aaronshaf/plane 0.1.2
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/.github/workflows/ci.yml +36 -0
- package/.github/workflows/publish.yml +36 -0
- package/.husky/pre-commit +7 -0
- package/README.md +120 -0
- package/bin/plane +2 -0
- package/bun.lock +244 -0
- package/package.json +57 -0
- package/scripts/check-coverage.ts +54 -0
- package/scripts/check-file-size.ts +36 -0
- package/src/api.ts +81 -0
- package/src/bin.ts +72 -0
- package/src/commands/cycles.ts +108 -0
- package/src/commands/init.ts +72 -0
- package/src/commands/intake.ts +85 -0
- package/src/commands/issue.ts +409 -0
- package/src/commands/issues.ts +51 -0
- package/src/commands/labels.ts +54 -0
- package/src/commands/members.ts +20 -0
- package/src/commands/modules.ts +129 -0
- package/src/commands/pages.ts +63 -0
- package/src/commands/projects.ts +24 -0
- package/src/commands/states.ts +28 -0
- package/src/config.ts +217 -0
- package/src/format.ts +11 -0
- package/src/resolve.ts +76 -0
- package/tests/api.test.ts +169 -0
- package/tests/cycles-extended.test.ts +183 -0
- package/tests/format.test.ts +60 -0
- package/tests/helpers/mock-api.ts +27 -0
- package/tests/intake.test.ts +157 -0
- package/tests/issue-activity.test.ts +167 -0
- package/tests/issue-commands.test.ts +322 -0
- package/tests/issue-comments-worklogs.test.ts +291 -0
- package/tests/issue-links.test.ts +206 -0
- package/tests/modules.test.ts +255 -0
- package/tests/new-schemas.test.ts +201 -0
- package/tests/new-schemas2.test.ts +205 -0
- package/tests/pages.test.ts +124 -0
- package/tests/resolve.test.ts +178 -0
- package/tests/schemas.test.ts +203 -0
- package/tsconfig.json +21 -0
package/src/api.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
|
|
6
|
+
const CONFIG_FILE = path.join(os.homedir(), ".config", "plane", "config.json")
|
|
7
|
+
|
|
8
|
+
function readConfigFile(): Partial<{ token: string; host: string; workspace: string }> {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"))
|
|
11
|
+
} catch {
|
|
12
|
+
return {}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getConfig() {
|
|
17
|
+
const file = readConfigFile()
|
|
18
|
+
return {
|
|
19
|
+
token: process.env["PLANE_API_TOKEN"] ?? file.token ?? "",
|
|
20
|
+
host: (process.env["PLANE_HOST"] ?? file.host ?? "https://plane.so").replace(/\/$/, ""),
|
|
21
|
+
workspace: process.env["PLANE_WORKSPACE"] ?? file.workspace ?? "",
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function request(
|
|
26
|
+
method: string,
|
|
27
|
+
path: string,
|
|
28
|
+
body?: unknown,
|
|
29
|
+
): Effect.Effect<unknown, Error> {
|
|
30
|
+
return Effect.tryPromise({
|
|
31
|
+
try: async () => {
|
|
32
|
+
const { token, host, workspace } = getConfig()
|
|
33
|
+
let url = `${host}/api/v1/workspaces/${workspace}/${path}`
|
|
34
|
+
|
|
35
|
+
// Always expand state on issue list/get calls (not intake-issues/ or cycle-issues/)
|
|
36
|
+
if (method === "GET" && /(?:^|\/)(issues\/)/.test(path)) {
|
|
37
|
+
url += url.includes("?") ? "&expand=state" : "?expand=state"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const headers: Record<string, string> = {
|
|
41
|
+
"X-Api-Key": token,
|
|
42
|
+
}
|
|
43
|
+
if (body !== undefined) {
|
|
44
|
+
headers["Content-Type"] = "application/json"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const res = await fetch(url, {
|
|
48
|
+
method,
|
|
49
|
+
headers,
|
|
50
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
const text = await res.text()
|
|
55
|
+
throw new Error(`HTTP ${res.status}: ${text}`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 204 No Content
|
|
59
|
+
if (res.status === 204) return null
|
|
60
|
+
|
|
61
|
+
return res.json()
|
|
62
|
+
},
|
|
63
|
+
catch: (e) => (e instanceof Error ? e : new Error(String(e))),
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const api = {
|
|
68
|
+
get: (path: string) => request("GET", path),
|
|
69
|
+
post: (path: string, body: unknown) => request("POST", path, body),
|
|
70
|
+
patch: (path: string, body: unknown) => request("PATCH", path, body),
|
|
71
|
+
delete: (path: string) => request("DELETE", path),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function decodeOrFail<A, I>(
|
|
75
|
+
schema: Schema.Schema<A, I>,
|
|
76
|
+
data: unknown,
|
|
77
|
+
): Effect.Effect<A, Error> {
|
|
78
|
+
return Schema.decodeUnknown(schema)(data).pipe(
|
|
79
|
+
Effect.mapError((e) => new Error(String(e))),
|
|
80
|
+
)
|
|
81
|
+
}
|
package/src/bin.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Command } from "@effect/cli"
|
|
2
|
+
import { NodeContext, NodeRuntime } from "@effect/platform-node"
|
|
3
|
+
import { Effect, Layer } from "effect"
|
|
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
|
|
55
|
+
- All list commands output one record per line, tab-separated
|
|
56
|
+
- 'plane issue get PROJ-N' outputs full JSON — pipe to jq for field extraction
|
|
57
|
+
- Use PLANE_API_TOKEN / PLANE_HOST / PLANE_WORKSPACE env vars to avoid 'plane init'
|
|
58
|
+
- Full Plane REST API reference (180+ endpoints):
|
|
59
|
+
https://developers.plane.so/api-reference/introduction`,
|
|
60
|
+
),
|
|
61
|
+
Command.withSubcommands([init, projects, issues, issue, states, labels, members, cycles, modules, intake, pages]),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
const cli = Command.run(plane, {
|
|
65
|
+
name: "plane",
|
|
66
|
+
version: "0.1.0",
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
Effect.suspend(() => cli(process.argv)).pipe(
|
|
70
|
+
Effect.provide(Layer.mergeAll(NodeContext.layer)),
|
|
71
|
+
NodeRuntime.runMain,
|
|
72
|
+
)
|
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
|
|
7
|
+
const projectArg = Args.text({ name: "project" }).pipe(
|
|
8
|
+
Args.withDescription("Project identifier (e.g. PROJ, WEB, OPS)"),
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
const cycleIdArg = Args.text({ name: "cycle-id" }).pipe(
|
|
12
|
+
Args.withDescription("Cycle UUID (from 'plane cycles list PROJECT')"),
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// --- cycles list ---
|
|
16
|
+
|
|
17
|
+
export const cyclesList = Command.make("list", { project: projectArg }, ({ project }) =>
|
|
18
|
+
Effect.gen(function* () {
|
|
19
|
+
const { id } = yield* resolveProject(project)
|
|
20
|
+
const raw = yield* api.get(`projects/${id}/cycles/`)
|
|
21
|
+
const { results } = yield* decodeOrFail(CyclesResponseSchema, raw)
|
|
22
|
+
if (results.length === 0) {
|
|
23
|
+
yield* Console.log("No cycles found")
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
const lines = results.map((c) => {
|
|
27
|
+
const start = c.start_date ?? "—"
|
|
28
|
+
const end = c.end_date ?? "—"
|
|
29
|
+
const status = (c.status ?? "?").padEnd(10)
|
|
30
|
+
return `${c.id} ${status} ${start} → ${end} ${c.name}`
|
|
31
|
+
})
|
|
32
|
+
yield* Console.log(lines.join("\n"))
|
|
33
|
+
}),
|
|
34
|
+
).pipe(
|
|
35
|
+
Command.withDescription(
|
|
36
|
+
"List cycles for a project. Shows cycle UUID, status, date range, and name.\n\nExample:\n plane cycles list PROJ",
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
// --- cycles issues list ---
|
|
41
|
+
|
|
42
|
+
export const cycleIssuesList = Command.make(
|
|
43
|
+
"list",
|
|
44
|
+
{ project: projectArg, cycleId: cycleIdArg },
|
|
45
|
+
({ project, cycleId }) =>
|
|
46
|
+
Effect.gen(function* () {
|
|
47
|
+
const { key, id } = yield* resolveProject(project)
|
|
48
|
+
const raw = yield* api.get(`projects/${id}/cycles/${cycleId}/cycle-issues/`)
|
|
49
|
+
const { results } = yield* decodeOrFail(CycleIssuesResponseSchema, raw)
|
|
50
|
+
if (results.length === 0) {
|
|
51
|
+
yield* Console.log("No issues in cycle")
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
const lines = results.map((ci) => {
|
|
55
|
+
if (ci.issue_detail) {
|
|
56
|
+
const seq = String(ci.issue_detail.sequence_id).padStart(3, " ")
|
|
57
|
+
return `${key}-${seq} ${ci.issue_detail.name} (${ci.id})`
|
|
58
|
+
}
|
|
59
|
+
return `${ci.issue} (cycle-issue: ${ci.id})`
|
|
60
|
+
})
|
|
61
|
+
yield* Console.log(lines.join("\n"))
|
|
62
|
+
}),
|
|
63
|
+
).pipe(
|
|
64
|
+
Command.withDescription(
|
|
65
|
+
"List issues in a cycle.\n\nExample:\n plane cycles issues list PROJ <cycle-id>",
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
// --- cycles issues add ---
|
|
70
|
+
|
|
71
|
+
const issueRefArg = Args.text({ name: "ref" }).pipe(
|
|
72
|
+
Args.withDescription("Issue reference to add (e.g. PROJ-29)"),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
export const cycleIssuesAdd = Command.make(
|
|
76
|
+
"add",
|
|
77
|
+
{ project: projectArg, cycleId: cycleIdArg, ref: issueRefArg },
|
|
78
|
+
({ project, cycleId, ref }) =>
|
|
79
|
+
Effect.gen(function* () {
|
|
80
|
+
const { id: projectId } = yield* resolveProject(project)
|
|
81
|
+
const { seq } = yield* parseIssueRef(ref)
|
|
82
|
+
const issue = yield* findIssueBySeq(projectId, seq)
|
|
83
|
+
yield* api.post(`projects/${projectId}/cycles/${cycleId}/cycle-issues/`, {
|
|
84
|
+
issues: [issue.id],
|
|
85
|
+
})
|
|
86
|
+
yield* Console.log(`Added ${ref} to cycle ${cycleId}`)
|
|
87
|
+
}),
|
|
88
|
+
).pipe(
|
|
89
|
+
Command.withDescription(
|
|
90
|
+
"Add an issue to a cycle.\n\nExample:\n plane cycles issues add PROJ <cycle-id> PROJ-29",
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
// --- cycles issues (parent) ---
|
|
95
|
+
|
|
96
|
+
export const cycleIssues = Command.make("issues").pipe(
|
|
97
|
+
Command.withDescription("Manage issues within a cycle. Subcommands: list, add"),
|
|
98
|
+
Command.withSubcommands([cycleIssuesList, cycleIssuesAdd]),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
// --- cycles (parent) ---
|
|
102
|
+
|
|
103
|
+
export const cycles = Command.make("cycles").pipe(
|
|
104
|
+
Command.withDescription(
|
|
105
|
+
"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",
|
|
106
|
+
),
|
|
107
|
+
Command.withSubcommands([cyclesList, cycleIssues]),
|
|
108
|
+
)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Command } from "@effect/cli"
|
|
2
|
+
import { Console, Effect } from "effect"
|
|
3
|
+
import * as readline from "node:readline"
|
|
4
|
+
import * as fs from "node:fs"
|
|
5
|
+
import * as path from "node:path"
|
|
6
|
+
import * as os from "node:os"
|
|
7
|
+
|
|
8
|
+
export const CONFIG_DIR = path.join(os.homedir(), ".config", "plane")
|
|
9
|
+
export const CONFIG_FILE = path.join(CONFIG_DIR, "config.json")
|
|
10
|
+
|
|
11
|
+
export interface PlaneConfig {
|
|
12
|
+
token: string
|
|
13
|
+
host: string
|
|
14
|
+
workspace: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function prompt(rl: readline.Interface, question: string): Promise<string> {
|
|
18
|
+
return new Promise((resolve) => rl.question(question, resolve))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const init = Command.make("init", {}, () =>
|
|
22
|
+
Effect.gen(function* () {
|
|
23
|
+
let existing: Partial<PlaneConfig> = {}
|
|
24
|
+
try {
|
|
25
|
+
existing = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"))
|
|
26
|
+
} catch {
|
|
27
|
+
// no existing config
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const rl = readline.createInterface({
|
|
31
|
+
input: process.stdin,
|
|
32
|
+
output: process.stdout,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const host = yield* Effect.promise(() =>
|
|
36
|
+
prompt(rl, `Plane host [${existing.host ?? "https://plane.so"}]: `),
|
|
37
|
+
)
|
|
38
|
+
const workspace = yield* Effect.promise(() =>
|
|
39
|
+
prompt(rl, `Workspace slug [${existing.workspace ?? ""}]: `),
|
|
40
|
+
)
|
|
41
|
+
const token = yield* Effect.promise(() =>
|
|
42
|
+
prompt(rl, `API token [${existing.token ? "***" : ""}]: `),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
rl.close()
|
|
46
|
+
|
|
47
|
+
const config: PlaneConfig = {
|
|
48
|
+
host: host.trim() || existing.host || "https://plane.so",
|
|
49
|
+
workspace: workspace.trim() || existing.workspace || "",
|
|
50
|
+
token: token.trim() || existing.token || "",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!config.token) {
|
|
54
|
+
yield* Effect.fail(new Error("API token is required"))
|
|
55
|
+
}
|
|
56
|
+
if (!config.workspace) {
|
|
57
|
+
yield* Effect.fail(new Error("Workspace slug is required"))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true })
|
|
61
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 })
|
|
62
|
+
|
|
63
|
+
yield* Console.log(`\nConfig saved to ${CONFIG_FILE}`)
|
|
64
|
+
yield* Console.log(` Host: ${config.host}`)
|
|
65
|
+
yield* Console.log(` Workspace: ${config.workspace}`)
|
|
66
|
+
yield* Console.log(` Token: ***`)
|
|
67
|
+
}),
|
|
68
|
+
).pipe(
|
|
69
|
+
Command.withDescription(
|
|
70
|
+
"Interactive setup. Prompts for host, workspace slug, and API token, then saves to ~/.config/plane/config.json (mode 0600). Safe to re-run — existing values shown as defaults.",
|
|
71
|
+
),
|
|
72
|
+
)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Command, Options, Args } from "@effect/cli"
|
|
2
|
+
import { Console, Effect } from "effect"
|
|
3
|
+
import { api, decodeOrFail } from "../api.js"
|
|
4
|
+
import { IntakeIssuesResponseSchema } from "../config.js"
|
|
5
|
+
import { resolveProject } from "../resolve.js"
|
|
6
|
+
|
|
7
|
+
const projectArg = Args.text({ name: "project" }).pipe(
|
|
8
|
+
Args.withDescription("Project identifier (e.g. PROJ, WEB, OPS)"),
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// Intake status codes: -2=rejected, -1=snoozed, 0=pending, 1=accepted, 2=duplicate
|
|
12
|
+
const STATUS_LABELS: Record<number, string> = {
|
|
13
|
+
[-2]: "rejected",
|
|
14
|
+
[-1]: "snoozed",
|
|
15
|
+
[0]: "pending",
|
|
16
|
+
[1]: "accepted",
|
|
17
|
+
[2]: "duplicate",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// --- intake list ---
|
|
21
|
+
|
|
22
|
+
export const intakeList = Command.make("list", { project: projectArg }, ({ project }) =>
|
|
23
|
+
Effect.gen(function* () {
|
|
24
|
+
const { id } = yield* resolveProject(project)
|
|
25
|
+
const raw = yield* api.get(`projects/${id}/intake-issues/`)
|
|
26
|
+
const { results } = yield* decodeOrFail(IntakeIssuesResponseSchema, raw)
|
|
27
|
+
if (results.length === 0) {
|
|
28
|
+
yield* Console.log("No intake issues")
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
const lines = results.map((i) => {
|
|
32
|
+
const status = STATUS_LABELS[i.status ?? 0] ?? String(i.status ?? "?")
|
|
33
|
+
const statusPad = status.padEnd(10)
|
|
34
|
+
if (i.issue_detail) {
|
|
35
|
+
const seq = String(i.issue_detail.sequence_id).padStart(3, " ")
|
|
36
|
+
return `${i.id} [${statusPad}] ${i.issue_detail.name}`
|
|
37
|
+
}
|
|
38
|
+
return `${i.id} [${statusPad}]`
|
|
39
|
+
})
|
|
40
|
+
yield* Console.log(lines.join("\n"))
|
|
41
|
+
}),
|
|
42
|
+
).pipe(
|
|
43
|
+
Command.withDescription(
|
|
44
|
+
"List intake (triage) issues for a project. Shows status: pending, accepted, rejected, snoozed, duplicate.\n\nExample:\n plane intake list PROJ",
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
// --- intake accept ---
|
|
49
|
+
|
|
50
|
+
const intakeIdArg = Args.text({ name: "intake-id" }).pipe(
|
|
51
|
+
Args.withDescription("Intake issue ID (from 'plane intake list')"),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
export const intakeAccept = Command.make(
|
|
55
|
+
"accept",
|
|
56
|
+
{ project: projectArg, intakeId: intakeIdArg },
|
|
57
|
+
({ project, intakeId }) =>
|
|
58
|
+
Effect.gen(function* () {
|
|
59
|
+
const { id } = yield* resolveProject(project)
|
|
60
|
+
yield* api.patch(`projects/${id}/intake-issues/${intakeId}/`, { status: 1 })
|
|
61
|
+
yield* Console.log(`Intake issue ${intakeId} accepted`)
|
|
62
|
+
}),
|
|
63
|
+
).pipe(Command.withDescription("Accept an intake issue, creating it as a tracked work item."))
|
|
64
|
+
|
|
65
|
+
// --- intake reject ---
|
|
66
|
+
|
|
67
|
+
export const intakeReject = Command.make(
|
|
68
|
+
"reject",
|
|
69
|
+
{ project: projectArg, intakeId: intakeIdArg },
|
|
70
|
+
({ project, intakeId }) =>
|
|
71
|
+
Effect.gen(function* () {
|
|
72
|
+
const { id } = yield* resolveProject(project)
|
|
73
|
+
yield* api.patch(`projects/${id}/intake-issues/${intakeId}/`, { status: -2 })
|
|
74
|
+
yield* Console.log(`Intake issue ${intakeId} rejected`)
|
|
75
|
+
}),
|
|
76
|
+
).pipe(Command.withDescription("Reject an intake issue."))
|
|
77
|
+
|
|
78
|
+
// --- intake (parent) ---
|
|
79
|
+
|
|
80
|
+
export const intake = Command.make("intake").pipe(
|
|
81
|
+
Command.withDescription(
|
|
82
|
+
"Manage intake (incoming request triage). Subcommands: list, accept, reject\n\nExamples:\n plane intake list PROJ\n plane intake accept PROJ <intake-id>\n plane intake reject PROJ <intake-id>",
|
|
83
|
+
),
|
|
84
|
+
Command.withSubcommands([intakeList, intakeAccept, intakeReject]),
|
|
85
|
+
)
|