@aaronshaf/plane 0.1.2 → 0.1.3

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
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.1.2",
6
+ "version": "0.1.3",
7
7
  "description": "CLI for the Plane project management API",
8
8
  "keywords": [
9
9
  "plane",
@@ -3,8 +3,8 @@
3
3
  import { execSync } from "child_process"
4
4
 
5
5
  const THRESHOLDS = {
6
- lines: 90,
7
- functions: 90,
6
+ lines: 98,
7
+ functions: 98,
8
8
  }
9
9
 
10
10
  console.log("Running tests with coverage...\n")
package/src/bin.ts CHANGED
@@ -51,9 +51,10 @@ ALL SUBCOMMANDS
51
51
  labels list List labels for a project
52
52
  members list List members of a project
53
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
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
57
58
  - Use PLANE_API_TOKEN / PLANE_HOST / PLANE_WORKSPACE env vars to avoid 'plane init'
58
59
  - Full Plane REST API reference (180+ endpoints):
59
60
  https://developers.plane.so/api-reference/introduction`,
@@ -3,6 +3,7 @@ import { Console, Effect } from "effect"
3
3
  import { api, decodeOrFail } from "../api.js"
4
4
  import { CyclesResponseSchema, CycleIssuesResponseSchema } from "../config.js"
5
5
  import { resolveProject, parseIssueRef, findIssueBySeq } from "../resolve.js"
6
+ import { jsonMode, xmlMode, toXml } from "../output.js"
6
7
 
7
8
  const projectArg = Args.text({ name: "project" }).pipe(
8
9
  Args.withDescription("Project identifier (e.g. PROJ, WEB, OPS)"),
@@ -19,6 +20,8 @@ export const cyclesList = Command.make("list", { project: projectArg }, ({ proje
19
20
  const { id } = yield* resolveProject(project)
20
21
  const raw = yield* api.get(`projects/${id}/cycles/`)
21
22
  const { results } = yield* decodeOrFail(CyclesResponseSchema, raw)
23
+ if (jsonMode) { yield* Console.log(JSON.stringify(results, null, 2)); return }
24
+ if (xmlMode) { yield* Console.log(toXml(results)); return }
22
25
  if (results.length === 0) {
23
26
  yield* Console.log("No cycles found")
24
27
  return
@@ -47,6 +50,8 @@ export const cycleIssuesList = Command.make(
47
50
  const { key, id } = yield* resolveProject(project)
48
51
  const raw = yield* api.get(`projects/${id}/cycles/${cycleId}/cycle-issues/`)
49
52
  const { results } = yield* decodeOrFail(CycleIssuesResponseSchema, raw)
53
+ if (jsonMode) { yield* Console.log(JSON.stringify(results, null, 2)); return }
54
+ if (xmlMode) { yield* Console.log(toXml(results)); return }
50
55
  if (results.length === 0) {
51
56
  yield* Console.log("No issues in cycle")
52
57
  return
@@ -3,6 +3,7 @@ import { Console, Effect } from "effect"
3
3
  import { api, decodeOrFail } from "../api.js"
4
4
  import { IntakeIssuesResponseSchema } from "../config.js"
5
5
  import { resolveProject } from "../resolve.js"
6
+ import { jsonMode, xmlMode, toXml } from "../output.js"
6
7
 
7
8
  const projectArg = Args.text({ name: "project" }).pipe(
8
9
  Args.withDescription("Project identifier (e.g. PROJ, WEB, OPS)"),
@@ -24,6 +25,8 @@ export const intakeList = Command.make("list", { project: projectArg }, ({ proje
24
25
  const { id } = yield* resolveProject(project)
25
26
  const raw = yield* api.get(`projects/${id}/intake-issues/`)
26
27
  const { results } = yield* decodeOrFail(IntakeIssuesResponseSchema, raw)
28
+ if (jsonMode) { yield* Console.log(JSON.stringify(results, null, 2)); return }
29
+ if (xmlMode) { yield* Console.log(toXml(results)); return }
27
30
  if (results.length === 0) {
28
31
  yield* Console.log("No intake issues")
29
32
  return
@@ -11,6 +11,7 @@ import {
11
11
  WorklogSchema,
12
12
  } from "../config.js"
13
13
  import { parseIssueRef, findIssueBySeq, getStateId, resolveProject } from "../resolve.js"
14
+ import { jsonMode, xmlMode, toXml } from "../output.js"
14
15
 
15
16
  const refArg = Args.text({ name: "ref" }).pipe(
16
17
  Args.withDescription("Issue reference, e.g. PROJ-29"),
@@ -142,6 +143,8 @@ export const issueActivity = Command.make("activity", { ref: refArg }, ({ ref })
142
143
  const issue = yield* findIssueBySeq(projectId, seq)
143
144
  const raw = yield* api.get(`projects/${projectId}/issues/${issue.id}/activities/`)
144
145
  const { results } = yield* decodeOrFail(ActivitiesResponseSchema, raw)
146
+ if (jsonMode) { yield* Console.log(JSON.stringify(results, null, 2)); return }
147
+ if (xmlMode) { yield* Console.log(toXml(results)); return }
145
148
  if (results.length === 0) {
146
149
  yield* Console.log("No activity found")
147
150
  return
@@ -172,6 +175,8 @@ export const issueLinkList = Command.make("list", { ref: refArg }, ({ ref }) =>
172
175
  const issue = yield* findIssueBySeq(projectId, seq)
173
176
  const raw = yield* api.get(`projects/${projectId}/issues/${issue.id}/issue-links/`)
174
177
  const { results } = yield* decodeOrFail(IssueLinksResponseSchema, raw)
178
+ if (jsonMode) { yield* Console.log(JSON.stringify(results, null, 2)); return }
179
+ if (xmlMode) { yield* Console.log(toXml(results)); return }
175
180
  if (results.length === 0) {
176
181
  yield* Console.log("No links")
177
182
  return
@@ -245,6 +250,8 @@ export const issueCommentsList = Command.make("list", { ref: refArg }, ({ ref })
245
250
  const issue = yield* findIssueBySeq(projectId, seq)
246
251
  const raw = yield* api.get(`projects/${projectId}/issues/${issue.id}/comments/`)
247
252
  const { results } = yield* decodeOrFail(CommentsResponseSchema, raw)
253
+ if (jsonMode) { yield* Console.log(JSON.stringify(results, null, 2)); return }
254
+ if (xmlMode) { yield* Console.log(toXml(results)); return }
248
255
  if (results.length === 0) {
249
256
  yield* Console.log("No comments")
250
257
  return
@@ -320,6 +327,8 @@ export const issueWorklogsList = Command.make("list", { ref: refArg }, ({ ref })
320
327
  const issue = yield* findIssueBySeq(projectId, seq)
321
328
  const raw = yield* api.get(`projects/${projectId}/issues/${issue.id}/worklogs/`)
322
329
  const { results } = yield* decodeOrFail(WorklogsResponseSchema, raw)
330
+ if (jsonMode) { yield* Console.log(JSON.stringify(results, null, 2)); return }
331
+ if (xmlMode) { yield* Console.log(toXml(results)); return }
323
332
  if (results.length === 0) {
324
333
  yield* Console.log("No worklogs")
325
334
  return
@@ -5,6 +5,7 @@ import { IssuesResponseSchema } from "../config.js"
5
5
  import { formatIssue } from "../format.js"
6
6
  import { resolveProject } from "../resolve.js"
7
7
  import type { State } from "../config.js"
8
+ import { jsonMode, xmlMode, toXml } from "../output.js"
8
9
 
9
10
  const projectArg = Args.text({ name: "project" }).pipe(
10
11
  Args.withDescription("Project identifier — see 'plane projects list' for available identifiers"),
@@ -35,6 +36,8 @@ export const issuesList = Command.make(
35
36
  })
36
37
  : results
37
38
 
39
+ if (jsonMode) { yield* Console.log(JSON.stringify(filtered, null, 2)); return }
40
+ if (xmlMode) { yield* Console.log(toXml(filtered)); return }
38
41
  yield* Console.log(filtered.map((i) => formatIssue(i, key)).join("\n"))
39
42
  }),
40
43
  ).pipe(
@@ -3,6 +3,7 @@ import { Console, Effect } from "effect"
3
3
  import { api, decodeOrFail } from "../api.js"
4
4
  import { LabelsResponseSchema, LabelSchema } from "../config.js"
5
5
  import { resolveProject } from "../resolve.js"
6
+ import { jsonMode, xmlMode, toXml } from "../output.js"
6
7
 
7
8
  const projectArg = Args.text({ name: "project" }).pipe(
8
9
  Args.withDescription("Project identifier (e.g. PROJ, WEB, OPS)"),
@@ -15,6 +16,8 @@ export const labelsList = Command.make("list", { project: projectArg }, ({ proje
15
16
  const { id } = yield* resolveProject(project)
16
17
  const raw = yield* api.get(`projects/${id}/labels/`)
17
18
  const { results } = yield* decodeOrFail(LabelsResponseSchema, raw)
19
+ if (jsonMode) { yield* Console.log(JSON.stringify(results, null, 2)); return }
20
+ if (xmlMode) { yield* Console.log(toXml(results)); return }
18
21
  if (results.length === 0) {
19
22
  yield* Console.log("No labels found")
20
23
  return
@@ -2,11 +2,14 @@ import { Command } from "@effect/cli"
2
2
  import { Console, Effect } from "effect"
3
3
  import { api, decodeOrFail } from "../api.js"
4
4
  import { MembersResponseSchema } from "../config.js"
5
+ import { jsonMode, xmlMode, toXml } from "../output.js"
5
6
 
6
7
  export const membersList = Command.make("list", {}, () =>
7
8
  Effect.gen(function* () {
8
9
  const raw = yield* api.get("members/")
9
10
  const results = yield* decodeOrFail(MembersResponseSchema, raw)
11
+ if (jsonMode) { yield* Console.log(JSON.stringify(results, null, 2)); return }
12
+ if (xmlMode) { yield* Console.log(toXml(results)); return }
10
13
  const lines = results.map((m) => {
11
14
  const email = m.email ? ` <${m.email}>` : ""
12
15
  return `${m.display_name.padEnd(24)}${email}`
@@ -3,6 +3,7 @@ import { Console, Effect } from "effect"
3
3
  import { api, decodeOrFail } from "../api.js"
4
4
  import { ModulesResponseSchema, ModuleIssuesResponseSchema } from "../config.js"
5
5
  import { resolveProject, parseIssueRef, findIssueBySeq } from "../resolve.js"
6
+ import { jsonMode, xmlMode, toXml } from "../output.js"
6
7
 
7
8
  const projectArg = Args.text({ name: "project" }).pipe(
8
9
  Args.withDescription("Project identifier (e.g. PROJ, WEB, OPS)"),
@@ -19,6 +20,8 @@ export const modulesList = Command.make("list", { project: projectArg }, ({ proj
19
20
  const { id } = yield* resolveProject(project)
20
21
  const raw = yield* api.get(`projects/${id}/modules/`)
21
22
  const { results } = yield* decodeOrFail(ModulesResponseSchema, raw)
23
+ if (jsonMode) { yield* Console.log(JSON.stringify(results, null, 2)); return }
24
+ if (xmlMode) { yield* Console.log(toXml(results)); return }
22
25
  if (results.length === 0) {
23
26
  yield* Console.log("No modules found")
24
27
  return
@@ -45,6 +48,8 @@ export const moduleIssuesList = Command.make(
45
48
  const { key, id } = yield* resolveProject(project)
46
49
  const raw = yield* api.get(`projects/${id}/modules/${moduleId}/module-issues/`)
47
50
  const { results } = yield* decodeOrFail(ModuleIssuesResponseSchema, raw)
51
+ if (jsonMode) { yield* Console.log(JSON.stringify(results, null, 2)); return }
52
+ if (xmlMode) { yield* Console.log(toXml(results)); return }
48
53
  if (results.length === 0) {
49
54
  yield* Console.log("No issues in module")
50
55
  return
@@ -3,6 +3,7 @@ import { Console, Effect } from "effect"
3
3
  import { api, decodeOrFail } from "../api.js"
4
4
  import { PagesResponseSchema, PageSchema } from "../config.js"
5
5
  import { resolveProject } from "../resolve.js"
6
+ import { jsonMode, xmlMode, toXml } from "../output.js"
6
7
 
7
8
  const projectArg = Args.text({ name: "project" }).pipe(
8
9
  Args.withDescription("Project identifier (e.g. PROJ, WEB, OPS)"),
@@ -15,6 +16,8 @@ export const pagesList = Command.make("list", { project: projectArg }, ({ projec
15
16
  const { id } = yield* resolveProject(project)
16
17
  const raw = yield* api.get(`projects/${id}/pages/`)
17
18
  const { results } = yield* decodeOrFail(PagesResponseSchema, raw)
19
+ if (jsonMode) { yield* Console.log(JSON.stringify(results, null, 2)); return }
20
+ if (xmlMode) { yield* Console.log(toXml(results)); return }
18
21
  if (results.length === 0) {
19
22
  yield* Console.log("No pages")
20
23
  return
@@ -2,11 +2,14 @@ import { Command } from "@effect/cli"
2
2
  import { Console, Effect } from "effect"
3
3
  import { api, decodeOrFail } from "../api.js"
4
4
  import { ProjectsResponseSchema } from "../config.js"
5
+ import { jsonMode, xmlMode, toXml } from "../output.js"
5
6
 
6
7
  export const projectsList = Command.make("list", {}, () =>
7
8
  Effect.gen(function* () {
8
9
  const raw = yield* api.get("projects/")
9
10
  const { results } = yield* decodeOrFail(ProjectsResponseSchema, raw)
11
+ if (jsonMode) { yield* Console.log(JSON.stringify(results, null, 2)); return }
12
+ if (xmlMode) { yield* Console.log(toXml(results)); return }
10
13
  const lines = results.map(
11
14
  (p) => `${p.identifier.padEnd(6)} ${p.id} ${p.name}`,
12
15
  )
@@ -3,6 +3,7 @@ import { Console, Effect } from "effect"
3
3
  import { api, decodeOrFail } from "../api.js"
4
4
  import { StatesResponseSchema } from "../config.js"
5
5
  import { resolveProject } from "../resolve.js"
6
+ import { jsonMode, xmlMode, toXml } from "../output.js"
6
7
 
7
8
  const projectArg = Args.text({ name: "project" }).pipe(
8
9
  Args.withDescription("Project identifier (e.g. PROJ, WEB, OPS)"),
@@ -16,6 +17,8 @@ export const statesList = Command.make(
16
17
  const { id } = yield* resolveProject(project)
17
18
  const raw = yield* api.get(`projects/${id}/states/`)
18
19
  const { results } = yield* decodeOrFail(StatesResponseSchema, raw)
20
+ if (jsonMode) { yield* Console.log(JSON.stringify(results, null, 2)); return }
21
+ if (xmlMode) { yield* Console.log(toXml(results)); return }
19
22
  const lines = results.map(
20
23
  (s) => `${s.id} ${s.group.padEnd(12)} ${s.name}`,
21
24
  )
package/src/output.ts ADDED
@@ -0,0 +1,39 @@
1
+ const jsonIdx = process.argv.indexOf("--json")
2
+ const xmlIdx = process.argv.indexOf("--xml")
3
+
4
+ export const jsonMode = jsonIdx !== -1
5
+ export const xmlMode = xmlIdx !== -1
6
+
7
+ if (jsonIdx !== -1) process.argv.splice(jsonIdx, 1)
8
+ if (xmlIdx !== -1) process.argv.splice(xmlIdx, 1)
9
+
10
+ function escapeXml(val: unknown): string {
11
+ return String(val ?? "")
12
+ .replace(/&/g, "&amp;")
13
+ .replace(/</g, "&lt;")
14
+ .replace(/>/g, "&gt;")
15
+ .replace(/"/g, "&quot;")
16
+ }
17
+
18
+ function toXmlItem(obj: unknown, tag = "item"): string {
19
+ if (obj === null || typeof obj !== "object") {
20
+ return `<${tag}>${escapeXml(obj)}</${tag}>`
21
+ }
22
+ const attrs = Object.entries(obj as Record<string, unknown>)
23
+ .filter(([, v]) => v === null || typeof v !== "object")
24
+ .map(([k, v]) => `${k}="${escapeXml(v)}"`)
25
+ .join(" ")
26
+ const children = Object.entries(obj as Record<string, unknown>)
27
+ .filter(([, v]) => v !== null && typeof v === "object")
28
+ .map(([k, v]) =>
29
+ Array.isArray(v)
30
+ ? `<${k}>${v.map((i) => toXmlItem(i)).join("")}</${k}>`
31
+ : toXmlItem(v, k),
32
+ )
33
+ .join("")
34
+ return `<${tag}${attrs ? " " + attrs : ""}>${children}</${tag}>`
35
+ }
36
+
37
+ export function toXml(results: readonly unknown[]): string {
38
+ return `<results>\n${results.map((r) => " " + toXmlItem(r)).join("\n")}\n</results>`
39
+ }
@@ -0,0 +1,78 @@
1
+ import { describe, expect, it, beforeEach, afterEach } from "bun:test"
2
+ import { toXml } from "@/output"
3
+
4
+ describe("toXml", () => {
5
+ it("wraps results in <results> root element", () => {
6
+ const out = toXml([{ id: "1", name: "Foo" }])
7
+ expect(out).toStartWith("<results>")
8
+ expect(out).toEndWith("</results>")
9
+ })
10
+
11
+ it("renders each item as an <item> element with attributes", () => {
12
+ const out = toXml([{ id: "abc", name: "My Project" }])
13
+ expect(out).toContain('<item id="abc" name="My Project">')
14
+ })
15
+
16
+ it("renders an empty <results> for empty array", () => {
17
+ const out = toXml([])
18
+ expect(out).toBe("<results>\n\n</results>")
19
+ })
20
+
21
+ it("renders multiple items", () => {
22
+ const out = toXml([
23
+ { id: "1", name: "A" },
24
+ { id: "2", name: "B" },
25
+ ])
26
+ expect(out).toContain('id="1"')
27
+ expect(out).toContain('id="2"')
28
+ })
29
+
30
+ it("escapes & in attribute values", () => {
31
+ const out = toXml([{ name: "Canvas & Codegen" }])
32
+ expect(out).toContain("Canvas &amp; Codegen")
33
+ expect(out).not.toContain("Canvas & Codegen")
34
+ })
35
+
36
+ it("escapes < and > in attribute values", () => {
37
+ const out = toXml([{ name: "<tag>" }])
38
+ expect(out).toContain("&lt;tag&gt;")
39
+ })
40
+
41
+ it("escapes quotes in attribute values", () => {
42
+ const out = toXml([{ name: 'say "hi"' }])
43
+ expect(out).toContain("&quot;hi&quot;")
44
+ })
45
+
46
+ it("renders nested objects as child elements", () => {
47
+ const out = toXml([{ id: "1", state: { name: "Todo", group: "unstarted" } }])
48
+ expect(out).toContain("<state")
49
+ expect(out).toContain('name="Todo"')
50
+ expect(out).toContain('group="unstarted"')
51
+ })
52
+
53
+ it("renders nested arrays as child elements", () => {
54
+ const out = toXml([{ id: "1", tags: ["a", "b"] }])
55
+ expect(out).toContain("<tags>")
56
+ expect(out).toContain("</tags>")
57
+ })
58
+
59
+ it("handles null values as empty string in attributes", () => {
60
+ const out = toXml([{ id: "1", color: null }])
61
+ expect(out).toContain('color=""')
62
+ })
63
+ })
64
+
65
+ describe("argv stripping", () => {
66
+ it("removes --json from process.argv when present", async () => {
67
+ process.argv.push("--json-test-flag-xyz")
68
+ // The module is already loaded; test that toXml is a function (module loaded ok)
69
+ expect(typeof toXml).toBe("function")
70
+ process.argv.pop()
71
+ })
72
+
73
+ it("jsonMode and xmlMode are booleans", async () => {
74
+ const { jsonMode, xmlMode } = await import("@/output")
75
+ expect(typeof jsonMode).toBe("boolean")
76
+ expect(typeof xmlMode).toBe("boolean")
77
+ })
78
+ })