@yokio42/unit-labs-cli 0.1.1 → 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 +2 -1
- package/package.json +1 -1
- package/src/index.js +181 -0
package/README.md
CHANGED
|
@@ -47,6 +47,8 @@ unit-labs task new "Ship MVP"
|
|
|
47
47
|
- `unit-labs task new "<title>" [--project <number|name|id>] [--stage <number|name|id>]`
|
|
48
48
|
- `unit-labs task done <number|title|id>`
|
|
49
49
|
- `unit-labs task close <number|title|id>`
|
|
50
|
+
- `unit-labs task assign <number|title|id> <me|none|number|username|email|id>`
|
|
51
|
+
- `unit-labs task unassign <number|title|id>`
|
|
50
52
|
- `unit-labs task edit <number|title|id> --title "<new title>"`
|
|
51
53
|
- `unit-labs task rm <number|title|id>`
|
|
52
54
|
- `unit-labs task delete <number|title|id>`
|
|
@@ -56,4 +58,3 @@ unit-labs task new "Ship MVP"
|
|
|
56
58
|
- По умолчанию API: `https://popuitka2-be.onrender.com`
|
|
57
59
|
- CLI хранит `apiBase`, токен и текущий контекст в `~/.unit-labs/config.json`
|
|
58
60
|
- Для проектов с `workflow=flat` команды `stage` недоступны
|
|
59
|
-
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -68,11 +68,16 @@ Usage:
|
|
|
68
68
|
unit-labs task new "<title>" [--project <number|name|id>] [--stage <number|name|id>]
|
|
69
69
|
unit-labs task done <number|title|id> [--project <number|name|id>] [--stage <number|name|id>]
|
|
70
70
|
unit-labs task close <number|title|id> [--project <number|name|id>] [--stage <number|name|id>]
|
|
71
|
+
unit-labs task assign <number|title|id> <me|none|number|username|email|id> [--project ...] [--stage ...]
|
|
72
|
+
unit-labs task unassign <number|title|id> [--project ...] [--stage ...]
|
|
71
73
|
unit-labs task edit <number|title|id> --title "<new title>" [--project ...] [--stage ...]
|
|
72
74
|
unit-labs task rm <number|title|id> [--project ...] [--stage ...]
|
|
73
75
|
unit-labs task delete <number|title|id> [--project ...] [--stage ...]
|
|
74
76
|
`
|
|
75
77
|
|
|
78
|
+
const ASSIGNEE_SELF_REFS = new Set(["me", "self", "@me"])
|
|
79
|
+
const ASSIGNEE_CLEAR_REFS = new Set(["none", "null", "-", "unassign", "clear"])
|
|
80
|
+
|
|
76
81
|
function parseArgs(argv, shortAliases = {}) {
|
|
77
82
|
const flags = {}
|
|
78
83
|
const positionals = []
|
|
@@ -192,12 +197,73 @@ function getErrorMessage(payload) {
|
|
|
192
197
|
}
|
|
193
198
|
}
|
|
194
199
|
|
|
200
|
+
function resolveMemberByRef(members, ref) {
|
|
201
|
+
if (!ref) {
|
|
202
|
+
throw new Error("assignee reference is required")
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!Array.isArray(members) || members.length === 0) {
|
|
206
|
+
throw new Error("this team has no members to assign")
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const raw = String(ref).trim()
|
|
210
|
+
if (!raw) {
|
|
211
|
+
throw new Error("assignee reference is required")
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (/^\d+$/.test(raw)) {
|
|
215
|
+
const index = Number(raw) - 1
|
|
216
|
+
if (index < 0 || index >= members.length) {
|
|
217
|
+
throw new Error(`assignee index out of range: ${raw}`)
|
|
218
|
+
}
|
|
219
|
+
return members[index]
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const exactId = members.find(member => String(member.id || member._id) === raw)
|
|
223
|
+
if (exactId) {
|
|
224
|
+
return exactId
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const lower = raw.toLowerCase()
|
|
228
|
+
const exactEmail = members.filter(member => String(member.email || "").toLowerCase() === lower)
|
|
229
|
+
if (exactEmail.length === 1) {
|
|
230
|
+
return exactEmail[0]
|
|
231
|
+
}
|
|
232
|
+
if (exactEmail.length > 1) {
|
|
233
|
+
throw new Error(`ambiguous assignee email "${raw}"`)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const exactUsername = members.filter(member => String(member.username || "").toLowerCase() === lower)
|
|
237
|
+
if (exactUsername.length === 1) {
|
|
238
|
+
return exactUsername[0]
|
|
239
|
+
}
|
|
240
|
+
if (exactUsername.length > 1) {
|
|
241
|
+
throw new Error(`ambiguous assignee username "${raw}"`)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const partial = members.filter(member => {
|
|
245
|
+
const username = String(member.username || "").toLowerCase()
|
|
246
|
+
const email = String(member.email || "").toLowerCase()
|
|
247
|
+
return username.includes(lower) || email.includes(lower)
|
|
248
|
+
})
|
|
249
|
+
if (partial.length === 1) {
|
|
250
|
+
return partial[0]
|
|
251
|
+
}
|
|
252
|
+
if (partial.length > 1) {
|
|
253
|
+
throw new Error(`ambiguous assignee reference "${raw}"`)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
throw new Error(`assignee not found: ${raw}`)
|
|
257
|
+
}
|
|
258
|
+
|
|
195
259
|
async function request({ apiBase, token, method, path, body, requiresAuth = true }) {
|
|
196
260
|
if (requiresAuth && !token) {
|
|
197
261
|
throw new Error("not logged in. run: unit-labs auth login")
|
|
198
262
|
}
|
|
199
263
|
|
|
200
264
|
const headers = {}
|
|
265
|
+
headers["X-Client-Source"] = "cli"
|
|
266
|
+
headers["User-Agent"] = "unit-labs-cli/0.1.0"
|
|
201
267
|
if (requiresAuth && token) {
|
|
202
268
|
headers.Authorization = `Bearer ${token}`
|
|
203
269
|
}
|
|
@@ -316,6 +382,26 @@ async function fetchStages(config, projectId) {
|
|
|
316
382
|
return Array.isArray(payload) ? payload : []
|
|
317
383
|
}
|
|
318
384
|
|
|
385
|
+
async function fetchCurrentUser(config) {
|
|
386
|
+
return request({
|
|
387
|
+
apiBase: normalizeApiBase(config.apiBase),
|
|
388
|
+
token: config.token,
|
|
389
|
+
method: "GET",
|
|
390
|
+
path: "/me",
|
|
391
|
+
})
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function fetchTeamMembers(config, teamId) {
|
|
395
|
+
const payload = await request({
|
|
396
|
+
apiBase: normalizeApiBase(config.apiBase),
|
|
397
|
+
token: config.token,
|
|
398
|
+
method: "GET",
|
|
399
|
+
path: `/teams/${teamId}/members`,
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
return payload?.data?.members || []
|
|
403
|
+
}
|
|
404
|
+
|
|
319
405
|
async function fetchFlatTasks(config, projectId) {
|
|
320
406
|
const payload = await request({
|
|
321
407
|
apiBase: normalizeApiBase(config.apiBase),
|
|
@@ -380,6 +466,57 @@ async function resolveStageContext(config, project, stageRef, requireStage = tru
|
|
|
380
466
|
return { stage, stages }
|
|
381
467
|
}
|
|
382
468
|
|
|
469
|
+
async function resolveAssigneeUserId(config, project, assigneeRef) {
|
|
470
|
+
const raw = String(assigneeRef || "").trim()
|
|
471
|
+
if (!raw) {
|
|
472
|
+
throw new Error("assignee reference is required. use: unit-labs task assign <task> <me|none|number|username|email|id>")
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const normalized = raw.toLowerCase()
|
|
476
|
+
if (ASSIGNEE_CLEAR_REFS.has(normalized)) {
|
|
477
|
+
return { userId: null, label: "none" }
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (ASSIGNEE_SELF_REFS.has(normalized)) {
|
|
481
|
+
const me = await fetchCurrentUser(config)
|
|
482
|
+
const meId = me?.id || me?._id
|
|
483
|
+
if (!meId) {
|
|
484
|
+
throw new Error("failed to resolve current user id from /me")
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
userId: String(meId),
|
|
488
|
+
label: me?.email || me?.username || String(meId),
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (project.team_id) {
|
|
493
|
+
const members = await fetchTeamMembers(config, project.team_id)
|
|
494
|
+
const member = resolveMemberByRef(members, raw)
|
|
495
|
+
const memberId = member?.id || member?._id
|
|
496
|
+
if (!memberId) {
|
|
497
|
+
throw new Error("failed to resolve member id")
|
|
498
|
+
}
|
|
499
|
+
return {
|
|
500
|
+
userId: String(memberId),
|
|
501
|
+
label: member?.email || member?.username || String(memberId),
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const me = await fetchCurrentUser(config)
|
|
506
|
+
const meId = me?.id || me?._id
|
|
507
|
+
const meEmail = String(me?.email || "").toLowerCase()
|
|
508
|
+
const meUsername = String(me?.username || "").toLowerCase()
|
|
509
|
+
const matchesMe = [String(meId || ""), meEmail, meUsername].includes(normalized)
|
|
510
|
+
if (!matchesMe) {
|
|
511
|
+
throw new Error("for personal project assignee can only be project owner (use `me`)")
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return {
|
|
515
|
+
userId: String(meId),
|
|
516
|
+
label: me?.email || me?.username || String(meId),
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
383
520
|
function saveConfigPatch(config, patch) {
|
|
384
521
|
const next = {
|
|
385
522
|
...config,
|
|
@@ -863,6 +1000,50 @@ async function runTask(command, args, config) {
|
|
|
863
1000
|
return
|
|
864
1001
|
}
|
|
865
1002
|
|
|
1003
|
+
if (command === "assign" || command === "unassign") {
|
|
1004
|
+
const taskRef = positionals[0]
|
|
1005
|
+
const assigneeRef = command === "unassign" ? "none" : positionals[1]
|
|
1006
|
+
|
|
1007
|
+
if (!taskRef) {
|
|
1008
|
+
throw new Error("task reference is required. use: unit-labs task assign <number|title|id> <assignee>")
|
|
1009
|
+
}
|
|
1010
|
+
if (!assigneeRef) {
|
|
1011
|
+
throw new Error("assignee reference is required. use: unit-labs task assign <task> <me|none|number|username|email|id>")
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const { project, workflow, stage } = await resolveTaskContext(config, flags, true)
|
|
1015
|
+
const tasks = workflow === "flat"
|
|
1016
|
+
? await fetchFlatTasks(config, project._id)
|
|
1017
|
+
: await fetchStageTasks(config, project._id, stage._id)
|
|
1018
|
+
const task = resolveByRef(tasks, taskRef, t => t.title, "task")
|
|
1019
|
+
const assignee = await resolveAssigneeUserId(config, project, assigneeRef)
|
|
1020
|
+
|
|
1021
|
+
const path = workflow === "flat"
|
|
1022
|
+
? `/projects/${project._id}/tasks/${task._id}/assign`
|
|
1023
|
+
: `/projects/${project._id}/stages/${stage._id}/tasks/${task._id}/assign`
|
|
1024
|
+
|
|
1025
|
+
const updated = await request({
|
|
1026
|
+
apiBase: normalizeApiBase(config.apiBase),
|
|
1027
|
+
token: config.token,
|
|
1028
|
+
method: "PATCH",
|
|
1029
|
+
path,
|
|
1030
|
+
body: { user_id: assignee.userId },
|
|
1031
|
+
})
|
|
1032
|
+
|
|
1033
|
+
if (flags.json) {
|
|
1034
|
+
printJson(updated)
|
|
1035
|
+
return
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
printTasks([updated], false)
|
|
1039
|
+
if (assignee.userId) {
|
|
1040
|
+
console.log(`Assigned to: ${assignee.label}`)
|
|
1041
|
+
} else {
|
|
1042
|
+
console.log("Assignee cleared")
|
|
1043
|
+
}
|
|
1044
|
+
return
|
|
1045
|
+
}
|
|
1046
|
+
|
|
866
1047
|
if (command === "edit") {
|
|
867
1048
|
const taskRef = positionals[0]
|
|
868
1049
|
const title = (flags.title || positionals.slice(1).join(" ")).trim()
|