@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
- // @ts-expect-error legacy JavaScript execution engine is copied during post-build.
3
- await import('../lib/legacy-runner.js');
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;AACnF,MAAM,MAAM,CAAC,yBAAyB,CAAC,CAAC"}
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,0BAA0B,EAAE,IAAI,EAAE,UAAU,EAAE;IACzK,iBAAiB,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,iCAAiC,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,0BAA0B,EAAE,IAAI,EAAE,UAAU,EAAE;IAC1J,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"}
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
- process.exit(2);
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
- process.exit(2);
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
- console.error(`Error ${res.status}: ${err.error?.message || 'Unknown error'}`);
116
- process.exit(1);
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
- console.error(`Error ${res.status}: ${err.error?.message || 'Unknown error'}`);
140
- process.exit(1);
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
- process.exit(1);
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
- process.exit(1);
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
- console.error(`Error ${res.status}: ${err.error?.message || 'Unknown error'}`);
212
- process.exit(1);
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
- process.exit(0);
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
- process.exit(1);
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
- process.exit(1);
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
- process.exit(1);
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
- process.exit(0);
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
- process.exit(0);
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
- process.exit(1);
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
- process.exit(0);
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"'); process.exit(1); }
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
- process.exit(0);
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
- process.exit(1);
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
- process.exit(0);
823
+ exitGracefully(0);
798
824
  }
799
825
 
800
826
  console.error(`Unknown milestone action: ${action}`);
801
- process.exit(1);
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
- process.exit(0);
841
+ exitGracefully(0);
816
842
  }
817
843
 
818
844
  if (cmd.method === 'DOWNLOAD') {
819
845
  await apiDownload(path, flags.output, cmd.auth);
820
- process.exit(0);
846
+ exitGracefully(0);
821
847
  }
822
848
 
823
849
  // Execute
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@klevar/portal-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "First-class npm CLI for the Klevar Client Management Portal",
5
5
  "type": "module",
6
6
  "bin": {