@klevar/portal-cli 0.1.2 → 0.1.4
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
CHANGED
|
@@ -43,6 +43,54 @@ npx @klevar/portal-cli metrics push --external_ref "idealo:ksh.de" --source_ref
|
|
|
43
43
|
npx @klevar/portal-cli portal me --portal-token "<client-token>"
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
## Capacity And Usage
|
|
47
|
+
|
|
48
|
+
Capacity commands manage included allowances for a project or client. They are not timesheets.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npx @klevar/portal-cli capacity list <projectId>
|
|
52
|
+
npx @klevar/portal-cli capacity create <projectId> --name "Monthly Support" --laneKey support --unitType hours --periodType monthly --startDate 2026-06-01 --capacityAmount 12 --resetBehavior no_rollover --visibility client_visible_summary
|
|
53
|
+
npx @klevar/portal-cli capacity update <budgetId> --capacityAmount 20
|
|
54
|
+
npx @klevar/portal-cli capacity summary <projectId> --period 2026-06
|
|
55
|
+
npx @klevar/portal-cli capacity budget-summary <budgetId> --period 2026-06
|
|
56
|
+
npx @klevar/portal-cli capacity report <projectId> --period 2026-06
|
|
57
|
+
npx @klevar/portal-cli capacity pause <budgetId>
|
|
58
|
+
npx @klevar/portal-cli capacity resume <budgetId>
|
|
59
|
+
npx @klevar/portal-cli capacity end <budgetId>
|
|
60
|
+
npx @klevar/portal-cli capacity delete <budgetId>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Usage commands record consumed capacity.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx @klevar/portal-cli usage record <taskId> --budgetId <budgetId> --units 1.5 --usageDate 2026-06-03 --title "Maintenance support"
|
|
67
|
+
npx @klevar/portal-cli usage manual <projectId> --budgetId <budgetId> --units 1 --usageDate 2026-06-03 --title "Advisory call"
|
|
68
|
+
npx @klevar/portal-cli usage update <usageId> --units 2 --notes "Adjusted after review"
|
|
69
|
+
npx @klevar/portal-cli usage approve <usageId>
|
|
70
|
+
npx @klevar/portal-cli usage reject <usageId>
|
|
71
|
+
npx @klevar/portal-cli usage delete <usageId>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Current smoke-tested `unitType` values are `hours` and `credits`.
|
|
75
|
+
|
|
76
|
+
Supported `periodType` values:
|
|
77
|
+
|
|
78
|
+
- `monthly`: period keys use `YYYY-MM`.
|
|
79
|
+
- `weekly`: ISO week period keys use `YYYY-Www`, for example `2026-W23`.
|
|
80
|
+
|
|
81
|
+
Supported `visibility` values:
|
|
82
|
+
|
|
83
|
+
- `internal_only`: hidden from client portal capacity APIs.
|
|
84
|
+
- `client_visible_summary`: client can see summary totals only.
|
|
85
|
+
- `client_visible_detail`: client can see summary totals and visible usage entries.
|
|
86
|
+
|
|
87
|
+
Validation failures print API field details when the server returns them, for example:
|
|
88
|
+
|
|
89
|
+
```text
|
|
90
|
+
Error 400: Validation failed
|
|
91
|
+
periodType: Invalid option: expected one of "monthly"|"weekly"|...
|
|
92
|
+
```
|
|
93
|
+
|
|
46
94
|
## Brain Usage
|
|
47
95
|
|
|
48
96
|
Brain usage should prefer the published `npx @klevar/portal-cli` entrypoint.
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
try {
|
|
3
|
+
// @ts-expect-error legacy JavaScript execution engine is copied during post-build.
|
|
4
|
+
await import('../lib/legacy-runner.js');
|
|
5
|
+
}
|
|
6
|
+
catch (err) {
|
|
7
|
+
if (err &&
|
|
8
|
+
typeof err === 'object' &&
|
|
9
|
+
'message' in err &&
|
|
10
|
+
typeof err.message === 'string' &&
|
|
11
|
+
err.message.startsWith('CLI_EXIT_')) {
|
|
12
|
+
process.exitCode =
|
|
13
|
+
'code' in err && typeof err.code === 'number'
|
|
14
|
+
? err.code
|
|
15
|
+
: Number(err.message.replace('CLI_EXIT_', '')) || 1;
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
throw err;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
4
21
|
export {};
|
|
5
22
|
//# sourceMappingURL=klevar-portal.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"klevar-portal.js","sourceRoot":"","sources":["../../bin/klevar-portal.ts"],"names":[],"mappings":";AAEA,mFAAmF;
|
|
1
|
+
{"version":3,"file":"klevar-portal.js","sourceRoot":"","sources":["../../bin/klevar-portal.ts"],"names":[],"mappings":";AAEA,IAAI,CAAC;IACH,mFAAmF;IACnF,MAAM,MAAM,CAAC,yBAAyB,CAAC,CAAC;AAC1C,CAAC;AAAC,OAAO,GAAG,EAAE,CAAC;IACb,IACE,GAAG;QACH,OAAO,GAAG,KAAK,QAAQ;QACvB,SAAS,IAAI,GAAG;QAChB,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ;QAC/B,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,EACnC,CAAC;QACD,OAAO,CAAC,QAAQ;YACd,MAAM,IAAI,GAAG,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ;gBAC3C,CAAC,CAAC,GAAG,CAAC,IAAI;gBACV,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;IAC1D,CAAC;SAAM,CAAC;QACN,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC"}
|
|
@@ -7,8 +7,8 @@ const budgetBody = [
|
|
|
7
7
|
export const capacityCommands = {
|
|
8
8
|
'capacity.list': { method: 'GET', path: '/api/admin/projects/:projectId/capacity-budgets', auth: 'apiKey', description: 'List project capacity budgets' },
|
|
9
9
|
'capacity.get': { method: 'GET', path: '/api/admin/capacity-budgets/:id', auth: 'apiKey', description: 'Get a capacity budget' },
|
|
10
|
-
'capacity.create': { method: 'POST', path: '/api/admin/projects/:projectId/capacity-budgets', auth: 'apiKey', description: 'Create a capacity budget', body: budgetBody },
|
|
11
|
-
'capacity.update': { method: 'PATCH', path: '/api/admin/capacity-budgets/:id', auth: 'apiKey', description: 'Update a capacity budget', body: budgetBody },
|
|
10
|
+
'capacity.create': { method: 'POST', path: '/api/admin/projects/:projectId/capacity-budgets', auth: 'apiKey', description: 'Create a capacity budget; periodType monthly|weekly, unitType hours|credits, visibility internal_only|client_visible_summary|client_visible_detail', body: budgetBody },
|
|
11
|
+
'capacity.update': { method: 'PATCH', path: '/api/admin/capacity-budgets/:id', auth: 'apiKey', description: 'Update a capacity budget; periodType monthly|weekly, unitType hours|credits, visibility internal_only|client_visible_summary|client_visible_detail', body: budgetBody },
|
|
12
12
|
'capacity.pause': { method: 'POST', path: '/api/admin/capacity-budgets/:id/pause', auth: 'apiKey', description: 'Pause a capacity budget' },
|
|
13
13
|
'capacity.resume': { method: 'POST', path: '/api/admin/capacity-budgets/:id/resume', auth: 'apiKey', description: 'Resume a capacity budget' },
|
|
14
14
|
'capacity.end': { method: 'POST', path: '/api/admin/capacity-budgets/:id/end', auth: 'apiKey', description: 'End a capacity budget' },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capacity.js","sourceRoot":"","sources":["../../commands/capacity.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,GAAG;IACjB,MAAM,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,iBAAiB;IAC3E,YAAY,EAAE,gBAAgB,EAAE,WAAW,EAAE,SAAS,EAAE,gBAAgB;IACxE,qBAAqB,EAAE,SAAS,EAAE,cAAc,EAAE,eAAe;IACjE,eAAe,EAAE,mBAAmB,EAAE,YAAY,EAAE,QAAQ,EAAE,WAAW;CAC1E,CAAC;AAEF,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,eAAe,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,iDAAiD,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,+BAA+B,EAAE;IACzJ,cAAc,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,iCAAiC,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,uBAAuB,EAAE;IAChI,iBAAiB,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,iDAAiD,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,
|
|
1
|
+
{"version":3,"file":"capacity.js","sourceRoot":"","sources":["../../commands/capacity.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,GAAG;IACjB,MAAM,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,iBAAiB;IAC3E,YAAY,EAAE,gBAAgB,EAAE,WAAW,EAAE,SAAS,EAAE,gBAAgB;IACxE,qBAAqB,EAAE,SAAS,EAAE,cAAc,EAAE,eAAe;IACjE,eAAe,EAAE,mBAAmB,EAAE,YAAY,EAAE,QAAQ,EAAE,WAAW;CAC1E,CAAC;AAEF,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,eAAe,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,iDAAiD,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,+BAA+B,EAAE;IACzJ,cAAc,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,iCAAiC,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,uBAAuB,EAAE;IAChI,iBAAiB,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,iDAAiD,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,oJAAoJ,EAAE,IAAI,EAAE,UAAU,EAAE;IACnS,iBAAiB,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,iCAAiC,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,oJAAoJ,EAAE,IAAI,EAAE,UAAU,EAAE;IACpR,gBAAgB,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,uCAAuC,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,yBAAyB,EAAE;IAC3I,iBAAiB,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,wCAAwC,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,0BAA0B,EAAE;IAC9I,cAAc,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,qCAAqC,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,uBAAuB,EAAE;IACrI,iBAAiB,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,iCAAiC,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,0BAA0B,EAAE;IACzI,kBAAkB,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,iDAAiD,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,0BAA0B,EAAE,WAAW,EAAE,CAAC,QAAQ,CAAC,EAAE;IAChL,yBAAyB,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,+CAA+C,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,yBAAyB,EAAE,WAAW,EAAE,CAAC,QAAQ,CAAC,EAAE;IACpL,yBAAyB,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,yCAAyC,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,yBAAyB,EAAE,WAAW,EAAE,CAAC,QAAQ,CAAC,EAAE;IAC9K,iBAAiB,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,gDAAgD,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,yBAAyB,EAAE,WAAW,EAAE,CAAC,QAAQ,CAAC,EAAE;IAC7K,yBAAyB,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,kDAAkD,EAAE,IAAI,EAAE,aAAa,EAAE,WAAW,EAAE,yBAAyB,EAAE,WAAW,EAAE,CAAC,QAAQ,CAAC,EAAE;IAC5L,uBAAuB,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,gDAAgD,EAAE,IAAI,EAAE,aAAa,EAAE,WAAW,EAAE,+BAA+B,EAAE,WAAW,EAAE,CAAC,QAAQ,CAAC,EAAE;CACxK,CAAC"}
|
|
@@ -25,6 +25,18 @@ import { join } from 'node:path';
|
|
|
25
25
|
import { homedir } from 'node:os';
|
|
26
26
|
import { COMMANDS } from '../commands/index.js';
|
|
27
27
|
|
|
28
|
+
class CliExit extends Error {
|
|
29
|
+
constructor(code = 1) {
|
|
30
|
+
super(`CLI_EXIT_${code}`);
|
|
31
|
+
this.code = code;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function exitGracefully(code = 1) {
|
|
36
|
+
process.exitCode = code;
|
|
37
|
+
throw new CliExit(code);
|
|
38
|
+
}
|
|
39
|
+
|
|
28
40
|
// ── Credential Loading ──────────────────────────────────────────
|
|
29
41
|
|
|
30
42
|
function loadEnvFile(filePath) {
|
|
@@ -58,7 +70,7 @@ function requireApiKey() {
|
|
|
58
70
|
if (!API_KEY) {
|
|
59
71
|
console.error('Error: PORTAL_API_KEY is required');
|
|
60
72
|
console.error('Set it via env var or create ~/.klevar/portal.env');
|
|
61
|
-
|
|
73
|
+
exitGracefully(2);
|
|
62
74
|
}
|
|
63
75
|
}
|
|
64
76
|
|
|
@@ -66,7 +78,7 @@ function requirePortalToken() {
|
|
|
66
78
|
if (!PORTAL_TOKEN) {
|
|
67
79
|
console.error('Error: PORTAL_TOKEN is required for portal commands');
|
|
68
80
|
console.error('Set it via env var, --portal-token, or ~/.klevar/portal.env');
|
|
69
|
-
|
|
81
|
+
exitGracefully(2);
|
|
70
82
|
}
|
|
71
83
|
}
|
|
72
84
|
|
|
@@ -112,8 +124,8 @@ async function api(method, path, body, auth = 'apiKey') {
|
|
|
112
124
|
|
|
113
125
|
if (!res.ok) {
|
|
114
126
|
const err = await res.json().catch(() => ({ error: { message: res.statusText } }));
|
|
115
|
-
|
|
116
|
-
|
|
127
|
+
printApiError(res.status, err);
|
|
128
|
+
exitGracefully(1);
|
|
117
129
|
}
|
|
118
130
|
|
|
119
131
|
if (res.status === 204) return null;
|
|
@@ -136,8 +148,8 @@ async function apiDownload(path, outputPath, auth = 'apiKey') {
|
|
|
136
148
|
|
|
137
149
|
if (!res.ok) {
|
|
138
150
|
const err = await res.json().catch(() => ({ error: { message: res.statusText } }));
|
|
139
|
-
|
|
140
|
-
|
|
151
|
+
printApiError(res.status, err);
|
|
152
|
+
exitGracefully(1);
|
|
141
153
|
}
|
|
142
154
|
|
|
143
155
|
const contentType = res.headers.get('content-type') || '';
|
|
@@ -158,7 +170,7 @@ async function apiDownload(path, outputPath, auth = 'apiKey') {
|
|
|
158
170
|
|
|
159
171
|
if (!outputPath) {
|
|
160
172
|
console.error('Error: --output is required when the API returns PDF bytes');
|
|
161
|
-
|
|
173
|
+
exitGracefully(1);
|
|
162
174
|
}
|
|
163
175
|
|
|
164
176
|
const bytes = Buffer.from(await res.arrayBuffer());
|
|
@@ -170,7 +182,7 @@ async function downloadPublicUrl(url, outputPath) {
|
|
|
170
182
|
const res = await fetch(url);
|
|
171
183
|
if (!res.ok) {
|
|
172
184
|
console.error(`Error ${res.status}: failed to download signed URL`);
|
|
173
|
-
|
|
185
|
+
exitGracefully(1);
|
|
174
186
|
}
|
|
175
187
|
const bytes = Buffer.from(await res.arrayBuffer());
|
|
176
188
|
writeFileSync(outputPath, bytes);
|
|
@@ -208,8 +220,8 @@ async function apiMultipart(method, path, fields, filePaths) {
|
|
|
208
220
|
});
|
|
209
221
|
if (!res.ok) {
|
|
210
222
|
const err = await res.json().catch(() => ({ error: { message: res.statusText } }));
|
|
211
|
-
|
|
212
|
-
|
|
223
|
+
printApiError(res.status, err);
|
|
224
|
+
exitGracefully(1);
|
|
213
225
|
}
|
|
214
226
|
if (res.status === 204) return null;
|
|
215
227
|
return res.json();
|
|
@@ -223,6 +235,20 @@ async function streamToBuffer(stream) {
|
|
|
223
235
|
|
|
224
236
|
// ── Arg Parsing ─────────────────────────────────────────────────
|
|
225
237
|
|
|
238
|
+
function printApiError(status, payload) {
|
|
239
|
+
const error = payload?.error ?? payload;
|
|
240
|
+
console.error(`Error ${status}: ${error?.message || 'Unknown error'}`);
|
|
241
|
+
if (Array.isArray(error?.details) && error.details.length > 0) {
|
|
242
|
+
for (const detail of error.details) {
|
|
243
|
+
const field = detail.field || detail.path || 'unknown';
|
|
244
|
+
const message = detail.message || JSON.stringify(detail);
|
|
245
|
+
console.error(` ${field}: ${message}`);
|
|
246
|
+
}
|
|
247
|
+
} else if (payload && typeof payload === 'object' && !payload.error) {
|
|
248
|
+
console.error(JSON.stringify(payload, null, 2));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
226
252
|
function parseArgs(args) {
|
|
227
253
|
const positional = [];
|
|
228
254
|
const flags = {};
|
|
@@ -599,7 +625,7 @@ if (!resource || resource === 'help' || resource === '--help') {
|
|
|
599
625
|
}
|
|
600
626
|
console.log(`\nConfig: PORTAL_API_URL, PORTAL_API_KEY, PORTAL_TOKEN (or ~/.klevar/portal.env)`);
|
|
601
627
|
console.log(`Target: ${BASE_URL}`);
|
|
602
|
-
|
|
628
|
+
exitGracefully(0);
|
|
603
629
|
}
|
|
604
630
|
|
|
605
631
|
// Resolve command key — handle single-word commands (e.g. "search <term>")
|
|
@@ -623,7 +649,7 @@ if (!cmd && COMMANDS[resource]) {
|
|
|
623
649
|
if (!cmd) {
|
|
624
650
|
console.error(`Unknown command: ${commandKey}`);
|
|
625
651
|
console.error(`Run 'klevar-portal help' for available commands.`);
|
|
626
|
-
|
|
652
|
+
exitGracefully(1);
|
|
627
653
|
}
|
|
628
654
|
|
|
629
655
|
// Parse remaining args
|
|
@@ -645,7 +671,7 @@ if (pathParams.length > 0) {
|
|
|
645
671
|
if (!value) {
|
|
646
672
|
const ordinal = index === 0 ? 'an ID argument' : `argument ${index + 1} for :${paramName}`;
|
|
647
673
|
console.error(`Command '${commandKey}' requires ${ordinal}.`);
|
|
648
|
-
|
|
674
|
+
exitGracefully(1);
|
|
649
675
|
}
|
|
650
676
|
path = path.replace(`:${paramName}`, value);
|
|
651
677
|
});
|
|
@@ -694,14 +720,14 @@ if (commandKey === 'search') {
|
|
|
694
720
|
const q = [id, ...positional.slice(1)].filter(Boolean).join(' ');
|
|
695
721
|
if (!q) {
|
|
696
722
|
console.error('Usage: search <term>');
|
|
697
|
-
|
|
723
|
+
exitGracefully(1);
|
|
698
724
|
}
|
|
699
725
|
const data = await api('GET', `/api/admin/search?q=${encodeURIComponent(q)}`);
|
|
700
726
|
const hasResults = data.projects.length + data.tasks.length + data.updates.length > 0;
|
|
701
727
|
|
|
702
728
|
if (!hasResults) {
|
|
703
729
|
console.log(`No results for "${q}"`);
|
|
704
|
-
|
|
730
|
+
exitGracefully(0);
|
|
705
731
|
}
|
|
706
732
|
|
|
707
733
|
console.log(`\nSearch: "${q}"`);
|
|
@@ -736,7 +762,7 @@ if (commandKey === 'search') {
|
|
|
736
762
|
console.log('\nUpdates: (none)');
|
|
737
763
|
}
|
|
738
764
|
|
|
739
|
-
|
|
765
|
+
exitGracefully(0);
|
|
740
766
|
}
|
|
741
767
|
|
|
742
768
|
// ── Milestone Commands (custom logic: fetch → modify → PATCH) ──
|
|
@@ -745,7 +771,7 @@ if (cmd.method === 'CUSTOM' && resource === 'milestones') {
|
|
|
745
771
|
const projectId = id;
|
|
746
772
|
if (!projectId) {
|
|
747
773
|
console.error('Usage: milestones <action> <projectId> [--name "..."] or [index]');
|
|
748
|
-
|
|
774
|
+
exitGracefully(1);
|
|
749
775
|
}
|
|
750
776
|
|
|
751
777
|
// Fetch current project
|
|
@@ -763,23 +789,23 @@ if (cmd.method === 'CUSTOM' && resource === 'milestones') {
|
|
|
763
789
|
console.log(` ${i + 1}. ${icon} ${m.name} [${m.status}]`);
|
|
764
790
|
});
|
|
765
791
|
}
|
|
766
|
-
|
|
792
|
+
exitGracefully(0);
|
|
767
793
|
}
|
|
768
794
|
|
|
769
795
|
if (action === 'add') {
|
|
770
796
|
const name = flags.name || positional[1];
|
|
771
|
-
if (!name) { console.error('Usage: milestones add <projectId> --name "Milestone name"');
|
|
797
|
+
if (!name) { console.error('Usage: milestones add <projectId> --name "Milestone name"'); exitGracefully(1); }
|
|
772
798
|
milestones.push({ name, status: 'pending' });
|
|
773
799
|
await api('PATCH', `/api/admin/projects/${projectId}`, { milestonesJson: milestones });
|
|
774
800
|
console.log(`Added milestone: ${name} (${milestones.length} total)`);
|
|
775
|
-
|
|
801
|
+
exitGracefully(0);
|
|
776
802
|
}
|
|
777
803
|
|
|
778
804
|
if (action === 'done' || action === 'undo' || action === 'remove') {
|
|
779
805
|
const idx = parseInt(positional[1] || flags.index, 10) - 1;
|
|
780
806
|
if (isNaN(idx) || idx < 0 || idx >= milestones.length) {
|
|
781
807
|
console.error(`Invalid index. Use 1-${milestones.length}.`);
|
|
782
|
-
|
|
808
|
+
exitGracefully(1);
|
|
783
809
|
}
|
|
784
810
|
if (action === 'done') {
|
|
785
811
|
milestones[idx].status = 'done';
|
|
@@ -794,11 +820,11 @@ if (cmd.method === 'CUSTOM' && resource === 'milestones') {
|
|
|
794
820
|
await api('PATCH', `/api/admin/projects/${projectId}`, { milestonesJson: milestones });
|
|
795
821
|
console.log(`Removed: ${removed[0].name} (${milestones.length} remaining)`);
|
|
796
822
|
}
|
|
797
|
-
|
|
823
|
+
exitGracefully(0);
|
|
798
824
|
}
|
|
799
825
|
|
|
800
826
|
console.error(`Unknown milestone action: ${action}`);
|
|
801
|
-
|
|
827
|
+
exitGracefully(1);
|
|
802
828
|
}
|
|
803
829
|
|
|
804
830
|
// ── File Upload (multipart) ──
|
|
@@ -812,12 +838,12 @@ if (cmd.supportsFiles && flags.file) {
|
|
|
812
838
|
if (!fields.visibility) fields.visibility = 'client';
|
|
813
839
|
const data = await apiMultipart(cmd.method, path, fields, filePaths);
|
|
814
840
|
formatOutput(data, commandKey);
|
|
815
|
-
|
|
841
|
+
exitGracefully(0);
|
|
816
842
|
}
|
|
817
843
|
|
|
818
844
|
if (cmd.method === 'DOWNLOAD') {
|
|
819
845
|
await apiDownload(path, flags.output, cmd.auth);
|
|
820
|
-
|
|
846
|
+
exitGracefully(0);
|
|
821
847
|
}
|
|
822
848
|
|
|
823
849
|
// Execute
|