@aaronshaf/plane 0.1.8 → 0.1.11
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/api.ts +16 -1
- package/src/commands/cycles.ts +86 -63
- package/src/commands/intake.ts +64 -44
- package/src/commands/issue.ts +386 -310
- 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 +0 -16
- package/src/output.ts +12 -9
- package/src/resolve.ts +17 -29
- 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 -295
- 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/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -66,7 +66,22 @@ function request(
|
|
|
66
66
|
// 204 No Content
|
|
67
67
|
if (res.status === 204) return null;
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
// Use text + lenient parse to handle bare control characters (U+0000–U+001F)
|
|
70
|
+
// that may appear inside JSON string values (e.g. description_html with \n in <pre>).
|
|
71
|
+
const text = await res.text();
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(text);
|
|
74
|
+
} catch {
|
|
75
|
+
// Escape bare control characters inside JSON string values and retry.
|
|
76
|
+
const sanitized = text.replace(
|
|
77
|
+
/"(?:[^"\\]|\\.)*"/g,
|
|
78
|
+
(match) => match.replace(/[\x00-\x1F]/g, (c) => {
|
|
79
|
+
const hex = c.charCodeAt(0).toString(16).padStart(4, "0");
|
|
80
|
+
return `\\u${hex}`;
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
return JSON.parse(sanitized);
|
|
84
|
+
}
|
|
70
85
|
},
|
|
71
86
|
catch: (e) => (e instanceof Error ? e : new Error(String(e))),
|
|
72
87
|
});
|
package/src/commands/cycles.ts
CHANGED
|
@@ -15,34 +15,37 @@ const cycleIdArg = Args.text({ name: "cycle-id" }).pipe(
|
|
|
15
15
|
|
|
16
16
|
// --- cycles list ---
|
|
17
17
|
|
|
18
|
+
export function cyclesListHandler({ project }: { project: string }) {
|
|
19
|
+
return Effect.gen(function* () {
|
|
20
|
+
const { id } = yield* resolveProject(project);
|
|
21
|
+
const raw = yield* api.get(`projects/${id}/cycles/`);
|
|
22
|
+
const { results } = yield* decodeOrFail(CyclesResponseSchema, raw);
|
|
23
|
+
if (jsonMode) {
|
|
24
|
+
yield* Console.log(JSON.stringify(results, null, 2));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (xmlMode) {
|
|
28
|
+
yield* Console.log(toXml(results));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (results.length === 0) {
|
|
32
|
+
yield* Console.log("No cycles found");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const lines = results.map((c) => {
|
|
36
|
+
const start = c.start_date ?? "—";
|
|
37
|
+
const end = c.end_date ?? "—";
|
|
38
|
+
const status = (c.status ?? "?").padEnd(10);
|
|
39
|
+
return `${c.id} ${status} ${start} → ${end} ${c.name}`;
|
|
40
|
+
});
|
|
41
|
+
yield* Console.log(lines.join("\n"));
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
18
45
|
export const cyclesList = Command.make(
|
|
19
46
|
"list",
|
|
20
47
|
{ project: projectArg },
|
|
21
|
-
|
|
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
|
-
}),
|
|
48
|
+
cyclesListHandler,
|
|
46
49
|
).pipe(
|
|
47
50
|
Command.withDescription(
|
|
48
51
|
"List cycles for a project. Shows cycle UUID, status, date range, and name.\n\nExample:\n plane cycles list PROJ",
|
|
@@ -51,37 +54,46 @@ export const cyclesList = Command.make(
|
|
|
51
54
|
|
|
52
55
|
// --- cycles issues list ---
|
|
53
56
|
|
|
57
|
+
export function cycleIssuesListHandler({
|
|
58
|
+
project,
|
|
59
|
+
cycleId,
|
|
60
|
+
}: {
|
|
61
|
+
project: string;
|
|
62
|
+
cycleId: string;
|
|
63
|
+
}) {
|
|
64
|
+
return Effect.gen(function* () {
|
|
65
|
+
const { key, id } = yield* resolveProject(project);
|
|
66
|
+
const raw = yield* api.get(
|
|
67
|
+
`projects/${id}/cycles/${cycleId}/cycle-issues/`,
|
|
68
|
+
);
|
|
69
|
+
const { results } = yield* decodeOrFail(CycleIssuesResponseSchema, raw);
|
|
70
|
+
if (jsonMode) {
|
|
71
|
+
yield* Console.log(JSON.stringify(results, null, 2));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (xmlMode) {
|
|
75
|
+
yield* Console.log(toXml(results));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (results.length === 0) {
|
|
79
|
+
yield* Console.log("No issues in cycle");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const lines = results.map((ci) => {
|
|
83
|
+
if (ci.issue_detail) {
|
|
84
|
+
const seq = String(ci.issue_detail.sequence_id).padStart(3, " ");
|
|
85
|
+
return `${key}-${seq} ${ci.issue_detail.name} (${ci.id})`;
|
|
86
|
+
}
|
|
87
|
+
return `${ci.issue} (cycle-issue: ${ci.id})`;
|
|
88
|
+
});
|
|
89
|
+
yield* Console.log(lines.join("\n"));
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
54
93
|
export const cycleIssuesList = Command.make(
|
|
55
94
|
"list",
|
|
56
95
|
{ project: projectArg, cycleId: cycleIdArg },
|
|
57
|
-
|
|
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
|
-
}),
|
|
96
|
+
cycleIssuesListHandler,
|
|
85
97
|
).pipe(
|
|
86
98
|
Command.withDescription(
|
|
87
99
|
"List issues in a cycle.\n\nExample:\n plane cycles issues list PROJ <cycle-id>",
|
|
@@ -94,19 +106,30 @@ const issueRefArg = Args.text({ name: "ref" }).pipe(
|
|
|
94
106
|
Args.withDescription("Issue reference to add (e.g. PROJ-29)"),
|
|
95
107
|
);
|
|
96
108
|
|
|
109
|
+
export function cycleIssuesAddHandler({
|
|
110
|
+
project,
|
|
111
|
+
cycleId,
|
|
112
|
+
ref,
|
|
113
|
+
}: {
|
|
114
|
+
project: string;
|
|
115
|
+
cycleId: string;
|
|
116
|
+
ref: string;
|
|
117
|
+
}) {
|
|
118
|
+
return Effect.gen(function* () {
|
|
119
|
+
const { id: projectId } = yield* resolveProject(project);
|
|
120
|
+
const { seq } = yield* parseIssueRef(ref);
|
|
121
|
+
const issue = yield* findIssueBySeq(projectId, seq);
|
|
122
|
+
yield* api.post(`projects/${projectId}/cycles/${cycleId}/cycle-issues/`, {
|
|
123
|
+
issues: [issue.id],
|
|
124
|
+
});
|
|
125
|
+
yield* Console.log(`Added ${ref} to cycle ${cycleId}`);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
97
129
|
export const cycleIssuesAdd = Command.make(
|
|
98
130
|
"add",
|
|
99
131
|
{ project: projectArg, cycleId: cycleIdArg, ref: issueRefArg },
|
|
100
|
-
|
|
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
|
-
}),
|
|
132
|
+
cycleIssuesAddHandler,
|
|
110
133
|
).pipe(
|
|
111
134
|
Command.withDescription(
|
|
112
135
|
"Add an issue to a cycle.\n\nExample:\n plane cycles issues add PROJ <cycle-id> PROJ-29",
|
package/src/commands/intake.ts
CHANGED
|
@@ -20,37 +20,39 @@ const STATUS_LABELS: Record<number, string> = {
|
|
|
20
20
|
|
|
21
21
|
// --- intake list ---
|
|
22
22
|
|
|
23
|
+
export function intakeListHandler({ project }: { project: string }) {
|
|
24
|
+
return Effect.gen(function* () {
|
|
25
|
+
const { id } = yield* resolveProject(project);
|
|
26
|
+
const raw = yield* api.get(`projects/${id}/intake-issues/`);
|
|
27
|
+
const { results } = yield* decodeOrFail(IntakeIssuesResponseSchema, raw);
|
|
28
|
+
if (jsonMode) {
|
|
29
|
+
yield* Console.log(JSON.stringify(results, null, 2));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (xmlMode) {
|
|
33
|
+
yield* Console.log(toXml(results));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (results.length === 0) {
|
|
37
|
+
yield* Console.log("No intake issues");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const lines = results.map((i) => {
|
|
41
|
+
const status = STATUS_LABELS[i.status ?? 0] ?? String(i.status ?? "?");
|
|
42
|
+
const statusPad = status.padEnd(10);
|
|
43
|
+
if (i.issue_detail) {
|
|
44
|
+
return `${i.id} [${statusPad}] ${i.issue_detail.name}`;
|
|
45
|
+
}
|
|
46
|
+
return `${i.id} [${statusPad}]`;
|
|
47
|
+
});
|
|
48
|
+
yield* Console.log(lines.join("\n"));
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
23
52
|
export const intakeList = Command.make(
|
|
24
53
|
"list",
|
|
25
54
|
{ project: projectArg },
|
|
26
|
-
|
|
27
|
-
Effect.gen(function* () {
|
|
28
|
-
const { id } = yield* resolveProject(project);
|
|
29
|
-
const raw = yield* api.get(`projects/${id}/intake-issues/`);
|
|
30
|
-
const { results } = yield* decodeOrFail(IntakeIssuesResponseSchema, raw);
|
|
31
|
-
if (jsonMode) {
|
|
32
|
-
yield* Console.log(JSON.stringify(results, null, 2));
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
if (xmlMode) {
|
|
36
|
-
yield* Console.log(toXml(results));
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
if (results.length === 0) {
|
|
40
|
-
yield* Console.log("No intake issues");
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
const lines = results.map((i) => {
|
|
44
|
-
const status = STATUS_LABELS[i.status ?? 0] ?? String(i.status ?? "?");
|
|
45
|
-
const statusPad = status.padEnd(10);
|
|
46
|
-
if (i.issue_detail) {
|
|
47
|
-
const seq = String(i.issue_detail.sequence_id).padStart(3, " ");
|
|
48
|
-
return `${i.id} [${statusPad}] ${i.issue_detail.name}`;
|
|
49
|
-
}
|
|
50
|
-
return `${i.id} [${statusPad}]`;
|
|
51
|
-
});
|
|
52
|
-
yield* Console.log(lines.join("\n"));
|
|
53
|
-
}),
|
|
55
|
+
intakeListHandler,
|
|
54
56
|
).pipe(
|
|
55
57
|
Command.withDescription(
|
|
56
58
|
"List intake (triage) issues for a project. Shows status: pending, accepted, rejected, snoozed, duplicate.\n\nExample:\n plane intake list PROJ",
|
|
@@ -63,17 +65,26 @@ const intakeIdArg = Args.text({ name: "intake-id" }).pipe(
|
|
|
63
65
|
Args.withDescription("Intake issue ID (from 'plane intake list')"),
|
|
64
66
|
);
|
|
65
67
|
|
|
68
|
+
export function intakeAcceptHandler({
|
|
69
|
+
project,
|
|
70
|
+
intakeId,
|
|
71
|
+
}: {
|
|
72
|
+
project: string;
|
|
73
|
+
intakeId: string;
|
|
74
|
+
}) {
|
|
75
|
+
return Effect.gen(function* () {
|
|
76
|
+
const { id } = yield* resolveProject(project);
|
|
77
|
+
yield* api.patch(`projects/${id}/intake-issues/${intakeId}/`, {
|
|
78
|
+
status: 1,
|
|
79
|
+
});
|
|
80
|
+
yield* Console.log(`Intake issue ${intakeId} accepted`);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
66
84
|
export const intakeAccept = Command.make(
|
|
67
85
|
"accept",
|
|
68
86
|
{ project: projectArg, intakeId: intakeIdArg },
|
|
69
|
-
|
|
70
|
-
Effect.gen(function* () {
|
|
71
|
-
const { id } = yield* resolveProject(project);
|
|
72
|
-
yield* api.patch(`projects/${id}/intake-issues/${intakeId}/`, {
|
|
73
|
-
status: 1,
|
|
74
|
-
});
|
|
75
|
-
yield* Console.log(`Intake issue ${intakeId} accepted`);
|
|
76
|
-
}),
|
|
87
|
+
intakeAcceptHandler,
|
|
77
88
|
).pipe(
|
|
78
89
|
Command.withDescription(
|
|
79
90
|
"Accept an intake issue, creating it as a tracked work item.",
|
|
@@ -82,17 +93,26 @@ export const intakeAccept = Command.make(
|
|
|
82
93
|
|
|
83
94
|
// --- intake reject ---
|
|
84
95
|
|
|
96
|
+
export function intakeRejectHandler({
|
|
97
|
+
project,
|
|
98
|
+
intakeId,
|
|
99
|
+
}: {
|
|
100
|
+
project: string;
|
|
101
|
+
intakeId: string;
|
|
102
|
+
}) {
|
|
103
|
+
return Effect.gen(function* () {
|
|
104
|
+
const { id } = yield* resolveProject(project);
|
|
105
|
+
yield* api.patch(`projects/${id}/intake-issues/${intakeId}/`, {
|
|
106
|
+
status: -2,
|
|
107
|
+
});
|
|
108
|
+
yield* Console.log(`Intake issue ${intakeId} rejected`);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
85
112
|
export const intakeReject = Command.make(
|
|
86
113
|
"reject",
|
|
87
114
|
{ project: projectArg, intakeId: intakeIdArg },
|
|
88
|
-
|
|
89
|
-
Effect.gen(function* () {
|
|
90
|
-
const { id } = yield* resolveProject(project);
|
|
91
|
-
yield* api.patch(`projects/${id}/intake-issues/${intakeId}/`, {
|
|
92
|
-
status: -2,
|
|
93
|
-
});
|
|
94
|
-
yield* Console.log(`Intake issue ${intakeId} rejected`);
|
|
95
|
-
}),
|
|
115
|
+
intakeRejectHandler,
|
|
96
116
|
).pipe(Command.withDescription("Reject an intake issue."));
|
|
97
117
|
|
|
98
118
|
// --- intake (parent) ---
|