not-manage 0.2.0 → 0.2.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 +70 -20
- package/package.json +7 -3
- package/src/cli-options.js +278 -10
- package/src/cli.js +8 -51
- package/src/clio-api.js +33 -4
- package/src/commands-activities.js +11 -119
- package/src/commands-practice-areas.js +13 -66
- package/src/redaction-policy.js +98 -3
- package/src/redaction.js +5 -1
- package/src/resource-display.js +100 -0
- package/src/resource-handlers.js +81 -0
- package/src/resource-metadata.js +1639 -2
- package/src/resource-query-builder.js +80 -0
- package/src/resource-utils.js +48 -0
- package/src/commands-billable-clients.js +0 -105
- package/src/commands-billable-matters.js +0 -106
- package/src/commands-bills.js +0 -192
- package/src/commands-contacts.js +0 -121
- package/src/commands-matters.js +0 -156
- package/src/commands-tasks.js +0 -155
- package/src/commands-users.js +0 -134
package/README.md
CHANGED
|
@@ -48,6 +48,7 @@ For local development:
|
|
|
48
48
|
git clone https://github.com/Not-Operations/not-manage.git
|
|
49
49
|
cd not-manage
|
|
50
50
|
npm install
|
|
51
|
+
npm run hooks:install
|
|
51
52
|
node bin/not-manage.js --help
|
|
52
53
|
```
|
|
53
54
|
|
|
@@ -95,6 +96,11 @@ During setup, the CLI asks you to acknowledge that:
|
|
|
95
96
|
- [SECURITY.md](SECURITY.md)
|
|
96
97
|
- [TERMS.md](TERMS.md)
|
|
97
98
|
- [OPERATIONS.md](OPERATIONS.md)
|
|
99
|
+
- [SUPPORT.md](SUPPORT.md)
|
|
100
|
+
- [CUSTOMER-TESTING.md](CUSTOMER-TESTING.md)
|
|
101
|
+
- [SUBPROCESSORS.md](SUBPROCESSORS.md)
|
|
102
|
+
- [MVSP-SELF-ASSESSMENT.md](MVSP-SELF-ASSESSMENT.md)
|
|
103
|
+
- [CLIO-APP-DIRECTORY-CHECKLIST.md](CLIO-APP-DIRECTORY-CHECKLIST.md)
|
|
98
104
|
|
|
99
105
|
## Core commands
|
|
100
106
|
|
|
@@ -103,34 +109,63 @@ not-manage setup
|
|
|
103
109
|
not-manage auth setup
|
|
104
110
|
not-manage auth login
|
|
105
111
|
not-manage auth status
|
|
106
|
-
not-manage activities list
|
|
107
|
-
not-manage activity get 123
|
|
108
|
-
not-manage tasks list
|
|
109
|
-
not-manage task get 789
|
|
110
|
-
not-manage contacts list
|
|
111
|
-
not-manage contact get 12345
|
|
112
|
-
not-manage time-entries list
|
|
113
|
-
not-manage billable-clients list
|
|
114
|
-
not-manage billable-matters list
|
|
115
|
-
not-manage bills list
|
|
116
|
-
not-manage bill get 987
|
|
117
|
-
not-manage invoices list
|
|
118
|
-
not-manage matters list
|
|
119
|
-
not-manage matter get 456
|
|
120
|
-
not-manage users list
|
|
121
|
-
not-manage user get 123
|
|
122
|
-
not-manage practice-areas list
|
|
123
|
-
not-manage practice-area get 45
|
|
124
|
-
not-manage matters list --status open --limit 50
|
|
125
|
-
not-manage matters list --all --json
|
|
126
112
|
not-manage whoami
|
|
127
113
|
not-manage auth revoke
|
|
128
114
|
```
|
|
129
115
|
|
|
116
|
+
## Resource command reference
|
|
117
|
+
|
|
118
|
+
<!-- GENERATED:CLI_REFERENCE:start -->
|
|
119
|
+
This table is generated from resource metadata. Global auth/setup commands stay hand-written.
|
|
120
|
+
|
|
121
|
+
| Command | Operations | Aliases | Required list filters |
|
|
122
|
+
| --- | --- | --- | --- |
|
|
123
|
+
| `activities` | `list`, `get` | `activity` | - |
|
|
124
|
+
| `calendar-entries` | `list`, `get` | `calendar-entry` | - |
|
|
125
|
+
| `reminders` | `list`, `get` | `reminder` | - |
|
|
126
|
+
| `tasks` | `list`, `get` | `task` | - |
|
|
127
|
+
| `contacts` | `list`, `get` | `contact` | - |
|
|
128
|
+
| `communications` | `list`, `get` | `communication` | - |
|
|
129
|
+
| `conversations` | `list`, `get` | `conversation` | - |
|
|
130
|
+
| `conversation-messages` | `list`, `get` | `conversation-message` | `--conversation-id` |
|
|
131
|
+
| `notes` | `list`, `get` | `note` | `--type` |
|
|
132
|
+
| `custom-fields` | `list`, `get` | `custom-field` | - |
|
|
133
|
+
| `time-entries` | `list`, `get` | `time-entry` | - |
|
|
134
|
+
| `billable-clients` | `list` | `billable-client` | - |
|
|
135
|
+
| `billable-matters` | `list` | `billable-matter` | - |
|
|
136
|
+
| `bills` | `list`, `get` | `bill` | - |
|
|
137
|
+
| `invoices` | `list`, `get` | `invoice` | - |
|
|
138
|
+
| `outstanding-client-balances` | `list` | `outstanding-client-balance` | - |
|
|
139
|
+
| `matters` | `list`, `get` | `matter` | - |
|
|
140
|
+
| `matter-dockets` | `list`, `get` | `matter-docket` | - |
|
|
141
|
+
| `users` | `list`, `get` | `user` | - |
|
|
142
|
+
| `practice-areas` | `list`, `get` | `practice-area` | - |
|
|
143
|
+
| `my-events` | `list` | `my-event` | - |
|
|
144
|
+
|
|
145
|
+
Required list filters are enforced by the CLI before it calls Clio.
|
|
146
|
+
<!-- GENERATED:CLI_REFERENCE:end -->
|
|
147
|
+
|
|
130
148
|
Plural commands still work. Singular aliases are accepted for the single-record flows so `contact get 12345` and `contacts get 12345` both work.
|
|
131
149
|
|
|
132
150
|
Every data command also accepts `--fields <comma-separated-list>` to override the default response shape. If you pass `--fields` with no value, the CLI prints the current default field list for that command.
|
|
133
151
|
|
|
152
|
+
## Live smoke checks
|
|
153
|
+
|
|
154
|
+
In a local checkout, if you already have an authenticated test account, you can run a safe read-only smoke pass:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
npm run smoke:live
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
This exercises a small set of real CLI reads against the currently authenticated Clio account:
|
|
161
|
+
|
|
162
|
+
- `auth status`
|
|
163
|
+
- `whoami`
|
|
164
|
+
- `users list`
|
|
165
|
+
- `notes list --type Matter`
|
|
166
|
+
- `outstanding-client-balances list`
|
|
167
|
+
- `calendar-entries list`
|
|
168
|
+
|
|
134
169
|
## Read-only examples
|
|
135
170
|
|
|
136
171
|
```bash
|
|
@@ -165,6 +200,14 @@ not-manage practice-areas list --name "Family"
|
|
|
165
200
|
not-manage practice-areas list --matter-id 456
|
|
166
201
|
not-manage practice-area get 45
|
|
167
202
|
|
|
203
|
+
not-manage calendar-entries list --from 2026-03-01T00:00:00Z --to 2026-03-31T23:59:59Z
|
|
204
|
+
not-manage reminders list --state pending
|
|
205
|
+
not-manage notes list --type Matter --limit 25
|
|
206
|
+
not-manage conversation-messages list --conversation-id 123
|
|
207
|
+
not-manage outstanding-client-balances list --limit 25
|
|
208
|
+
not-manage matter-dockets list --matter-id 456
|
|
209
|
+
not-manage my-events list --limit 25
|
|
210
|
+
|
|
168
211
|
not-manage billable-matters list --client-id 999
|
|
169
212
|
not-manage billable-clients list --start-date 2026-03-01
|
|
170
213
|
not-manage matter get 456 --redacted
|
|
@@ -201,6 +244,13 @@ not-manage matter get 456 --redacted
|
|
|
201
244
|
- Dependabot is configured for both npm dependencies and GitHub Actions.
|
|
202
245
|
- npm release publishing is set up for provenance-backed trusted publishing from GitHub Actions using the `npm` environment.
|
|
203
246
|
|
|
247
|
+
## Support
|
|
248
|
+
|
|
249
|
+
- Public support page: [SUPPORT.md](SUPPORT.md)
|
|
250
|
+
- Public support contact: `hello@notoperations.com`
|
|
251
|
+
- Bug reports and feature requests: `https://github.com/Not-Operations/not-manage/issues`
|
|
252
|
+
- Security reports: see [SECURITY.md](SECURITY.md)
|
|
253
|
+
|
|
204
254
|
## Troubleshooting
|
|
205
255
|
|
|
206
256
|
- `OS keychain ... failed`: re-enable your OS keychain service or run the CLI in a supported desktop session with keychain access.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "not-manage",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Unofficial command-line tool for Clio Manage integrations",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"private": false,
|
|
@@ -31,14 +31,18 @@
|
|
|
31
31
|
"cli"
|
|
32
32
|
],
|
|
33
33
|
"scripts": {
|
|
34
|
-
"check": "npm run check:syntax && npm test",
|
|
34
|
+
"check": "npm run check:syntax && npm test && npm run docs:check",
|
|
35
35
|
"check:security": "npm audit --audit-level=high",
|
|
36
|
-
"check:syntax": "node --check bin/not-manage.js && node --check src/*.js && node --check test/*.test.js && node --check test/helpers/*.js",
|
|
36
|
+
"check:syntax": "node --check bin/not-manage.js && node --check src/*.js && node --check scripts/*.js && node --check test/*.test.js && node --check test/helpers/*.js",
|
|
37
37
|
"dev": "node bin/not-manage.js",
|
|
38
|
+
"docs:check": "node scripts/generate-readme-cli-reference.js --check",
|
|
39
|
+
"docs:generate": "node scripts/generate-readme-cli-reference.js",
|
|
40
|
+
"hooks:install": "node scripts/install-git-hooks.js",
|
|
38
41
|
"pack:check": "npm pack --dry-run",
|
|
39
42
|
"postinstall": "node src/postinstall.js",
|
|
40
43
|
"seed:handbook-imports": "node scripts/generate-handbook-import-csvs.js",
|
|
41
44
|
"seed:historical": "node scripts/seed-historical-data.js",
|
|
45
|
+
"smoke:live": "node scripts/smoke-read-only-live.js",
|
|
42
46
|
"start": "node bin/not-manage.js",
|
|
43
47
|
"test": "node --test test/*.test.js"
|
|
44
48
|
},
|
package/src/cli-options.js
CHANGED
|
@@ -2,6 +2,10 @@ function hasOwn(object, key) {
|
|
|
2
2
|
return Object.prototype.hasOwnProperty.call(object, key);
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
+
function isPlainObject(value) {
|
|
6
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
|
|
5
9
|
function normalizeOptionName(name) {
|
|
6
10
|
return String(name).replace(/^--/, "");
|
|
7
11
|
}
|
|
@@ -23,6 +27,20 @@ function hasFlag(args, ...flags) {
|
|
|
23
27
|
return flags.some((flag) => args.includes(flag));
|
|
24
28
|
}
|
|
25
29
|
|
|
30
|
+
function appendParsedValue(parsed, key, value) {
|
|
31
|
+
if (!hasOwn(parsed, key)) {
|
|
32
|
+
parsed[key] = value;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (Array.isArray(parsed[key])) {
|
|
37
|
+
parsed[key].push(value);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
parsed[key] = [parsed[key], value];
|
|
42
|
+
}
|
|
43
|
+
|
|
26
44
|
function parseOptions(args) {
|
|
27
45
|
const parsed = {};
|
|
28
46
|
const positional = [];
|
|
@@ -39,35 +57,61 @@ function parseOptions(args) {
|
|
|
39
57
|
const inlineValue = rest.length > 0 ? rest.join("=") : null;
|
|
40
58
|
|
|
41
59
|
if (inlineValue !== null) {
|
|
42
|
-
parsed
|
|
60
|
+
appendParsedValue(parsed, rawKey, inlineValue);
|
|
43
61
|
continue;
|
|
44
62
|
}
|
|
45
63
|
|
|
46
64
|
const next = args[index + 1];
|
|
47
65
|
if (next && !next.startsWith("--")) {
|
|
48
|
-
parsed
|
|
66
|
+
appendParsedValue(parsed, rawKey, next);
|
|
49
67
|
index += 1;
|
|
50
68
|
continue;
|
|
51
69
|
}
|
|
52
70
|
|
|
53
|
-
parsed
|
|
71
|
+
appendParsedValue(parsed, rawKey, true);
|
|
54
72
|
}
|
|
55
73
|
|
|
56
74
|
return { parsed, positional };
|
|
57
75
|
}
|
|
58
76
|
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
77
|
+
function normalizeOptionValues(value) {
|
|
78
|
+
if (value === undefined) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return Array.isArray(value) ? value : [value];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readOptionValues(parsedOptions, name) {
|
|
86
|
+
return optionKeyCandidates(name).reduce((values, candidate) => {
|
|
87
|
+
if (!hasOwn(parsedOptions, candidate)) {
|
|
88
|
+
return values;
|
|
63
89
|
}
|
|
90
|
+
|
|
91
|
+
return values.concat(normalizeOptionValues(parsedOptions[candidate]));
|
|
92
|
+
}, []);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function readOption(parsedOptions, name) {
|
|
96
|
+
const values = readOptionValues(parsedOptions, name);
|
|
97
|
+
if (values.length === 0) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return values.length === 1 ? values[0] : values;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function readLastOptionValue(parsedOptions, name) {
|
|
105
|
+
const values = readOptionValues(parsedOptions, name);
|
|
106
|
+
if (values.length === 0) {
|
|
107
|
+
return undefined;
|
|
64
108
|
}
|
|
65
109
|
|
|
66
|
-
return
|
|
110
|
+
return values[values.length - 1];
|
|
67
111
|
}
|
|
68
112
|
|
|
69
113
|
function readStringOption(parsedOptions, name) {
|
|
70
|
-
const value =
|
|
114
|
+
const value = readLastOptionValue(parsedOptions, name);
|
|
71
115
|
if (value === undefined) {
|
|
72
116
|
return undefined;
|
|
73
117
|
}
|
|
@@ -79,6 +123,34 @@ function readStringOption(parsedOptions, name) {
|
|
|
79
123
|
return String(value);
|
|
80
124
|
}
|
|
81
125
|
|
|
126
|
+
function splitCommaSeparatedValues(text) {
|
|
127
|
+
return String(text)
|
|
128
|
+
.split(",")
|
|
129
|
+
.map((value) => value.trim())
|
|
130
|
+
.filter(Boolean);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function readStringArrayOption(parsedOptions, name) {
|
|
134
|
+
const values = readOptionValues(parsedOptions, name);
|
|
135
|
+
if (values.length === 0) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const output = [];
|
|
140
|
+
|
|
141
|
+
values.forEach((value) => {
|
|
142
|
+
if (value === true) {
|
|
143
|
+
throw new Error(`\`--${toKebabCase(name)}\` requires a value.`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
splitCommaSeparatedValues(value).forEach((item) => {
|
|
147
|
+
output.push(item);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return output.length > 0 ? output : undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
82
154
|
function parseBooleanValue(value, name) {
|
|
83
155
|
if (value === true || value === false) {
|
|
84
156
|
return value;
|
|
@@ -98,7 +170,7 @@ function parseBooleanValue(value, name) {
|
|
|
98
170
|
}
|
|
99
171
|
|
|
100
172
|
function readBooleanOption(parsedOptions, name) {
|
|
101
|
-
const value =
|
|
173
|
+
const value = readLastOptionValue(parsedOptions, name);
|
|
102
174
|
if (value === undefined) {
|
|
103
175
|
return undefined;
|
|
104
176
|
}
|
|
@@ -156,6 +228,185 @@ function readIsoDateTimeOption(parsedOptions, name) {
|
|
|
156
228
|
return validateIsoDateTime(value, name);
|
|
157
229
|
}
|
|
158
230
|
|
|
231
|
+
function readIsoDateArrayOption(parsedOptions, name) {
|
|
232
|
+
const values = readStringArrayOption(parsedOptions, name);
|
|
233
|
+
if (values === undefined) {
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return values.map((value) => validateIsoDate(value, name));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function readIsoDateTimeArrayOption(parsedOptions, name) {
|
|
241
|
+
const values = readStringArrayOption(parsedOptions, name);
|
|
242
|
+
if (values === undefined) {
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return values.map((value) => validateIsoDateTime(value, name));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function splitTopLevel(text, delimiter = ",") {
|
|
250
|
+
const segments = [];
|
|
251
|
+
let current = "";
|
|
252
|
+
let depth = 0;
|
|
253
|
+
let quote = null;
|
|
254
|
+
let escapeNext = false;
|
|
255
|
+
|
|
256
|
+
for (const char of String(text)) {
|
|
257
|
+
if (escapeNext) {
|
|
258
|
+
current += char;
|
|
259
|
+
escapeNext = false;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (quote) {
|
|
264
|
+
current += char;
|
|
265
|
+
if (char === "\\") {
|
|
266
|
+
escapeNext = true;
|
|
267
|
+
} else if (char === quote) {
|
|
268
|
+
quote = null;
|
|
269
|
+
}
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (char === "'" || char === '"') {
|
|
274
|
+
quote = char;
|
|
275
|
+
current += char;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (char === "[" || char === "{") {
|
|
280
|
+
depth += 1;
|
|
281
|
+
current += char;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (char === "]" || char === "}") {
|
|
286
|
+
depth = Math.max(0, depth - 1);
|
|
287
|
+
current += char;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (char === delimiter && depth === 0) {
|
|
292
|
+
if (current.trim()) {
|
|
293
|
+
segments.push(current.trim());
|
|
294
|
+
}
|
|
295
|
+
current = "";
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
current += char;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (current.trim()) {
|
|
303
|
+
segments.push(current.trim());
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return segments;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function unwrapQuotedValue(value) {
|
|
310
|
+
const text = String(value).trim();
|
|
311
|
+
if (
|
|
312
|
+
(text.startsWith('"') && text.endsWith('"')) ||
|
|
313
|
+
(text.startsWith("'") && text.endsWith("'"))
|
|
314
|
+
) {
|
|
315
|
+
return text.slice(1, -1);
|
|
316
|
+
}
|
|
317
|
+
return text;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function parseStructuredValue(rawValue) {
|
|
321
|
+
const text = String(rawValue).trim();
|
|
322
|
+
if (!text) {
|
|
323
|
+
return "";
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (
|
|
327
|
+
(text.startsWith("{") && text.endsWith("}")) ||
|
|
328
|
+
(text.startsWith("[") && text.endsWith("]")) ||
|
|
329
|
+
(text.startsWith('"') && text.endsWith('"'))
|
|
330
|
+
) {
|
|
331
|
+
try {
|
|
332
|
+
return JSON.parse(text);
|
|
333
|
+
} catch (_error) {
|
|
334
|
+
if (text.startsWith("[") && text.endsWith("]")) {
|
|
335
|
+
return splitTopLevel(text.slice(1, -1)).map((item) => unwrapQuotedValue(item));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return unwrapQuotedValue(text);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function mergeStructuredValues(existingValue, nextValue) {
|
|
344
|
+
if (existingValue === undefined) {
|
|
345
|
+
return nextValue;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (isPlainObject(existingValue) && isPlainObject(nextValue)) {
|
|
349
|
+
return Object.entries(nextValue).reduce((merged, [key, value]) => {
|
|
350
|
+
merged[key] = mergeStructuredValues(merged[key], value);
|
|
351
|
+
return merged;
|
|
352
|
+
}, { ...existingValue });
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const existingItems = Array.isArray(existingValue) ? existingValue : [existingValue];
|
|
356
|
+
const nextItems = Array.isArray(nextValue) ? nextValue : [nextValue];
|
|
357
|
+
return existingItems.concat(nextItems);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function parseObjectEntries(rawValue, name) {
|
|
361
|
+
const text = String(rawValue).trim();
|
|
362
|
+
if (!text) {
|
|
363
|
+
throw new Error(`\`--${toKebabCase(name)}\` requires a value.`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (text.startsWith("{")) {
|
|
367
|
+
const parsed = parseStructuredValue(text);
|
|
368
|
+
if (!isPlainObject(parsed)) {
|
|
369
|
+
throw new Error(`\`--${toKebabCase(name)}\` must be an object or key=value pairs.`);
|
|
370
|
+
}
|
|
371
|
+
return parsed;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return splitTopLevel(text).reduce((output, entry) => {
|
|
375
|
+
const separatorIndex = entry.search(/[:=]/);
|
|
376
|
+
if (separatorIndex <= 0) {
|
|
377
|
+
throw new Error(
|
|
378
|
+
`\`--${toKebabCase(name)}\` entries must look like \`field=value\` or JSON.`
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const key = entry.slice(0, separatorIndex).trim();
|
|
383
|
+
const value = entry.slice(separatorIndex + 1).trim();
|
|
384
|
+
if (!key) {
|
|
385
|
+
throw new Error(
|
|
386
|
+
`\`--${toKebabCase(name)}\` entries must include a field identifier before \`=\` or \`:\`.`
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
output[key] = mergeStructuredValues(output[key], parseStructuredValue(value));
|
|
391
|
+
return output;
|
|
392
|
+
}, {});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function readObjectOption(parsedOptions, name) {
|
|
396
|
+
const values = readOptionValues(parsedOptions, name);
|
|
397
|
+
if (values.length === 0) {
|
|
398
|
+
return undefined;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return values.reduce((output, value) => {
|
|
402
|
+
if (value === true) {
|
|
403
|
+
throw new Error(`\`--${toKebabCase(name)}\` requires a value.`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return mergeStructuredValues(output, parseObjectEntries(value, name));
|
|
407
|
+
}, undefined);
|
|
408
|
+
}
|
|
409
|
+
|
|
159
410
|
function readCommandOptions(parsedOptions, schema, positional = [], baseOptions = {}, fixed = {}) {
|
|
160
411
|
const options = { ...baseOptions };
|
|
161
412
|
|
|
@@ -175,9 +426,21 @@ function readCommandOptions(parsedOptions, schema, positional = [], baseOptions
|
|
|
175
426
|
case "iso-date":
|
|
176
427
|
options[propertyName] = readIsoDateOption(parsedOptions, optionDef.option);
|
|
177
428
|
return;
|
|
429
|
+
case "iso-date-array":
|
|
430
|
+
options[propertyName] = readIsoDateArrayOption(parsedOptions, optionDef.option);
|
|
431
|
+
return;
|
|
178
432
|
case "iso-datetime":
|
|
179
433
|
options[propertyName] = readIsoDateTimeOption(parsedOptions, optionDef.option);
|
|
180
434
|
return;
|
|
435
|
+
case "iso-datetime-array":
|
|
436
|
+
options[propertyName] = readIsoDateTimeArrayOption(parsedOptions, optionDef.option);
|
|
437
|
+
return;
|
|
438
|
+
case "object":
|
|
439
|
+
options[propertyName] = readObjectOption(parsedOptions, optionDef.option);
|
|
440
|
+
return;
|
|
441
|
+
case "string-array":
|
|
442
|
+
options[propertyName] = readStringArrayOption(parsedOptions, optionDef.option);
|
|
443
|
+
return;
|
|
181
444
|
case "string":
|
|
182
445
|
default:
|
|
183
446
|
options[propertyName] = readStringOption(parsedOptions, optionDef.option);
|
|
@@ -197,8 +460,13 @@ module.exports = {
|
|
|
197
460
|
readCommandOptions,
|
|
198
461
|
readFlagOption,
|
|
199
462
|
readIsoDateOption,
|
|
463
|
+
readIsoDateArrayOption,
|
|
200
464
|
readIsoDateTimeOption,
|
|
465
|
+
readIsoDateTimeArrayOption,
|
|
466
|
+
readObjectOption,
|
|
201
467
|
readOption,
|
|
468
|
+
readOptionValues,
|
|
202
469
|
readStringOption,
|
|
470
|
+
readStringArrayOption,
|
|
203
471
|
toKebabCase,
|
|
204
472
|
};
|
package/src/cli.js
CHANGED
|
@@ -7,60 +7,16 @@ const {
|
|
|
7
7
|
setupWizard,
|
|
8
8
|
whoAmI,
|
|
9
9
|
} = require("./commands-auth");
|
|
10
|
-
const { activitiesGet, activitiesList } = require("./commands-activities");
|
|
11
|
-
const { billableClientsList } = require("./commands-billable-clients");
|
|
12
|
-
const { billableMattersList } = require("./commands-billable-matters");
|
|
13
|
-
const { billsGet, billsList } = require("./commands-bills");
|
|
14
|
-
const { contactsGet, contactsList } = require("./commands-contacts");
|
|
15
|
-
const { mattersGet, mattersList } = require("./commands-matters");
|
|
16
|
-
const { practiceAreasGet, practiceAreasList } = require("./commands-practice-areas");
|
|
17
|
-
const { tasksGet, tasksList } = require("./commands-tasks");
|
|
18
|
-
const { usersGet, usersList } = require("./commands-users");
|
|
19
10
|
const { hasFlag, parseOptions, readBooleanOption, readCommandOptions } = require("./cli-options");
|
|
11
|
+
const { getResourceHandler } = require("./resource-handlers");
|
|
20
12
|
const {
|
|
21
13
|
RESOURCE_ORDER,
|
|
22
14
|
getResourceMetadata,
|
|
15
|
+
listRequiredOptionFlags,
|
|
23
16
|
normalizeResourceCommand,
|
|
24
17
|
} = require("./resource-metadata");
|
|
25
18
|
const { version } = require("../package.json");
|
|
26
19
|
|
|
27
|
-
const RESOURCE_HANDLERS = {
|
|
28
|
-
activities: {
|
|
29
|
-
get: activitiesGet,
|
|
30
|
-
list: activitiesList,
|
|
31
|
-
},
|
|
32
|
-
"billable-clients": {
|
|
33
|
-
list: billableClientsList,
|
|
34
|
-
},
|
|
35
|
-
"billable-matters": {
|
|
36
|
-
list: billableMattersList,
|
|
37
|
-
},
|
|
38
|
-
bills: {
|
|
39
|
-
get: billsGet,
|
|
40
|
-
list: billsList,
|
|
41
|
-
},
|
|
42
|
-
contacts: {
|
|
43
|
-
get: contactsGet,
|
|
44
|
-
list: contactsList,
|
|
45
|
-
},
|
|
46
|
-
matters: {
|
|
47
|
-
get: mattersGet,
|
|
48
|
-
list: mattersList,
|
|
49
|
-
},
|
|
50
|
-
"practice-areas": {
|
|
51
|
-
get: practiceAreasGet,
|
|
52
|
-
list: practiceAreasList,
|
|
53
|
-
},
|
|
54
|
-
tasks: {
|
|
55
|
-
get: tasksGet,
|
|
56
|
-
list: tasksList,
|
|
57
|
-
},
|
|
58
|
-
users: {
|
|
59
|
-
get: usersGet,
|
|
60
|
-
list: usersList,
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
|
|
64
20
|
function maybePrintDefaultFields(command, sub, optionValues) {
|
|
65
21
|
if (optionValues.fields !== true) {
|
|
66
22
|
return false;
|
|
@@ -134,12 +90,15 @@ function printHelp() {
|
|
|
134
90
|
RESOURCE_ORDER.forEach((command) => {
|
|
135
91
|
const resourceMetadata = getResourceMetadata(command);
|
|
136
92
|
["list", "get"].forEach((sub) => {
|
|
137
|
-
if (!resourceMetadata.
|
|
93
|
+
if (!resourceMetadata.capabilities[sub].enabled) {
|
|
138
94
|
return;
|
|
139
95
|
}
|
|
140
96
|
|
|
141
97
|
const usage = `${command} ${sub}`.padEnd(18, " ");
|
|
142
|
-
|
|
98
|
+
const requiredFlags = listRequiredOptionFlags(resourceMetadata, sub);
|
|
99
|
+
const requirementNote =
|
|
100
|
+
requiredFlags.length > 0 ? ` (requires ${requiredFlags.join(", ")})` : "";
|
|
101
|
+
console.log(` ${usage} ${resourceMetadata.help[sub]}${requirementNote}`);
|
|
143
102
|
});
|
|
144
103
|
});
|
|
145
104
|
|
|
@@ -231,9 +190,7 @@ async function run(args) {
|
|
|
231
190
|
}
|
|
232
191
|
|
|
233
192
|
const resourceMetadata = getResourceMetadata(command);
|
|
234
|
-
const handler = resourceMetadata
|
|
235
|
-
? RESOURCE_HANDLERS[resourceMetadata.handlerKey]?.[sub]
|
|
236
|
-
: null;
|
|
193
|
+
const handler = getResourceHandler(resourceMetadata, sub);
|
|
237
194
|
|
|
238
195
|
if (resourceMetadata && handler) {
|
|
239
196
|
warnAboutRedaction(resourceMetadata, sub, optionValues, redacted);
|
package/src/clio-api.js
CHANGED
|
@@ -5,6 +5,10 @@ function createError(message, responseText) {
|
|
|
5
5
|
return new Error(`${message}.${suffix}`.trim());
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
function isPlainObject(value) {
|
|
9
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
|
|
8
12
|
async function postForm(url, formFields, headers = {}) {
|
|
9
13
|
const response = await fetch(url, {
|
|
10
14
|
method: "POST",
|
|
@@ -136,21 +140,45 @@ function parseTrustedApiUrl(config, url, expectedPathPrefix = "/api/v4/") {
|
|
|
136
140
|
function buildUrlWithQuery(baseUrl, query = {}) {
|
|
137
141
|
const url = new URL(baseUrl);
|
|
138
142
|
|
|
139
|
-
|
|
143
|
+
function appendQueryValue(key, value) {
|
|
140
144
|
if (value === undefined || value === null || value === "") {
|
|
141
145
|
return;
|
|
142
146
|
}
|
|
143
147
|
|
|
144
148
|
if (Array.isArray(value)) {
|
|
145
149
|
value.forEach((item) => {
|
|
146
|
-
|
|
147
|
-
|
|
150
|
+
appendQueryValue(key, item);
|
|
151
|
+
});
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (isPlainObject(value)) {
|
|
156
|
+
Object.entries(value).forEach(([nestedKey, nestedValue]) => {
|
|
157
|
+
if (nestedValue === undefined || nestedValue === null || nestedValue === "") {
|
|
158
|
+
return;
|
|
148
159
|
}
|
|
160
|
+
|
|
161
|
+
const compositeKey = `${key}[${nestedKey}]`;
|
|
162
|
+
if (Array.isArray(nestedValue)) {
|
|
163
|
+
const serialized = nestedValue
|
|
164
|
+
.filter((item) => item !== undefined && item !== null && item !== "")
|
|
165
|
+
.map((item) => String(item));
|
|
166
|
+
if (serialized.length > 0) {
|
|
167
|
+
url.searchParams.append(compositeKey, `[${serialized.join(", ")}]`);
|
|
168
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
appendQueryValue(compositeKey, nestedValue);
|
|
149
173
|
});
|
|
150
174
|
return;
|
|
151
175
|
}
|
|
152
176
|
|
|
153
|
-
url.searchParams.
|
|
177
|
+
url.searchParams.append(key, String(value));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
Object.entries(query).forEach(([key, value]) => {
|
|
181
|
+
appendQueryValue(key, value);
|
|
154
182
|
});
|
|
155
183
|
|
|
156
184
|
return url.toString();
|
|
@@ -382,6 +410,7 @@ module.exports = {
|
|
|
382
410
|
fetchWhoAmI,
|
|
383
411
|
getValidAccessToken,
|
|
384
412
|
__private: {
|
|
413
|
+
buildUrlWithQuery,
|
|
385
414
|
parseTrustedApiUrl,
|
|
386
415
|
},
|
|
387
416
|
};
|