@yokio42/unit-labs-cli 0.1.0 → 0.1.1
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 +59 -0
- package/package.json +1 -1
- package/src/config.js +34 -17
- package/src/index.js +844 -143
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# @yokio42/unit-labs-cli
|
|
2
|
+
|
|
3
|
+
CLI для Unit Labs: логин, выбор проекта/стейджа и работа с задачами из терминала.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i -g @yokio42/unit-labs-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
unit-labs auth login
|
|
15
|
+
unit-labs project ls
|
|
16
|
+
unit-labs project use 1
|
|
17
|
+
unit-labs task ls
|
|
18
|
+
unit-labs task new "Ship MVP"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Commands
|
|
22
|
+
|
|
23
|
+
### Auth
|
|
24
|
+
- `unit-labs auth login`
|
|
25
|
+
- `unit-labs auth whoami`
|
|
26
|
+
- `unit-labs auth logout`
|
|
27
|
+
|
|
28
|
+
### Config
|
|
29
|
+
- `unit-labs config show`
|
|
30
|
+
- `unit-labs config set-api <url>`
|
|
31
|
+
|
|
32
|
+
### Project
|
|
33
|
+
- `unit-labs project ls [--team <teamId>] [--json]`
|
|
34
|
+
- `unit-labs project new --name "<name>" [--workflow stages|flat] [--team <teamId>]`
|
|
35
|
+
- `unit-labs project view <number|name|id> [--json]`
|
|
36
|
+
- `unit-labs project use <number|name|id>`
|
|
37
|
+
- `unit-labs project current [--json]`
|
|
38
|
+
|
|
39
|
+
### Stage
|
|
40
|
+
- `unit-labs stage ls [--project <number|name|id>] [--json]`
|
|
41
|
+
- `unit-labs stage new --name "<name>" --description "<text>" [--project <number|name|id>]`
|
|
42
|
+
- `unit-labs stage use <number|name|id> [--project <number|name|id>]`
|
|
43
|
+
- `unit-labs stage current [--json]`
|
|
44
|
+
|
|
45
|
+
### Task
|
|
46
|
+
- `unit-labs task ls [--project <number|name|id>] [--stage <number|name|id>] [--all] [--json]`
|
|
47
|
+
- `unit-labs task new "<title>" [--project <number|name|id>] [--stage <number|name|id>]`
|
|
48
|
+
- `unit-labs task done <number|title|id>`
|
|
49
|
+
- `unit-labs task close <number|title|id>`
|
|
50
|
+
- `unit-labs task edit <number|title|id> --title "<new title>"`
|
|
51
|
+
- `unit-labs task rm <number|title|id>`
|
|
52
|
+
- `unit-labs task delete <number|title|id>`
|
|
53
|
+
|
|
54
|
+
## Notes
|
|
55
|
+
|
|
56
|
+
- По умолчанию API: `https://popuitka2-be.onrender.com`
|
|
57
|
+
- CLI хранит `apiBase`, токен и текущий контекст в `~/.unit-labs/config.json`
|
|
58
|
+
- Для проектов с `workflow=flat` команды `stage` недоступны
|
|
59
|
+
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -20,37 +20,53 @@ function ensureConfigDir() {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
function getDefaultConfig() {
|
|
24
|
+
return {
|
|
25
|
+
apiBase: DEFAULT_API_BASE,
|
|
26
|
+
token: null,
|
|
27
|
+
currentProjectId: null,
|
|
28
|
+
currentProjectName: null,
|
|
29
|
+
currentProjectWorkflow: null,
|
|
30
|
+
currentStageId: null,
|
|
31
|
+
currentStageName: null,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeConfig(input) {
|
|
36
|
+
const base = getDefaultConfig()
|
|
37
|
+
const raw = input && typeof input === "object" ? input : {}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
...base,
|
|
41
|
+
...raw,
|
|
42
|
+
apiBase: normalizeApiBase(raw.apiBase || base.apiBase),
|
|
43
|
+
token: raw.token || null,
|
|
44
|
+
currentProjectId: raw.currentProjectId || null,
|
|
45
|
+
currentProjectName: raw.currentProjectName || null,
|
|
46
|
+
currentProjectWorkflow: raw.currentProjectWorkflow || null,
|
|
47
|
+
currentStageId: raw.currentStageId || null,
|
|
48
|
+
currentStageName: raw.currentStageName || null,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
23
52
|
function readConfig() {
|
|
24
53
|
ensureConfigDir()
|
|
25
54
|
|
|
26
55
|
if (!fs.existsSync(CONFIG_FILE)) {
|
|
27
|
-
return
|
|
28
|
-
apiBase: DEFAULT_API_BASE,
|
|
29
|
-
token: null,
|
|
30
|
-
}
|
|
56
|
+
return getDefaultConfig()
|
|
31
57
|
}
|
|
32
58
|
|
|
33
59
|
try {
|
|
34
60
|
const raw = fs.readFileSync(CONFIG_FILE, "utf-8")
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
apiBase: normalizeApiBase(parsed.apiBase),
|
|
38
|
-
token: parsed.token || null,
|
|
39
|
-
}
|
|
61
|
+
return normalizeConfig(JSON.parse(raw))
|
|
40
62
|
} catch (error) {
|
|
41
|
-
return
|
|
42
|
-
apiBase: DEFAULT_API_BASE,
|
|
43
|
-
token: null,
|
|
44
|
-
}
|
|
63
|
+
return getDefaultConfig()
|
|
45
64
|
}
|
|
46
65
|
}
|
|
47
66
|
|
|
48
67
|
function writeConfig(config) {
|
|
49
68
|
ensureConfigDir()
|
|
50
|
-
const normalized =
|
|
51
|
-
apiBase: normalizeApiBase(config.apiBase),
|
|
52
|
-
token: config.token || null,
|
|
53
|
-
}
|
|
69
|
+
const normalized = normalizeConfig(config)
|
|
54
70
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(normalized, null, 2), "utf-8")
|
|
55
71
|
}
|
|
56
72
|
|
|
@@ -61,3 +77,4 @@ module.exports = {
|
|
|
61
77
|
readConfig,
|
|
62
78
|
writeConfig,
|
|
63
79
|
}
|
|
80
|
+
|
package/src/index.js
CHANGED
|
@@ -3,78 +3,178 @@ const { stdin: input, stdout: output } = require("process")
|
|
|
3
3
|
const { CONFIG_FILE, DEFAULT_API_BASE, normalizeApiBase, readConfig, writeConfig } = require("./config")
|
|
4
4
|
|
|
5
5
|
const UNIT_LABS_BANNER = String.raw`
|
|
6
|
-
|
|
7
|
-
| |
|
|
8
|
-
| |
|
|
9
|
-
| |
|
|
10
|
-
|
|
6
|
+
/$$ /$$ /$$ /$$ /$$$$$$ /$$$$$$$$ /$$ /$$$$$$ /$$$$$$$ /$$$$$$
|
|
7
|
+
| $$ | $$| $$$ | $$|_ $$_/|__ $$__/ | $$ /$$__ $$| $$__ $$ /$$__ $$
|
|
8
|
+
| $$ | $$| $$$$| $$ | $$ | $$ | $$ | $$ \ $$| $$ \ $$| $$ \__/
|
|
9
|
+
| $$ | $$| $$ $$ $$ | $$ | $$ /$$$$$$| $$ | $$$$$$$$| $$$$$$$ | $$$$$$
|
|
10
|
+
| $$ | $$| $$ $$$$ | $$ | $$|______/| $$ | $$__ $$| $$__ $$ \____ $$
|
|
11
|
+
| $$ | $$| $$\ $$$ | $$ | $$ | $$ | $$ | $$| $$ \ $$ /$$ \ $$
|
|
12
|
+
| $$$$$$/| $$ \ $$ /$$$$$$ | $$ | $$$$$$$$| $$ | $$| $$$$$$$/| $$$$$$/
|
|
13
|
+
\______/ |__/ \__/|______/ |__/ |________/|__/ |__/|_______/ \______/
|
|
11
14
|
`
|
|
12
15
|
|
|
13
|
-
const
|
|
16
|
+
const ROOT_HELP = `
|
|
14
17
|
Unit Labs CLI
|
|
15
18
|
|
|
19
|
+
Usage:
|
|
20
|
+
unit-labs auth <command>
|
|
21
|
+
unit-labs config <command>
|
|
22
|
+
unit-labs project <command>
|
|
23
|
+
unit-labs stage <command>
|
|
24
|
+
unit-labs task <command>
|
|
25
|
+
unit-labs --help
|
|
26
|
+
|
|
27
|
+
Quick start:
|
|
28
|
+
unit-labs auth login
|
|
29
|
+
unit-labs project ls
|
|
30
|
+
unit-labs project use 1
|
|
31
|
+
unit-labs task ls
|
|
32
|
+
unit-labs task new "Ship MVP"
|
|
33
|
+
`
|
|
34
|
+
|
|
35
|
+
const AUTH_HELP = `
|
|
16
36
|
Usage:
|
|
17
37
|
unit-labs auth login [--login <email>] [--password <password>] [--api <url>]
|
|
18
|
-
unit-labs auth logout
|
|
19
38
|
unit-labs auth whoami
|
|
39
|
+
unit-labs auth logout
|
|
40
|
+
`
|
|
41
|
+
|
|
42
|
+
const CONFIG_HELP = `
|
|
43
|
+
Usage:
|
|
20
44
|
unit-labs config show
|
|
21
45
|
unit-labs config set-api <url>
|
|
22
|
-
|
|
46
|
+
`
|
|
47
|
+
|
|
48
|
+
const PROJECT_HELP = `
|
|
49
|
+
Usage:
|
|
50
|
+
unit-labs project ls [--team <teamId>] [--json]
|
|
51
|
+
unit-labs project new --name "<name>" [--workflow stages|flat] [--team <teamId>]
|
|
52
|
+
unit-labs project view <number|name|id> [--json]
|
|
53
|
+
unit-labs project use <number|name|id>
|
|
54
|
+
unit-labs project current [--json]
|
|
55
|
+
`
|
|
56
|
+
|
|
57
|
+
const STAGE_HELP = `
|
|
58
|
+
Usage:
|
|
59
|
+
unit-labs stage ls [--project <number|name|id>] [--json]
|
|
60
|
+
unit-labs stage new --name "<name>" --description "<text>" [--project <number|name|id>]
|
|
61
|
+
unit-labs stage use <number|name|id> [--project <number|name|id>]
|
|
62
|
+
unit-labs stage current [--json]
|
|
63
|
+
`
|
|
23
64
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
--
|
|
29
|
-
-
|
|
65
|
+
const TASK_HELP = `
|
|
66
|
+
Usage:
|
|
67
|
+
unit-labs task ls [--project <number|name|id>] [--stage <number|name|id>] [--all] [--json]
|
|
68
|
+
unit-labs task new "<title>" [--project <number|name|id>] [--stage <number|name|id>]
|
|
69
|
+
unit-labs task done <number|title|id> [--project <number|name|id>] [--stage <number|name|id>]
|
|
70
|
+
unit-labs task close <number|title|id> [--project <number|name|id>] [--stage <number|name|id>]
|
|
71
|
+
unit-labs task edit <number|title|id> --title "<new title>" [--project ...] [--stage ...]
|
|
72
|
+
unit-labs task rm <number|title|id> [--project ...] [--stage ...]
|
|
73
|
+
unit-labs task delete <number|title|id> [--project ...] [--stage ...]
|
|
30
74
|
`
|
|
31
75
|
|
|
32
|
-
function
|
|
76
|
+
function parseArgs(argv, shortAliases = {}) {
|
|
33
77
|
const flags = {}
|
|
78
|
+
const positionals = []
|
|
34
79
|
|
|
35
80
|
for (let i = 0; i < argv.length; i += 1) {
|
|
36
|
-
const
|
|
37
|
-
const value = argv[i + 1]
|
|
81
|
+
const arg = argv[i]
|
|
38
82
|
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
continue
|
|
83
|
+
if (arg === "--") {
|
|
84
|
+
positionals.push(...argv.slice(i + 1))
|
|
85
|
+
break
|
|
43
86
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
87
|
+
|
|
88
|
+
if (arg.startsWith("--")) {
|
|
89
|
+
const [rawKey, inlineValue] = arg.slice(2).split("=")
|
|
90
|
+
if (inlineValue !== undefined) {
|
|
91
|
+
flags[rawKey] = inlineValue
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const next = argv[i + 1]
|
|
96
|
+
if (next && !next.startsWith("-")) {
|
|
97
|
+
flags[rawKey] = next
|
|
98
|
+
i += 1
|
|
99
|
+
} else {
|
|
100
|
+
flags[rawKey] = true
|
|
101
|
+
}
|
|
52
102
|
continue
|
|
53
103
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
104
|
+
|
|
105
|
+
if (arg.startsWith("-") && arg.length > 1) {
|
|
106
|
+
const short = arg.slice(1)
|
|
107
|
+
const key = shortAliases[short] || short
|
|
108
|
+
const next = argv[i + 1]
|
|
109
|
+
if (next && !next.startsWith("-")) {
|
|
110
|
+
flags[key] = next
|
|
111
|
+
i += 1
|
|
112
|
+
} else {
|
|
113
|
+
flags[key] = true
|
|
114
|
+
}
|
|
57
115
|
continue
|
|
58
116
|
}
|
|
117
|
+
|
|
118
|
+
positionals.push(arg)
|
|
59
119
|
}
|
|
60
120
|
|
|
61
|
-
return flags
|
|
121
|
+
return { flags, positionals }
|
|
62
122
|
}
|
|
63
123
|
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
|
|
124
|
+
function shortId(id) {
|
|
125
|
+
if (!id) return "-"
|
|
126
|
+
const value = String(id)
|
|
127
|
+
if (value.length <= 8) return value
|
|
128
|
+
return `${value.slice(0, 4)}…${value.slice(-4)}`
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function printJson(value) {
|
|
132
|
+
console.log(JSON.stringify(value, null, 2))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function printProjects(projects, currentProjectId) {
|
|
136
|
+
if (!Array.isArray(projects) || projects.length === 0) {
|
|
137
|
+
console.log("No projects")
|
|
138
|
+
return
|
|
67
139
|
}
|
|
68
140
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
141
|
+
projects.forEach((project, index) => {
|
|
142
|
+
const marker = String(project._id) === String(currentProjectId) ? "*" : " "
|
|
143
|
+
const workflow = project.workflow_type || "stages"
|
|
144
|
+
console.log(
|
|
145
|
+
`${String(index + 1).padStart(2)}.${marker} ${project.project_name} [${workflow}] ${shortId(project._id)}`
|
|
146
|
+
)
|
|
147
|
+
})
|
|
148
|
+
}
|
|
73
149
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
150
|
+
function printStages(stages, currentStageId) {
|
|
151
|
+
if (!Array.isArray(stages) || stages.length === 0) {
|
|
152
|
+
console.log("No stages")
|
|
153
|
+
return
|
|
77
154
|
}
|
|
155
|
+
|
|
156
|
+
stages.forEach((stage, index) => {
|
|
157
|
+
const marker = String(stage._id) === String(currentStageId) ? "*" : " "
|
|
158
|
+
const status = stage.status || "-"
|
|
159
|
+
console.log(
|
|
160
|
+
`${String(index + 1).padStart(2)}.${marker} ${stage.stage_name} [${status}] ${shortId(stage._id)}`
|
|
161
|
+
)
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function printTasks(tasks, withStage = false) {
|
|
166
|
+
if (!Array.isArray(tasks) || tasks.length === 0) {
|
|
167
|
+
console.log("No tasks")
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
tasks.forEach((task, index) => {
|
|
172
|
+
const done = task.is_done ? "x" : " "
|
|
173
|
+
const stagePrefix = withStage && task._stageName ? `(${task._stageName}) ` : ""
|
|
174
|
+
console.log(
|
|
175
|
+
`${String(index + 1).padStart(2)}. [${done}] ${stagePrefix}${task.title} ${shortId(task._id)}`
|
|
176
|
+
)
|
|
177
|
+
})
|
|
78
178
|
}
|
|
79
179
|
|
|
80
180
|
function printWelcomeBanner() {
|
|
@@ -82,180 +182,782 @@ function printWelcomeBanner() {
|
|
|
82
182
|
console.log(UNIT_LABS_BANNER)
|
|
83
183
|
}
|
|
84
184
|
|
|
85
|
-
|
|
86
|
-
|
|
185
|
+
function getErrorMessage(payload) {
|
|
186
|
+
if (typeof payload === "string") return payload
|
|
187
|
+
if (payload && typeof payload === "object" && payload.message) return payload.message
|
|
188
|
+
try {
|
|
189
|
+
return JSON.stringify(payload)
|
|
190
|
+
} catch (error) {
|
|
191
|
+
return "unknown error"
|
|
192
|
+
}
|
|
193
|
+
}
|
|
87
194
|
|
|
88
|
-
|
|
89
|
-
|
|
195
|
+
async function request({ apiBase, token, method, path, body, requiresAuth = true }) {
|
|
196
|
+
if (requiresAuth && !token) {
|
|
197
|
+
throw new Error("not logged in. run: unit-labs auth login")
|
|
90
198
|
}
|
|
91
199
|
|
|
200
|
+
const headers = {}
|
|
201
|
+
if (requiresAuth && token) {
|
|
202
|
+
headers.Authorization = `Bearer ${token}`
|
|
203
|
+
}
|
|
92
204
|
if (body) {
|
|
93
205
|
headers["Content-Type"] = "application/json"
|
|
94
206
|
}
|
|
95
207
|
|
|
96
|
-
const response = await fetch(
|
|
208
|
+
const response = await fetch(`${apiBase}${path}`, {
|
|
97
209
|
method,
|
|
98
210
|
headers,
|
|
99
211
|
body: body ? JSON.stringify(body) : undefined,
|
|
100
212
|
})
|
|
101
213
|
|
|
102
|
-
const
|
|
103
|
-
let payload =
|
|
214
|
+
const rawText = await response.text()
|
|
215
|
+
let payload = rawText
|
|
104
216
|
|
|
105
217
|
try {
|
|
106
|
-
payload =
|
|
218
|
+
payload = rawText ? JSON.parse(rawText) : null
|
|
107
219
|
} catch (error) {
|
|
108
|
-
payload =
|
|
220
|
+
payload = rawText
|
|
109
221
|
}
|
|
110
222
|
|
|
111
223
|
if (!response.ok) {
|
|
112
|
-
|
|
113
|
-
? payload
|
|
114
|
-
: payload?.message || JSON.stringify(payload)
|
|
115
|
-
throw new Error(`${response.status} ${message}`)
|
|
224
|
+
throw new Error(`${response.status} ${getErrorMessage(payload)}`)
|
|
116
225
|
}
|
|
117
226
|
|
|
118
227
|
return payload
|
|
119
228
|
}
|
|
120
229
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
230
|
+
function resolveByRef(items, ref, nameSelector, label) {
|
|
231
|
+
if (!ref) {
|
|
232
|
+
throw new Error(`${label} reference is required`)
|
|
233
|
+
}
|
|
125
234
|
|
|
126
|
-
if (!
|
|
127
|
-
throw new Error(
|
|
235
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
236
|
+
throw new Error(`no ${label}s found`)
|
|
128
237
|
}
|
|
129
238
|
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
email: credentials.login,
|
|
135
|
-
password: credentials.password,
|
|
136
|
-
},
|
|
137
|
-
})
|
|
239
|
+
const raw = String(ref).trim()
|
|
240
|
+
if (!raw) {
|
|
241
|
+
throw new Error(`${label} reference is required`)
|
|
242
|
+
}
|
|
138
243
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
244
|
+
if (/^\d+$/.test(raw)) {
|
|
245
|
+
const index = Number(raw) - 1
|
|
246
|
+
if (index < 0 || index >= items.length) {
|
|
247
|
+
throw new Error(`${label} index out of range: ${raw}`)
|
|
248
|
+
}
|
|
249
|
+
return items[index]
|
|
142
250
|
}
|
|
143
251
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
252
|
+
const exactId = items.find(item => String(item._id) === raw)
|
|
253
|
+
if (exactId) {
|
|
254
|
+
return exactId
|
|
255
|
+
}
|
|
148
256
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
257
|
+
const lower = raw.toLowerCase()
|
|
258
|
+
const exactNameMatches = items.filter(item => String(nameSelector(item)).toLowerCase() === lower)
|
|
259
|
+
if (exactNameMatches.length === 1) {
|
|
260
|
+
return exactNameMatches[0]
|
|
261
|
+
}
|
|
262
|
+
if (exactNameMatches.length > 1) {
|
|
263
|
+
throw new Error(`ambiguous ${label} name "${raw}"`)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const partialMatches = items.filter(item => String(nameSelector(item)).toLowerCase().includes(lower))
|
|
267
|
+
if (partialMatches.length === 1) {
|
|
268
|
+
return partialMatches[0]
|
|
269
|
+
}
|
|
270
|
+
if (partialMatches.length > 1) {
|
|
271
|
+
throw new Error(`ambiguous ${label} reference "${raw}"`)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
throw new Error(`${label} not found: ${raw}`)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function fetchProjects(config, teamId = null) {
|
|
278
|
+
if (teamId) {
|
|
279
|
+
const payload = await request({
|
|
280
|
+
apiBase: normalizeApiBase(config.apiBase),
|
|
281
|
+
token: config.token,
|
|
152
282
|
method: "GET",
|
|
153
|
-
|
|
154
|
-
token,
|
|
283
|
+
path: `/teams/${teamId}/projects`,
|
|
155
284
|
})
|
|
156
|
-
|
|
157
|
-
|
|
285
|
+
|
|
286
|
+
return payload?.data?.projects || []
|
|
158
287
|
}
|
|
159
288
|
|
|
160
|
-
|
|
289
|
+
const payload = await request({
|
|
290
|
+
apiBase: normalizeApiBase(config.apiBase),
|
|
291
|
+
token: config.token,
|
|
292
|
+
method: "GET",
|
|
293
|
+
path: "/projects",
|
|
294
|
+
})
|
|
161
295
|
|
|
162
|
-
|
|
163
|
-
console.log(`Logged in as ${me.email}`)
|
|
164
|
-
} else {
|
|
165
|
-
console.log("Logged in")
|
|
166
|
-
}
|
|
167
|
-
console.log(`Config saved: ${CONFIG_FILE}`)
|
|
296
|
+
return payload?.projects || []
|
|
168
297
|
}
|
|
169
298
|
|
|
170
|
-
function
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
299
|
+
async function fetchProjectById(config, projectId) {
|
|
300
|
+
return request({
|
|
301
|
+
apiBase: normalizeApiBase(config.apiBase),
|
|
302
|
+
token: config.token,
|
|
303
|
+
method: "GET",
|
|
304
|
+
path: `/projects/${projectId}`,
|
|
175
305
|
})
|
|
176
|
-
console.log("Logged out")
|
|
177
306
|
}
|
|
178
307
|
|
|
179
|
-
async function
|
|
180
|
-
const
|
|
181
|
-
|
|
308
|
+
async function fetchStages(config, projectId) {
|
|
309
|
+
const payload = await request({
|
|
310
|
+
apiBase: normalizeApiBase(config.apiBase),
|
|
311
|
+
token: config.token,
|
|
312
|
+
method: "GET",
|
|
313
|
+
path: `/projects/${projectId}/stages`,
|
|
314
|
+
})
|
|
182
315
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
316
|
+
return Array.isArray(payload) ? payload : []
|
|
317
|
+
}
|
|
186
318
|
|
|
187
|
-
|
|
319
|
+
async function fetchFlatTasks(config, projectId) {
|
|
320
|
+
const payload = await request({
|
|
321
|
+
apiBase: normalizeApiBase(config.apiBase),
|
|
322
|
+
token: config.token,
|
|
188
323
|
method: "GET",
|
|
189
|
-
|
|
190
|
-
token,
|
|
324
|
+
path: `/projects/${projectId}/tasks`,
|
|
191
325
|
})
|
|
192
326
|
|
|
193
|
-
|
|
327
|
+
return Array.isArray(payload) ? payload : []
|
|
194
328
|
}
|
|
195
329
|
|
|
196
|
-
function
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
330
|
+
async function fetchStageTasks(config, projectId, stageId) {
|
|
331
|
+
const payload = await request({
|
|
332
|
+
apiBase: normalizeApiBase(config.apiBase),
|
|
333
|
+
token: config.token,
|
|
334
|
+
method: "GET",
|
|
335
|
+
path: `/projects/${projectId}/stages/${stageId}/tasks`,
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
return Array.isArray(payload) ? payload : []
|
|
202
339
|
}
|
|
203
340
|
|
|
204
|
-
function
|
|
205
|
-
|
|
206
|
-
|
|
341
|
+
async function resolveProjectContext(config, projectRef, requireProject = true) {
|
|
342
|
+
const projects = await fetchProjects(config)
|
|
343
|
+
let project = null
|
|
344
|
+
|
|
345
|
+
if (projectRef) {
|
|
346
|
+
project = resolveByRef(projects, projectRef, p => p.project_name, "project")
|
|
347
|
+
} else if (config.currentProjectId) {
|
|
348
|
+
project = projects.find(p => String(p._id) === String(config.currentProjectId)) || null
|
|
207
349
|
}
|
|
208
350
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
})
|
|
215
|
-
console.log(`API saved: ${apiBase}`)
|
|
351
|
+
if (!project && requireProject) {
|
|
352
|
+
throw new Error("no project selected. run: unit-labs project ls && unit-labs project use <number|name>")
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return { project, projects }
|
|
216
356
|
}
|
|
217
357
|
|
|
218
|
-
async function
|
|
219
|
-
if (
|
|
220
|
-
|
|
358
|
+
async function resolveStageContext(config, project, stageRef, requireStage = true) {
|
|
359
|
+
if (!project) {
|
|
360
|
+
throw new Error("project is required")
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if ((project.workflow_type || "stages") === "flat") {
|
|
364
|
+
throw new Error("this project uses flat workflow. stage commands are unavailable")
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const stages = await fetchStages(config, project._id)
|
|
368
|
+
let stage = null
|
|
369
|
+
|
|
370
|
+
if (stageRef) {
|
|
371
|
+
stage = resolveByRef(stages, stageRef, s => s.stage_name, "stage")
|
|
372
|
+
} else if (config.currentStageId) {
|
|
373
|
+
stage = stages.find(s => String(s._id) === String(config.currentStageId)) || null
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (!stage && requireStage) {
|
|
377
|
+
throw new Error("no stage selected. run: unit-labs stage ls && unit-labs stage use <number|name>")
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return { stage, stages }
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function saveConfigPatch(config, patch) {
|
|
384
|
+
const next = {
|
|
385
|
+
...config,
|
|
386
|
+
...patch,
|
|
387
|
+
}
|
|
388
|
+
writeConfig(next)
|
|
389
|
+
return next
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function promptMissingCredentials({ login, password }) {
|
|
393
|
+
if (login && password) {
|
|
394
|
+
return { login, password }
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const rl = readline.createInterface({ input, output })
|
|
398
|
+
const nextLogin = login || (await rl.question("Login: ")).trim()
|
|
399
|
+
const nextPassword = password || (await rl.question("Password: ")).trim()
|
|
400
|
+
rl.close()
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
login: nextLogin,
|
|
404
|
+
password: nextPassword,
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function runAuth(command, args, config) {
|
|
409
|
+
const { flags } = parseArgs(args, { l: "login", e: "login", p: "password", h: "help" })
|
|
410
|
+
const apiBase = normalizeApiBase(flags.api || config.apiBase || DEFAULT_API_BASE)
|
|
411
|
+
|
|
412
|
+
if (!command || command === "--help" || command === "-h" || flags.help) {
|
|
413
|
+
console.log(AUTH_HELP.trim())
|
|
221
414
|
return
|
|
222
415
|
}
|
|
223
416
|
|
|
224
|
-
|
|
417
|
+
if (command === "login") {
|
|
418
|
+
const credentials = await promptMissingCredentials(flags)
|
|
419
|
+
if (!credentials.login || !credentials.password) {
|
|
420
|
+
throw new Error("login and password are required")
|
|
421
|
+
}
|
|
225
422
|
|
|
226
|
-
|
|
227
|
-
|
|
423
|
+
const signinPayload = await request({
|
|
424
|
+
apiBase,
|
|
425
|
+
method: "POST",
|
|
426
|
+
path: "/signin",
|
|
427
|
+
body: { email: credentials.login, password: credentials.password },
|
|
428
|
+
requiresAuth: false,
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
const token = signinPayload?.token
|
|
432
|
+
if (!token) {
|
|
433
|
+
throw new Error("signin response does not contain token")
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
let me = null
|
|
437
|
+
try {
|
|
438
|
+
me = await request({
|
|
439
|
+
apiBase,
|
|
440
|
+
token,
|
|
441
|
+
method: "GET",
|
|
442
|
+
path: "/me",
|
|
443
|
+
})
|
|
444
|
+
} catch (error) {
|
|
445
|
+
me = null
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
saveConfigPatch(config, {
|
|
449
|
+
apiBase,
|
|
450
|
+
token,
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
printWelcomeBanner()
|
|
454
|
+
if (me?.email) {
|
|
455
|
+
console.log(`Logged in as ${me.email}`)
|
|
456
|
+
} else {
|
|
457
|
+
console.log("Logged in")
|
|
458
|
+
}
|
|
459
|
+
console.log(`Config saved: ${CONFIG_FILE}`)
|
|
460
|
+
return
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (command === "logout") {
|
|
464
|
+
saveConfigPatch(config, { token: null })
|
|
465
|
+
console.log("Logged out")
|
|
466
|
+
return
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (command === "whoami") {
|
|
470
|
+
const me = await request({
|
|
471
|
+
apiBase,
|
|
472
|
+
token: config.token,
|
|
473
|
+
method: "GET",
|
|
474
|
+
path: "/me",
|
|
475
|
+
})
|
|
476
|
+
printJson(me)
|
|
477
|
+
return
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
throw new Error(`unknown auth command: ${command}`)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function runConfig(command, args, config) {
|
|
484
|
+
const { flags, positionals } = parseArgs(args, { h: "help" })
|
|
485
|
+
|
|
486
|
+
if (!command || command === "--help" || command === "-h" || flags.help) {
|
|
487
|
+
console.log(CONFIG_HELP.trim())
|
|
488
|
+
return
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (command === "show") {
|
|
492
|
+
printJson({
|
|
493
|
+
apiBase: normalizeApiBase(config.apiBase || DEFAULT_API_BASE),
|
|
494
|
+
loggedIn: Boolean(config.token),
|
|
495
|
+
currentProjectId: config.currentProjectId,
|
|
496
|
+
currentProjectName: config.currentProjectName,
|
|
497
|
+
currentProjectWorkflow: config.currentProjectWorkflow,
|
|
498
|
+
currentStageId: config.currentStageId,
|
|
499
|
+
currentStageName: config.currentStageName,
|
|
500
|
+
})
|
|
501
|
+
return
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (command === "set-api") {
|
|
505
|
+
const rawApi = positionals[0] || flags.api
|
|
506
|
+
if (!rawApi) {
|
|
507
|
+
throw new Error("api url is required. usage: unit-labs config set-api <url>")
|
|
508
|
+
}
|
|
509
|
+
const apiBase = normalizeApiBase(rawApi)
|
|
510
|
+
saveConfigPatch(config, { apiBase })
|
|
511
|
+
console.log(`API saved: ${apiBase}`)
|
|
512
|
+
return
|
|
513
|
+
}
|
|
228
514
|
|
|
229
|
-
|
|
230
|
-
|
|
515
|
+
throw new Error(`unknown config command: ${command}`)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async function runProject(command, args, config) {
|
|
519
|
+
const { flags, positionals } = parseArgs(args, { t: "team", w: "workflow", n: "name", j: "json", h: "help" })
|
|
520
|
+
|
|
521
|
+
if (!command || command === "--help" || command === "-h" || flags.help) {
|
|
522
|
+
console.log(PROJECT_HELP.trim())
|
|
523
|
+
return
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (command === "ls") {
|
|
527
|
+
const projects = await fetchProjects(config, flags.team || null)
|
|
528
|
+
|
|
529
|
+
if (flags.json) {
|
|
530
|
+
printJson(projects)
|
|
231
531
|
return
|
|
232
532
|
}
|
|
233
533
|
|
|
234
|
-
|
|
235
|
-
|
|
534
|
+
printProjects(projects, config.currentProjectId)
|
|
535
|
+
return
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (command === "new") {
|
|
539
|
+
const name = (flags.name || positionals.join(" ")).trim()
|
|
540
|
+
if (!name) {
|
|
541
|
+
throw new Error("project name is required. use: --name \"...\"")
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const workflow = (flags.workflow || "stages").trim()
|
|
545
|
+
if (!["stages", "flat"].includes(workflow)) {
|
|
546
|
+
throw new Error("workflow must be one of: stages, flat")
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const body = {
|
|
550
|
+
project_name: name,
|
|
551
|
+
workflow_type: workflow,
|
|
552
|
+
}
|
|
553
|
+
if (flags.team) {
|
|
554
|
+
body.team_id = flags.team
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const payload = await request({
|
|
558
|
+
apiBase: normalizeApiBase(config.apiBase),
|
|
559
|
+
token: config.token,
|
|
560
|
+
method: "POST",
|
|
561
|
+
path: "/projects",
|
|
562
|
+
body,
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
if (typeof payload === "string") {
|
|
566
|
+
console.log(payload)
|
|
567
|
+
} else {
|
|
568
|
+
printJson(payload)
|
|
569
|
+
}
|
|
570
|
+
return
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (command === "view") {
|
|
574
|
+
const ref = positionals[0]
|
|
575
|
+
const { project } = await resolveProjectContext(config, ref, true)
|
|
576
|
+
|
|
577
|
+
let data = project
|
|
578
|
+
try {
|
|
579
|
+
data = await fetchProjectById(config, project._id)
|
|
580
|
+
} catch (error) {
|
|
581
|
+
data = project
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (flags.json) {
|
|
585
|
+
printJson(data)
|
|
236
586
|
return
|
|
237
587
|
}
|
|
238
588
|
|
|
239
|
-
|
|
240
|
-
|
|
589
|
+
console.log(`Name: ${data.project_name}`)
|
|
590
|
+
console.log(`Workflow: ${data.workflow_type || "stages"}`)
|
|
591
|
+
console.log(`Status: ${data.status || "-"}`)
|
|
592
|
+
console.log(`ID: ${data._id}`)
|
|
593
|
+
return
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (command === "use") {
|
|
597
|
+
const ref = positionals[0]
|
|
598
|
+
if (!ref) {
|
|
599
|
+
throw new Error("project reference is required. use: unit-labs project use <number|name|id>")
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const { project } = await resolveProjectContext(config, ref, true)
|
|
603
|
+
const workflow = project.workflow_type || "stages"
|
|
604
|
+
saveConfigPatch(config, {
|
|
605
|
+
currentProjectId: project._id,
|
|
606
|
+
currentProjectName: project.project_name,
|
|
607
|
+
currentProjectWorkflow: workflow,
|
|
608
|
+
currentStageId: null,
|
|
609
|
+
currentStageName: null,
|
|
610
|
+
})
|
|
611
|
+
console.log(`Current project: ${project.project_name} [${workflow}]`)
|
|
612
|
+
return
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (command === "current") {
|
|
616
|
+
if (!config.currentProjectId) {
|
|
617
|
+
throw new Error("no current project. use: unit-labs project use <number|name>")
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const { project } = await resolveProjectContext(config, null, true)
|
|
621
|
+
|
|
622
|
+
if (flags.json) {
|
|
623
|
+
printJson(project)
|
|
241
624
|
return
|
|
242
625
|
}
|
|
243
626
|
|
|
244
|
-
|
|
627
|
+
console.log(`Current project: ${project.project_name}`)
|
|
628
|
+
console.log(`Workflow: ${project.workflow_type || "stages"}`)
|
|
629
|
+
console.log(`ID: ${project._id}`)
|
|
630
|
+
return
|
|
245
631
|
}
|
|
246
632
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
633
|
+
throw new Error(`unknown project command: ${command}`)
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
async function runStage(command, args, config) {
|
|
637
|
+
const { flags, positionals } = parseArgs(args, {
|
|
638
|
+
p: "project",
|
|
639
|
+
n: "name",
|
|
640
|
+
d: "description",
|
|
641
|
+
j: "json",
|
|
642
|
+
h: "help",
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
if (!command || command === "--help" || command === "-h" || flags.help) {
|
|
646
|
+
console.log(STAGE_HELP.trim())
|
|
647
|
+
return
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const projectRef = flags.project || null
|
|
651
|
+
const { project } = await resolveProjectContext(config, projectRef, true)
|
|
652
|
+
if ((project.workflow_type || "stages") === "flat") {
|
|
653
|
+
throw new Error("this project uses flat workflow. stage commands are unavailable")
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (command === "ls") {
|
|
657
|
+
const stages = await fetchStages(config, project._id)
|
|
658
|
+
if (flags.json) {
|
|
659
|
+
printJson(stages)
|
|
250
660
|
return
|
|
251
661
|
}
|
|
662
|
+
printStages(stages, config.currentStageId)
|
|
663
|
+
return
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (command === "new") {
|
|
667
|
+
const stageName = (flags.name || "").trim()
|
|
668
|
+
const description = (flags.description || "").trim()
|
|
669
|
+
|
|
670
|
+
if (!stageName || !description) {
|
|
671
|
+
throw new Error("stage name and description are required. use: --name \"...\" --description \"...\"")
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const payload = await request({
|
|
675
|
+
apiBase: normalizeApiBase(config.apiBase),
|
|
676
|
+
token: config.token,
|
|
677
|
+
method: "POST",
|
|
678
|
+
path: `/projects/${project._id}/stages`,
|
|
679
|
+
body: {
|
|
680
|
+
stage_name: stageName,
|
|
681
|
+
description,
|
|
682
|
+
},
|
|
683
|
+
})
|
|
252
684
|
|
|
253
|
-
if (
|
|
254
|
-
|
|
685
|
+
if (typeof payload === "string") {
|
|
686
|
+
console.log(payload)
|
|
687
|
+
} else {
|
|
688
|
+
printJson(payload)
|
|
689
|
+
}
|
|
690
|
+
return
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (command === "use") {
|
|
694
|
+
const ref = positionals[0]
|
|
695
|
+
if (!ref) {
|
|
696
|
+
throw new Error("stage reference is required. use: unit-labs stage use <number|name|id>")
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const { stage } = await resolveStageContext(config, project, ref, true)
|
|
700
|
+
saveConfigPatch(config, {
|
|
701
|
+
currentProjectId: project._id,
|
|
702
|
+
currentProjectName: project.project_name,
|
|
703
|
+
currentProjectWorkflow: project.workflow_type || "stages",
|
|
704
|
+
currentStageId: stage._id,
|
|
705
|
+
currentStageName: stage.stage_name,
|
|
706
|
+
})
|
|
707
|
+
console.log(`Current stage: ${stage.stage_name}`)
|
|
708
|
+
return
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (command === "current") {
|
|
712
|
+
if (!config.currentStageId) {
|
|
713
|
+
throw new Error("no current stage. use: unit-labs stage use <number|name>")
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const { stage } = await resolveStageContext(config, project, null, true)
|
|
717
|
+
if (flags.json) {
|
|
718
|
+
printJson(stage)
|
|
255
719
|
return
|
|
256
720
|
}
|
|
721
|
+
console.log(`Current stage: ${stage.stage_name}`)
|
|
722
|
+
console.log(`Status: ${stage.status || "-"}`)
|
|
723
|
+
console.log(`ID: ${stage._id}`)
|
|
724
|
+
return
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
throw new Error(`unknown stage command: ${command}`)
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
async function resolveTaskContext(config, flags, requireStage = true) {
|
|
731
|
+
const projectRef = flags.project || null
|
|
732
|
+
const { project } = await resolveProjectContext(config, projectRef, true)
|
|
733
|
+
const workflow = project.workflow_type || "stages"
|
|
257
734
|
|
|
258
|
-
|
|
735
|
+
if (workflow === "flat") {
|
|
736
|
+
return { project, workflow, stage: null }
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const stageRef = flags.stage || null
|
|
740
|
+
const { stage } = await resolveStageContext(config, project, stageRef, requireStage)
|
|
741
|
+
return { project, workflow, stage }
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
async function listAllStageTasks(config, project) {
|
|
745
|
+
const stages = await fetchStages(config, project._id)
|
|
746
|
+
const result = []
|
|
747
|
+
|
|
748
|
+
for (const stage of stages) {
|
|
749
|
+
const tasks = await fetchStageTasks(config, project._id, stage._id)
|
|
750
|
+
tasks.forEach(task => {
|
|
751
|
+
result.push({
|
|
752
|
+
...task,
|
|
753
|
+
_stageName: stage.stage_name,
|
|
754
|
+
_stageId: stage._id,
|
|
755
|
+
})
|
|
756
|
+
})
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
return result
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
async function runTask(command, args, config) {
|
|
763
|
+
const { flags, positionals } = parseArgs(args, {
|
|
764
|
+
p: "project",
|
|
765
|
+
s: "stage",
|
|
766
|
+
t: "title",
|
|
767
|
+
a: "all",
|
|
768
|
+
j: "json",
|
|
769
|
+
h: "help",
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
if (!command || command === "--help" || command === "-h" || flags.help) {
|
|
773
|
+
console.log(TASK_HELP.trim())
|
|
774
|
+
return
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
if (command === "ls") {
|
|
778
|
+
const { project, workflow, stage } = await resolveTaskContext(config, flags, !flags.all)
|
|
779
|
+
|
|
780
|
+
if (workflow === "flat") {
|
|
781
|
+
const tasks = await fetchFlatTasks(config, project._id)
|
|
782
|
+
if (flags.json) {
|
|
783
|
+
printJson(tasks)
|
|
784
|
+
return
|
|
785
|
+
}
|
|
786
|
+
printTasks(tasks, false)
|
|
787
|
+
return
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (flags.all) {
|
|
791
|
+
const tasks = await listAllStageTasks(config, project)
|
|
792
|
+
if (flags.json) {
|
|
793
|
+
printJson(tasks)
|
|
794
|
+
return
|
|
795
|
+
}
|
|
796
|
+
printTasks(tasks, true)
|
|
797
|
+
return
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const tasks = await fetchStageTasks(config, project._id, stage._id)
|
|
801
|
+
if (flags.json) {
|
|
802
|
+
printJson(tasks)
|
|
803
|
+
return
|
|
804
|
+
}
|
|
805
|
+
printTasks(tasks, false)
|
|
806
|
+
return
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (command === "new") {
|
|
810
|
+
const title = (flags.title || positionals.join(" ")).trim()
|
|
811
|
+
if (!title) {
|
|
812
|
+
throw new Error("task title is required. use: unit-labs task new \"Task title\"")
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const { project, workflow, stage } = await resolveTaskContext(config, flags, true)
|
|
816
|
+
|
|
817
|
+
const path = workflow === "flat"
|
|
818
|
+
? `/projects/${project._id}/tasks`
|
|
819
|
+
: `/projects/${project._id}/stages/${stage._id}/tasks`
|
|
820
|
+
|
|
821
|
+
const payload = await request({
|
|
822
|
+
apiBase: normalizeApiBase(config.apiBase),
|
|
823
|
+
token: config.token,
|
|
824
|
+
method: "POST",
|
|
825
|
+
path,
|
|
826
|
+
body: { title },
|
|
827
|
+
})
|
|
828
|
+
|
|
829
|
+
if (Array.isArray(payload)) {
|
|
830
|
+
printTasks(payload, false)
|
|
831
|
+
} else if (typeof payload === "string") {
|
|
832
|
+
console.log(payload)
|
|
833
|
+
} else {
|
|
834
|
+
printJson(payload)
|
|
835
|
+
}
|
|
836
|
+
return
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
if (command === "done" || command === "close") {
|
|
840
|
+
const taskRef = positionals[0]
|
|
841
|
+
if (!taskRef) {
|
|
842
|
+
throw new Error("task reference is required. use: unit-labs task done <number|title|id>")
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const { project, workflow, stage } = await resolveTaskContext(config, flags, true)
|
|
846
|
+
const tasks = workflow === "flat"
|
|
847
|
+
? await fetchFlatTasks(config, project._id)
|
|
848
|
+
: await fetchStageTasks(config, project._id, stage._id)
|
|
849
|
+
const task = resolveByRef(tasks, taskRef, t => t.title, "task")
|
|
850
|
+
|
|
851
|
+
const path = workflow === "flat"
|
|
852
|
+
? `/projects/${project._id}/tasks/${task._id}/toggle`
|
|
853
|
+
: `/projects/${project._id}/stages/${stage._id}/tasks/${task._id}/toggle`
|
|
854
|
+
|
|
855
|
+
const updated = await request({
|
|
856
|
+
apiBase: normalizeApiBase(config.apiBase),
|
|
857
|
+
token: config.token,
|
|
858
|
+
method: "PATCH",
|
|
859
|
+
path,
|
|
860
|
+
})
|
|
861
|
+
|
|
862
|
+
printTasks([updated], false)
|
|
863
|
+
return
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (command === "edit") {
|
|
867
|
+
const taskRef = positionals[0]
|
|
868
|
+
const title = (flags.title || positionals.slice(1).join(" ")).trim()
|
|
869
|
+
|
|
870
|
+
if (!taskRef) {
|
|
871
|
+
throw new Error("task reference is required. use: unit-labs task edit <number|title|id> --title \"...\"")
|
|
872
|
+
}
|
|
873
|
+
if (!title) {
|
|
874
|
+
throw new Error("new title is required. use: --title \"New title\"")
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const { project, workflow, stage } = await resolveTaskContext(config, flags, true)
|
|
878
|
+
const tasks = workflow === "flat"
|
|
879
|
+
? await fetchFlatTasks(config, project._id)
|
|
880
|
+
: await fetchStageTasks(config, project._id, stage._id)
|
|
881
|
+
const task = resolveByRef(tasks, taskRef, t => t.title, "task")
|
|
882
|
+
|
|
883
|
+
const path = workflow === "flat"
|
|
884
|
+
? `/projects/${project._id}/tasks/${task._id}/title`
|
|
885
|
+
: `/projects/${project._id}/stages/${stage._id}/tasks/${task._id}/title`
|
|
886
|
+
|
|
887
|
+
const updated = await request({
|
|
888
|
+
apiBase: normalizeApiBase(config.apiBase),
|
|
889
|
+
token: config.token,
|
|
890
|
+
method: "PATCH",
|
|
891
|
+
path,
|
|
892
|
+
body: { title },
|
|
893
|
+
})
|
|
894
|
+
|
|
895
|
+
printTasks([updated], false)
|
|
896
|
+
return
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
if (command === "rm" || command === "delete") {
|
|
900
|
+
const taskRef = positionals[0]
|
|
901
|
+
if (!taskRef) {
|
|
902
|
+
throw new Error("task reference is required. use: unit-labs task rm <number|title|id>")
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const { project, workflow, stage } = await resolveTaskContext(config, flags, true)
|
|
906
|
+
const tasks = workflow === "flat"
|
|
907
|
+
? await fetchFlatTasks(config, project._id)
|
|
908
|
+
: await fetchStageTasks(config, project._id, stage._id)
|
|
909
|
+
const task = resolveByRef(tasks, taskRef, t => t.title, "task")
|
|
910
|
+
|
|
911
|
+
const path = workflow === "flat"
|
|
912
|
+
? `/projects/${project._id}/tasks/${task._id}`
|
|
913
|
+
: `/projects/${project._id}/stages/${stage._id}/tasks/${task._id}`
|
|
914
|
+
|
|
915
|
+
await request({
|
|
916
|
+
apiBase: normalizeApiBase(config.apiBase),
|
|
917
|
+
token: config.token,
|
|
918
|
+
method: "DELETE",
|
|
919
|
+
path,
|
|
920
|
+
})
|
|
921
|
+
|
|
922
|
+
console.log(`Deleted task: ${task.title}`)
|
|
923
|
+
return
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
throw new Error(`unknown task command: ${command}`)
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
async function run(argv) {
|
|
930
|
+
if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
|
|
931
|
+
console.log(ROOT_HELP.trim())
|
|
932
|
+
return
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const [scope, command, ...rest] = argv
|
|
936
|
+
const config = readConfig()
|
|
937
|
+
|
|
938
|
+
if (scope === "auth") {
|
|
939
|
+
await runAuth(command, rest, config)
|
|
940
|
+
return
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
if (scope === "config") {
|
|
944
|
+
await runConfig(command, rest, config)
|
|
945
|
+
return
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if (scope === "project") {
|
|
949
|
+
await runProject(command, rest, config)
|
|
950
|
+
return
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
if (scope === "stage") {
|
|
954
|
+
await runStage(command, rest, config)
|
|
955
|
+
return
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
if (scope === "task") {
|
|
959
|
+
await runTask(command, rest, config)
|
|
960
|
+
return
|
|
259
961
|
}
|
|
260
962
|
|
|
261
963
|
throw new Error(`unknown scope: ${scope}`)
|
|
@@ -264,4 +966,3 @@ async function run(argv) {
|
|
|
264
966
|
module.exports = {
|
|
265
967
|
run,
|
|
266
968
|
}
|
|
267
|
-
|