boringpm 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/README.md +170 -0
- package/dist/api.js +487 -0
- package/dist/auth-store.js +50 -0
- package/dist/index.js +786 -0
- package/dist/types.js +10 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# CLI
|
|
2
|
+
|
|
3
|
+
Project manager CLI for scripts, automation, and agent workflows.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
When installed globally, the command is:
|
|
8
|
+
|
|
9
|
+
- `boringpm`
|
|
10
|
+
|
|
11
|
+
## Scripts
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm run dev -- --help
|
|
15
|
+
npm run lint
|
|
16
|
+
npm run build
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Login UX (recommended)
|
|
20
|
+
|
|
21
|
+
Interactive login with persisted token:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm run dev -- login
|
|
25
|
+
npm run dev -- whoami
|
|
26
|
+
npm run dev -- project list
|
|
27
|
+
npm run dev -- logout
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
This stores auth session data in:
|
|
31
|
+
|
|
32
|
+
- `~/.projectmanager/auth.json`
|
|
33
|
+
|
|
34
|
+
## Project reference behavior
|
|
35
|
+
|
|
36
|
+
Commands that target a project accept either:
|
|
37
|
+
|
|
38
|
+
- project ID
|
|
39
|
+
- project name (case-insensitive exact match)
|
|
40
|
+
|
|
41
|
+
If multiple projects have the same name, use the project ID.
|
|
42
|
+
|
|
43
|
+
## Team management examples
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Create and manage teams
|
|
47
|
+
npm run dev -- team create "Engineering Team"
|
|
48
|
+
npm run dev -- team list
|
|
49
|
+
npm run dev -- team view <team-id>
|
|
50
|
+
npm run dev -- team update <team-id> --name "New Team Name"
|
|
51
|
+
|
|
52
|
+
# Add and remove team members (owner only)
|
|
53
|
+
npm run dev -- team add-member <team-id> <user-id>
|
|
54
|
+
npm run dev -- team remove-member <team-id> <user-id>
|
|
55
|
+
|
|
56
|
+
# List projects in a team
|
|
57
|
+
npm run dev -- team projects <team-id>
|
|
58
|
+
|
|
59
|
+
# Delete team (owner only, blocked if projects exist)
|
|
60
|
+
npm run dev -- team delete <team-id>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Project management examples
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Create project (optionally linked to team)
|
|
67
|
+
npm run dev -- project create "My Project"
|
|
68
|
+
npm run dev -- project create "Team Project" --team <team-id>
|
|
69
|
+
|
|
70
|
+
# View projects
|
|
71
|
+
npm run dev -- project list
|
|
72
|
+
npm run dev -- project view minigta
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Single-entity view examples
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm run dev -- plan view <plan-id>
|
|
79
|
+
npm run dev -- task view <task-id>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Spec-first workflow examples
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# Create and approve spec first
|
|
86
|
+
npm run dev -- plan create minigta "Auth hardening spec" --content "..."
|
|
87
|
+
npm run dev -- plan approve <plan-id>
|
|
88
|
+
|
|
89
|
+
# Create task linked to approved spec
|
|
90
|
+
npm run dev -- task create minigta "Implement auth guard" --plan <plan-id>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Agent workflow examples
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# See only tasks assigned to you in one project
|
|
97
|
+
npm run dev -- task list minigta --mine
|
|
98
|
+
|
|
99
|
+
# See your tasks across all visible projects
|
|
100
|
+
npm run dev -- task mine --status in_progress
|
|
101
|
+
|
|
102
|
+
# Claim a specific task
|
|
103
|
+
npm run dev -- task claim <task-id>
|
|
104
|
+
|
|
105
|
+
# Claim the next pending task in the project
|
|
106
|
+
npm run dev -- task next minigta
|
|
107
|
+
|
|
108
|
+
# Add test scenarios and record results
|
|
109
|
+
npm run dev -- task test-add minigta <task-id> "happy path" --steps "..." --expected "..."
|
|
110
|
+
npm run dev -- task tests minigta <task-id>
|
|
111
|
+
npm run dev -- task test-result minigta <task-id> <scenario-id> pass --evidence "CI run #123"
|
|
112
|
+
|
|
113
|
+
# Move task to testing, then complete with note
|
|
114
|
+
npm run dev -- task move <task-id> testing
|
|
115
|
+
npm run dev -- task done <task-id> --note "Implemented endpoint + tests"
|
|
116
|
+
|
|
117
|
+
# Forced completion requires waiver metadata
|
|
118
|
+
npm run dev -- task done <task-id> --force --waiver-reason "prod incident" --approved-by lead-user-id
|
|
119
|
+
|
|
120
|
+
# Release assignment (defaults to pending when in_progress/testing)
|
|
121
|
+
npm run dev -- task release <task-id>
|
|
122
|
+
|
|
123
|
+
# Add or read task comments
|
|
124
|
+
npm run dev -- task comment minigta <task-id> "started implementation"
|
|
125
|
+
npm run dev -- task comments minigta <task-id>
|
|
126
|
+
|
|
127
|
+
# Legacy alias (also clears assignee)
|
|
128
|
+
npm run dev -- task unclaim <task-id>
|
|
129
|
+
|
|
130
|
+
# Delete commands prompt for confirmation by default
|
|
131
|
+
npm run dev -- task delete <task-id>
|
|
132
|
+
npm run dev -- plan delete <plan-id>
|
|
133
|
+
|
|
134
|
+
# Use --force / -f for non-interactive runs
|
|
135
|
+
npm run dev -- task delete <task-id> --force
|
|
136
|
+
npm run dev -- plan delete <plan-id> -f
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Completion gate
|
|
140
|
+
|
|
141
|
+
`task done` is server-enforced and succeeds only when:
|
|
142
|
+
|
|
143
|
+
- task status is `testing`
|
|
144
|
+
- linked spec plan is `approved`
|
|
145
|
+
- at least one test scenario exists
|
|
146
|
+
- every test scenario has `status=pass`
|
|
147
|
+
|
|
148
|
+
Use `--force --waiver-reason --approved-by` only for audited exceptions.
|
|
149
|
+
|
|
150
|
+
## Non-interactive auth options
|
|
151
|
+
|
|
152
|
+
### 1) Environment token
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
export PM_AUTH_TOKEN=<token>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### 2) Per-command token
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
npm run dev -- --token <token> project list
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### 3) Raw auth endpoints
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
npm run dev -- auth send-link you@company.com
|
|
168
|
+
npm run dev -- auth verify you@company.com <code>
|
|
169
|
+
npm run dev -- auth me --token <token>
|
|
170
|
+
```
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const apiResponseSchema = (dataSchema) => z.object({
|
|
3
|
+
data: dataSchema,
|
|
4
|
+
});
|
|
5
|
+
const teamSchema = z.object({
|
|
6
|
+
id: z.string(),
|
|
7
|
+
name: z.string(),
|
|
8
|
+
owner: z.string(),
|
|
9
|
+
members: z.array(z.string()),
|
|
10
|
+
created_at: z.string(),
|
|
11
|
+
updated_at: z.string(),
|
|
12
|
+
});
|
|
13
|
+
const projectSchema = z.object({
|
|
14
|
+
id: z.string(),
|
|
15
|
+
name: z.string(),
|
|
16
|
+
created_at: z.string(),
|
|
17
|
+
owner: z.string(),
|
|
18
|
+
participants: z.array(z.string()),
|
|
19
|
+
team: z.string().nullable(),
|
|
20
|
+
});
|
|
21
|
+
const planSchema = z.object({
|
|
22
|
+
id: z.string(),
|
|
23
|
+
title: z.string(),
|
|
24
|
+
content: z.string(),
|
|
25
|
+
created_at: z.string(),
|
|
26
|
+
updated_at: z.string(),
|
|
27
|
+
owner: z.string(),
|
|
28
|
+
project: z.string(),
|
|
29
|
+
spec_status: z.enum(["draft", "approved", "deprecated"]),
|
|
30
|
+
});
|
|
31
|
+
const validationSnapshotSchema = z.object({
|
|
32
|
+
spec_plan_id: z.string(),
|
|
33
|
+
spec_plan_status: z.enum(["draft", "approved", "deprecated"]),
|
|
34
|
+
total_scenarios: z.number(),
|
|
35
|
+
pass_scenarios: z.number(),
|
|
36
|
+
all_passed: z.boolean(),
|
|
37
|
+
validated_by: z.string(),
|
|
38
|
+
validated_at: z.string(),
|
|
39
|
+
waived: z.boolean(),
|
|
40
|
+
waiver_reason: z.string().nullable(),
|
|
41
|
+
approved_by: z.string().nullable(),
|
|
42
|
+
scenario_statuses: z.array(z.object({
|
|
43
|
+
id: z.string(),
|
|
44
|
+
status: z.enum(["pending", "pass", "fail", "blocked"]),
|
|
45
|
+
ran_at: z.string().nullable(),
|
|
46
|
+
ran_by: z.string().nullable(),
|
|
47
|
+
})),
|
|
48
|
+
});
|
|
49
|
+
const taskSchema = z.object({
|
|
50
|
+
id: z.string(),
|
|
51
|
+
title: z.string(),
|
|
52
|
+
description: z.string(),
|
|
53
|
+
created_at: z.string(),
|
|
54
|
+
owner: z.string(),
|
|
55
|
+
assignee: z.string().nullable(),
|
|
56
|
+
agent: z.string().nullable(),
|
|
57
|
+
status: z.enum(["pending", "in_progress", "testing", "completed", "archived"]),
|
|
58
|
+
project: z.string(),
|
|
59
|
+
plan: z.string().nullable(),
|
|
60
|
+
updated_at: z.string(),
|
|
61
|
+
completed_note: z.string().nullable(),
|
|
62
|
+
completed_at: z.string().nullable(),
|
|
63
|
+
validation_snapshot: validationSnapshotSchema.nullable(),
|
|
64
|
+
});
|
|
65
|
+
const testScenarioSchema = z.object({
|
|
66
|
+
id: z.string(),
|
|
67
|
+
task: z.string(),
|
|
68
|
+
title: z.string(),
|
|
69
|
+
type: z.enum(["manual", "automated", "integration", "e2e"]),
|
|
70
|
+
steps: z.string(),
|
|
71
|
+
expected_result: z.string(),
|
|
72
|
+
status: z.enum(["pending", "pass", "fail", "blocked"]),
|
|
73
|
+
evidence: z.string().nullable(),
|
|
74
|
+
ran_by: z.string().nullable(),
|
|
75
|
+
ran_at: z.string().nullable(),
|
|
76
|
+
created_at: z.string(),
|
|
77
|
+
updated_at: z.string(),
|
|
78
|
+
});
|
|
79
|
+
const commentSchema = z.object({
|
|
80
|
+
id: z.string(),
|
|
81
|
+
task: z.string(),
|
|
82
|
+
author: z.string(),
|
|
83
|
+
content: z.string(),
|
|
84
|
+
created_at: z.string(),
|
|
85
|
+
});
|
|
86
|
+
const boardDataSchema = z.object({
|
|
87
|
+
project: projectSchema,
|
|
88
|
+
plans: z.array(planSchema),
|
|
89
|
+
tasks: z.array(taskSchema),
|
|
90
|
+
groupedTasks: z.record(z.array(taskSchema)),
|
|
91
|
+
});
|
|
92
|
+
const authUserSchema = z.object({
|
|
93
|
+
id: z.string(),
|
|
94
|
+
email: z.string().nullable(),
|
|
95
|
+
});
|
|
96
|
+
export class ApiClient {
|
|
97
|
+
baseUrl;
|
|
98
|
+
authToken;
|
|
99
|
+
actorName;
|
|
100
|
+
constructor(config) {
|
|
101
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
102
|
+
this.authToken = config.authToken;
|
|
103
|
+
this.actorName = config.actorName;
|
|
104
|
+
}
|
|
105
|
+
async health() {
|
|
106
|
+
return this.request("/health", {
|
|
107
|
+
method: "GET",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
async sendMagicLink(email) {
|
|
111
|
+
const payload = await this.request("/auth/magic-link", {
|
|
112
|
+
method: "POST",
|
|
113
|
+
body: { email },
|
|
114
|
+
});
|
|
115
|
+
return apiResponseSchema(z.object({ sent: z.boolean() })).parse(payload).data;
|
|
116
|
+
}
|
|
117
|
+
async verifyMagicCode(email, code) {
|
|
118
|
+
const payload = await this.request("/auth/verify", {
|
|
119
|
+
method: "POST",
|
|
120
|
+
body: { email, code },
|
|
121
|
+
});
|
|
122
|
+
return apiResponseSchema(z.object({ token: z.string(), user: authUserSchema })).parse(payload)
|
|
123
|
+
.data;
|
|
124
|
+
}
|
|
125
|
+
async me() {
|
|
126
|
+
const payload = await this.request("/auth/me", {
|
|
127
|
+
method: "GET",
|
|
128
|
+
authToken: this.requireToken(),
|
|
129
|
+
});
|
|
130
|
+
return apiResponseSchema(authUserSchema).parse(payload).data;
|
|
131
|
+
}
|
|
132
|
+
async createTeam(input) {
|
|
133
|
+
const payload = await this.request("/teams", {
|
|
134
|
+
method: "POST",
|
|
135
|
+
body: input,
|
|
136
|
+
authToken: this.requireToken(),
|
|
137
|
+
});
|
|
138
|
+
return apiResponseSchema(teamSchema).parse(payload).data;
|
|
139
|
+
}
|
|
140
|
+
async listTeams() {
|
|
141
|
+
const payload = await this.request("/teams", {
|
|
142
|
+
method: "GET",
|
|
143
|
+
authToken: this.requireToken(),
|
|
144
|
+
});
|
|
145
|
+
return apiResponseSchema(z.array(teamSchema)).parse(payload).data;
|
|
146
|
+
}
|
|
147
|
+
async getTeam(teamId) {
|
|
148
|
+
const payload = await this.request(`/teams/${teamId}`, {
|
|
149
|
+
method: "GET",
|
|
150
|
+
authToken: this.requireToken(),
|
|
151
|
+
});
|
|
152
|
+
return apiResponseSchema(teamSchema).parse(payload).data;
|
|
153
|
+
}
|
|
154
|
+
async updateTeam(input) {
|
|
155
|
+
const payload = await this.request(`/teams/${input.teamId}`, {
|
|
156
|
+
method: "PATCH",
|
|
157
|
+
body: {
|
|
158
|
+
name: input.name,
|
|
159
|
+
},
|
|
160
|
+
authToken: this.requireToken(),
|
|
161
|
+
});
|
|
162
|
+
return apiResponseSchema(teamSchema).parse(payload).data;
|
|
163
|
+
}
|
|
164
|
+
async deleteTeam(teamId) {
|
|
165
|
+
const payload = await this.request(`/teams/${teamId}`, {
|
|
166
|
+
method: "DELETE",
|
|
167
|
+
authToken: this.requireToken(),
|
|
168
|
+
});
|
|
169
|
+
return apiResponseSchema(z.object({
|
|
170
|
+
id: z.string(),
|
|
171
|
+
deleted: z.boolean(),
|
|
172
|
+
})).parse(payload).data;
|
|
173
|
+
}
|
|
174
|
+
async addTeamMember(teamId, userId) {
|
|
175
|
+
const payload = await this.request(`/teams/${teamId}/members`, {
|
|
176
|
+
method: "POST",
|
|
177
|
+
body: { userId },
|
|
178
|
+
authToken: this.requireToken(),
|
|
179
|
+
});
|
|
180
|
+
return apiResponseSchema(teamSchema).parse(payload).data;
|
|
181
|
+
}
|
|
182
|
+
async removeTeamMember(teamId, userId) {
|
|
183
|
+
const payload = await this.request(`/teams/${teamId}/members/${userId}`, {
|
|
184
|
+
method: "DELETE",
|
|
185
|
+
authToken: this.requireToken(),
|
|
186
|
+
});
|
|
187
|
+
return apiResponseSchema(teamSchema).parse(payload).data;
|
|
188
|
+
}
|
|
189
|
+
async listTeamProjects(teamId) {
|
|
190
|
+
const payload = await this.request(`/teams/${teamId}/projects`, {
|
|
191
|
+
method: "GET",
|
|
192
|
+
authToken: this.requireToken(),
|
|
193
|
+
});
|
|
194
|
+
return apiResponseSchema(z.array(projectSchema)).parse(payload).data;
|
|
195
|
+
}
|
|
196
|
+
async createTeamProject(input) {
|
|
197
|
+
const payload = await this.request(`/teams/${input.teamId}/projects`, {
|
|
198
|
+
method: "POST",
|
|
199
|
+
body: {
|
|
200
|
+
name: input.name,
|
|
201
|
+
participants: input.participants,
|
|
202
|
+
},
|
|
203
|
+
authToken: this.requireToken(),
|
|
204
|
+
});
|
|
205
|
+
return apiResponseSchema(projectSchema).parse(payload).data;
|
|
206
|
+
}
|
|
207
|
+
async listProjects() {
|
|
208
|
+
const payload = await this.request("/projects", {
|
|
209
|
+
method: "GET",
|
|
210
|
+
authToken: this.requireToken(),
|
|
211
|
+
});
|
|
212
|
+
return apiResponseSchema(z.array(projectSchema)).parse(payload).data;
|
|
213
|
+
}
|
|
214
|
+
async getProject(projectId) {
|
|
215
|
+
const payload = await this.request(`/projects/${projectId}`, {
|
|
216
|
+
method: "GET",
|
|
217
|
+
authToken: this.requireToken(),
|
|
218
|
+
});
|
|
219
|
+
return apiResponseSchema(projectSchema).parse(payload).data;
|
|
220
|
+
}
|
|
221
|
+
async createProject(input) {
|
|
222
|
+
const payload = await this.request("/projects", {
|
|
223
|
+
method: "POST",
|
|
224
|
+
body: input,
|
|
225
|
+
authToken: this.requireToken(),
|
|
226
|
+
});
|
|
227
|
+
return apiResponseSchema(projectSchema).parse(payload).data;
|
|
228
|
+
}
|
|
229
|
+
async addProjectParticipant(projectId, participantUserId) {
|
|
230
|
+
const payload = await this.request(`/projects/${projectId}/participants`, {
|
|
231
|
+
method: "POST",
|
|
232
|
+
body: { userId: participantUserId },
|
|
233
|
+
authToken: this.requireToken(),
|
|
234
|
+
});
|
|
235
|
+
return apiResponseSchema(projectSchema).parse(payload).data;
|
|
236
|
+
}
|
|
237
|
+
async removeProjectParticipant(projectId, participantUserId) {
|
|
238
|
+
const payload = await this.request(`/projects/${projectId}/participants/${participantUserId}`, {
|
|
239
|
+
method: "DELETE",
|
|
240
|
+
authToken: this.requireToken(),
|
|
241
|
+
});
|
|
242
|
+
return apiResponseSchema(projectSchema).parse(payload).data;
|
|
243
|
+
}
|
|
244
|
+
async listPlans(projectId) {
|
|
245
|
+
const payload = await this.request(`/projects/${projectId}/plans`, {
|
|
246
|
+
method: "GET",
|
|
247
|
+
authToken: this.requireToken(),
|
|
248
|
+
});
|
|
249
|
+
return apiResponseSchema(z.array(planSchema)).parse(payload).data;
|
|
250
|
+
}
|
|
251
|
+
async getPlan(planId) {
|
|
252
|
+
const payload = await this.request(`/plans/${planId}`, {
|
|
253
|
+
method: "GET",
|
|
254
|
+
authToken: this.requireToken(),
|
|
255
|
+
});
|
|
256
|
+
return apiResponseSchema(planSchema).parse(payload).data;
|
|
257
|
+
}
|
|
258
|
+
async createPlan(input) {
|
|
259
|
+
const payload = await this.request(`/projects/${input.projectId}/plans`, {
|
|
260
|
+
method: "POST",
|
|
261
|
+
body: {
|
|
262
|
+
title: input.title,
|
|
263
|
+
content: input.content,
|
|
264
|
+
spec_status: input.spec_status,
|
|
265
|
+
},
|
|
266
|
+
authToken: this.requireToken(),
|
|
267
|
+
});
|
|
268
|
+
return apiResponseSchema(planSchema).parse(payload).data;
|
|
269
|
+
}
|
|
270
|
+
async updatePlan(input) {
|
|
271
|
+
const payload = await this.request(`/plans/${input.planId}`, {
|
|
272
|
+
method: "PATCH",
|
|
273
|
+
body: {
|
|
274
|
+
title: input.title,
|
|
275
|
+
content: input.content,
|
|
276
|
+
spec_status: input.spec_status,
|
|
277
|
+
},
|
|
278
|
+
authToken: this.requireToken(),
|
|
279
|
+
});
|
|
280
|
+
return apiResponseSchema(planSchema).parse(payload).data;
|
|
281
|
+
}
|
|
282
|
+
async deletePlan(planId) {
|
|
283
|
+
const payload = await this.request(`/plans/${planId}`, {
|
|
284
|
+
method: "DELETE",
|
|
285
|
+
authToken: this.requireToken(),
|
|
286
|
+
});
|
|
287
|
+
return apiResponseSchema(z.object({
|
|
288
|
+
id: z.string(),
|
|
289
|
+
deleted: z.boolean(),
|
|
290
|
+
unlinkedTasks: z.number(),
|
|
291
|
+
})).parse(payload).data;
|
|
292
|
+
}
|
|
293
|
+
async listTasks(projectId, filters) {
|
|
294
|
+
const query = new URLSearchParams();
|
|
295
|
+
if (filters?.status) {
|
|
296
|
+
query.set("status", filters.status);
|
|
297
|
+
}
|
|
298
|
+
if (filters?.mine) {
|
|
299
|
+
query.set("mine", "true");
|
|
300
|
+
}
|
|
301
|
+
if (filters?.assignee !== undefined) {
|
|
302
|
+
query.set("assignee", filters.assignee === null ? "none" : filters.assignee);
|
|
303
|
+
}
|
|
304
|
+
if (filters?.agent !== undefined) {
|
|
305
|
+
query.set("agent", filters.agent === null ? "none" : filters.agent);
|
|
306
|
+
}
|
|
307
|
+
const queryString = query.toString();
|
|
308
|
+
const path = queryString
|
|
309
|
+
? `/projects/${projectId}/tasks?${queryString}`
|
|
310
|
+
: `/projects/${projectId}/tasks`;
|
|
311
|
+
const payload = await this.request(path, {
|
|
312
|
+
method: "GET",
|
|
313
|
+
authToken: this.requireToken(),
|
|
314
|
+
});
|
|
315
|
+
return apiResponseSchema(z.array(taskSchema)).parse(payload).data;
|
|
316
|
+
}
|
|
317
|
+
async getTask(taskId) {
|
|
318
|
+
const payload = await this.request(`/tasks/${taskId}`, {
|
|
319
|
+
method: "GET",
|
|
320
|
+
authToken: this.requireToken(),
|
|
321
|
+
});
|
|
322
|
+
return apiResponseSchema(taskSchema).parse(payload).data;
|
|
323
|
+
}
|
|
324
|
+
async createTask(input) {
|
|
325
|
+
const payload = await this.request(`/projects/${input.projectId}/tasks`, {
|
|
326
|
+
method: "POST",
|
|
327
|
+
body: {
|
|
328
|
+
title: input.title,
|
|
329
|
+
description: input.description,
|
|
330
|
+
status: input.status,
|
|
331
|
+
plan: input.plan,
|
|
332
|
+
agent: input.agent,
|
|
333
|
+
},
|
|
334
|
+
authToken: this.requireToken(),
|
|
335
|
+
});
|
|
336
|
+
return apiResponseSchema(taskSchema).parse(payload).data;
|
|
337
|
+
}
|
|
338
|
+
async updateTask(input) {
|
|
339
|
+
const payload = await this.request(`/tasks/${input.taskId}`, {
|
|
340
|
+
method: "PATCH",
|
|
341
|
+
body: {
|
|
342
|
+
title: input.title,
|
|
343
|
+
description: input.description,
|
|
344
|
+
status: input.status,
|
|
345
|
+
plan: input.plan,
|
|
346
|
+
assignee: input.assignee,
|
|
347
|
+
agent: input.agent,
|
|
348
|
+
},
|
|
349
|
+
authToken: this.requireToken(),
|
|
350
|
+
});
|
|
351
|
+
return apiResponseSchema(taskSchema).parse(payload).data;
|
|
352
|
+
}
|
|
353
|
+
async claimTask(taskId) {
|
|
354
|
+
const payload = await this.request(`/tasks/${taskId}/claim`, {
|
|
355
|
+
method: "POST",
|
|
356
|
+
authToken: this.requireToken(),
|
|
357
|
+
});
|
|
358
|
+
return apiResponseSchema(taskSchema).parse(payload).data;
|
|
359
|
+
}
|
|
360
|
+
async releaseTask(input) {
|
|
361
|
+
const payload = await this.request(`/tasks/${input.taskId}/release`, {
|
|
362
|
+
method: "POST",
|
|
363
|
+
body: {
|
|
364
|
+
status: input.status,
|
|
365
|
+
},
|
|
366
|
+
authToken: this.requireToken(),
|
|
367
|
+
});
|
|
368
|
+
return apiResponseSchema(taskSchema).parse(payload).data;
|
|
369
|
+
}
|
|
370
|
+
async claimNextTask(projectId) {
|
|
371
|
+
const payload = await this.request(`/projects/${projectId}/tasks/next`, {
|
|
372
|
+
method: "POST",
|
|
373
|
+
authToken: this.requireToken(),
|
|
374
|
+
});
|
|
375
|
+
return apiResponseSchema(taskSchema).parse(payload).data;
|
|
376
|
+
}
|
|
377
|
+
async completeTask(input) {
|
|
378
|
+
const payload = await this.request(`/tasks/${input.taskId}/done`, {
|
|
379
|
+
method: "POST",
|
|
380
|
+
body: {
|
|
381
|
+
note: input.note,
|
|
382
|
+
force: input.force,
|
|
383
|
+
waiver_reason: input.waiver_reason,
|
|
384
|
+
approved_by: input.approved_by,
|
|
385
|
+
},
|
|
386
|
+
authToken: this.requireToken(),
|
|
387
|
+
});
|
|
388
|
+
return apiResponseSchema(taskSchema).parse(payload).data;
|
|
389
|
+
}
|
|
390
|
+
async listTestScenarios(taskId) {
|
|
391
|
+
const payload = await this.request(`/tasks/${taskId}/test-scenarios`, {
|
|
392
|
+
method: "GET",
|
|
393
|
+
authToken: this.requireToken(),
|
|
394
|
+
});
|
|
395
|
+
return apiResponseSchema(z.array(testScenarioSchema)).parse(payload).data;
|
|
396
|
+
}
|
|
397
|
+
async createTestScenario(input) {
|
|
398
|
+
const payload = await this.request(`/tasks/${input.taskId}/test-scenarios`, {
|
|
399
|
+
method: "POST",
|
|
400
|
+
body: {
|
|
401
|
+
title: input.title,
|
|
402
|
+
type: input.type,
|
|
403
|
+
steps: input.steps,
|
|
404
|
+
expected_result: input.expected_result,
|
|
405
|
+
},
|
|
406
|
+
authToken: this.requireToken(),
|
|
407
|
+
});
|
|
408
|
+
return apiResponseSchema(testScenarioSchema).parse(payload).data;
|
|
409
|
+
}
|
|
410
|
+
async updateTestScenarioResult(input) {
|
|
411
|
+
const payload = await this.request(`/tasks/${input.taskId}/test-scenarios/${input.scenarioId}/result`, {
|
|
412
|
+
method: "PATCH",
|
|
413
|
+
body: {
|
|
414
|
+
status: input.status,
|
|
415
|
+
evidence: input.evidence,
|
|
416
|
+
},
|
|
417
|
+
authToken: this.requireToken(),
|
|
418
|
+
});
|
|
419
|
+
return apiResponseSchema(testScenarioSchema).parse(payload).data;
|
|
420
|
+
}
|
|
421
|
+
async listTaskComments(taskId) {
|
|
422
|
+
const payload = await this.request(`/tasks/${taskId}/comments`, {
|
|
423
|
+
method: "GET",
|
|
424
|
+
authToken: this.requireToken(),
|
|
425
|
+
});
|
|
426
|
+
return apiResponseSchema(z.array(commentSchema)).parse(payload).data;
|
|
427
|
+
}
|
|
428
|
+
async createTaskComment(taskId, content) {
|
|
429
|
+
const payload = await this.request(`/tasks/${taskId}/comments`, {
|
|
430
|
+
method: "POST",
|
|
431
|
+
body: {
|
|
432
|
+
content,
|
|
433
|
+
},
|
|
434
|
+
authToken: this.requireToken(),
|
|
435
|
+
});
|
|
436
|
+
return apiResponseSchema(commentSchema).parse(payload).data;
|
|
437
|
+
}
|
|
438
|
+
async deleteTask(taskId) {
|
|
439
|
+
const payload = await this.request(`/tasks/${taskId}`, {
|
|
440
|
+
method: "DELETE",
|
|
441
|
+
authToken: this.requireToken(),
|
|
442
|
+
});
|
|
443
|
+
return apiResponseSchema(z.object({
|
|
444
|
+
id: z.string(),
|
|
445
|
+
deleted: z.boolean(),
|
|
446
|
+
})).parse(payload).data;
|
|
447
|
+
}
|
|
448
|
+
async board(projectId) {
|
|
449
|
+
const payload = await this.request(`/projects/${projectId}/board`, {
|
|
450
|
+
method: "GET",
|
|
451
|
+
authToken: this.requireToken(),
|
|
452
|
+
});
|
|
453
|
+
return apiResponseSchema(boardDataSchema).parse(payload).data;
|
|
454
|
+
}
|
|
455
|
+
requireToken() {
|
|
456
|
+
if (!this.authToken) {
|
|
457
|
+
throw new Error("Missing auth token. Provide --token, PM_AUTH_TOKEN, or log in to save a session token.");
|
|
458
|
+
}
|
|
459
|
+
return this.authToken;
|
|
460
|
+
}
|
|
461
|
+
async request(path, options = {}) {
|
|
462
|
+
const headers = {
|
|
463
|
+
"Content-Type": "application/json",
|
|
464
|
+
};
|
|
465
|
+
if (options.authToken) {
|
|
466
|
+
headers.Authorization = `Bearer ${options.authToken}`;
|
|
467
|
+
headers["x-auth-token"] = options.authToken;
|
|
468
|
+
}
|
|
469
|
+
if (this.actorName) {
|
|
470
|
+
headers["x-actor-name"] = this.actorName;
|
|
471
|
+
}
|
|
472
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
473
|
+
method: options.method ?? "GET",
|
|
474
|
+
headers,
|
|
475
|
+
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
|
|
476
|
+
});
|
|
477
|
+
const text = await response.text();
|
|
478
|
+
const payload = text ? JSON.parse(text) : {};
|
|
479
|
+
if (!response.ok) {
|
|
480
|
+
const message = typeof payload?.error === "string"
|
|
481
|
+
? payload.error
|
|
482
|
+
: `Request failed with status ${response.status}`;
|
|
483
|
+
throw new Error(message);
|
|
484
|
+
}
|
|
485
|
+
return payload;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
const AUTH_DIRECTORY = join(homedir(), ".projectmanager");
|
|
6
|
+
const AUTH_FILE = join(AUTH_DIRECTORY, "auth.json");
|
|
7
|
+
export function getAuthFilePath() {
|
|
8
|
+
return AUTH_FILE;
|
|
9
|
+
}
|
|
10
|
+
export function loadStoredAuthSession() {
|
|
11
|
+
if (!existsSync(AUTH_FILE)) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const raw = readFileSync(AUTH_FILE, "utf8");
|
|
16
|
+
const parsed = JSON.parse(raw);
|
|
17
|
+
if (typeof parsed.token !== "string" || !parsed.token.trim()) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
token: parsed.token,
|
|
22
|
+
user: parsed.user &&
|
|
23
|
+
typeof parsed.user.id === "string" &&
|
|
24
|
+
(typeof parsed.user.email === "string" || parsed.user.email === null)
|
|
25
|
+
? {
|
|
26
|
+
id: parsed.user.id,
|
|
27
|
+
email: parsed.user.email,
|
|
28
|
+
}
|
|
29
|
+
: undefined,
|
|
30
|
+
savedAt: typeof parsed.savedAt === "string" && parsed.savedAt.trim()
|
|
31
|
+
? parsed.savedAt
|
|
32
|
+
: new Date().toISOString(),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function loadStoredAuthToken() {
|
|
40
|
+
return loadStoredAuthSession()?.token;
|
|
41
|
+
}
|
|
42
|
+
export async function saveStoredAuthSession(session) {
|
|
43
|
+
await mkdir(AUTH_DIRECTORY, { recursive: true });
|
|
44
|
+
await writeFile(AUTH_FILE, JSON.stringify(session, null, 2), {
|
|
45
|
+
mode: 0o600,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
export async function clearStoredAuthSession() {
|
|
49
|
+
await rm(AUTH_FILE, { force: true });
|
|
50
|
+
}
|