not-manage 0.1.18 → 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 +472 -0
- package/src/cli.js +53 -513
- package/src/clio-api.js +36 -4
- package/src/commands-activities.js +99 -224
- package/src/commands-practice-areas.js +68 -149
- package/src/redaction-policy.js +134 -0
- package/src/redaction.js +41 -38
- package/src/resource-command-runner.js +142 -0
- package/src/resource-display.js +100 -0
- package/src/resource-handlers.js +81 -0
- package/src/resource-metadata.js +2228 -0
- package/src/resource-query-builder.js +80 -0
- package/src/resource-utils.js +48 -0
- package/src/commands-billable-clients.js +0 -152
- package/src/commands-billable-matters.js +0 -150
- package/src/commands-bills.js +0 -250
- package/src/commands-contacts.js +0 -179
- package/src/commands-matters.js +0 -214
- package/src/commands-tasks.js +0 -213
- package/src/commands-users.js +0 -192
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.1
|
|
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
|
},
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
function hasOwn(object, key) {
|
|
2
|
+
return Object.prototype.hasOwnProperty.call(object, key);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function isPlainObject(value) {
|
|
6
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function normalizeOptionName(name) {
|
|
10
|
+
return String(name).replace(/^--/, "");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function toSnakeCase(name) {
|
|
14
|
+
return normalizeOptionName(name).replace(/-/g, "_");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function toKebabCase(name) {
|
|
18
|
+
return normalizeOptionName(name).replace(/_/g, "-");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function optionKeyCandidates(name) {
|
|
22
|
+
const normalized = normalizeOptionName(name);
|
|
23
|
+
return [...new Set([normalized, toKebabCase(normalized), toSnakeCase(normalized)])];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function hasFlag(args, ...flags) {
|
|
27
|
+
return flags.some((flag) => args.includes(flag));
|
|
28
|
+
}
|
|
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
|
+
|
|
44
|
+
function parseOptions(args) {
|
|
45
|
+
const parsed = {};
|
|
46
|
+
const positional = [];
|
|
47
|
+
|
|
48
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
49
|
+
const token = args[index];
|
|
50
|
+
|
|
51
|
+
if (!token.startsWith("--")) {
|
|
52
|
+
positional.push(token);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const [rawKey, ...rest] = token.slice(2).split("=");
|
|
57
|
+
const inlineValue = rest.length > 0 ? rest.join("=") : null;
|
|
58
|
+
|
|
59
|
+
if (inlineValue !== null) {
|
|
60
|
+
appendParsedValue(parsed, rawKey, inlineValue);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const next = args[index + 1];
|
|
65
|
+
if (next && !next.startsWith("--")) {
|
|
66
|
+
appendParsedValue(parsed, rawKey, next);
|
|
67
|
+
index += 1;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
appendParsedValue(parsed, rawKey, true);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { parsed, positional };
|
|
75
|
+
}
|
|
76
|
+
|
|
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;
|
|
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;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return values[values.length - 1];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function readStringOption(parsedOptions, name) {
|
|
114
|
+
const value = readLastOptionValue(parsedOptions, name);
|
|
115
|
+
if (value === undefined) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (value === true) {
|
|
120
|
+
throw new Error(`\`--${toKebabCase(name)}\` requires a value.`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return String(value);
|
|
124
|
+
}
|
|
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
|
+
|
|
154
|
+
function parseBooleanValue(value, name) {
|
|
155
|
+
if (value === true || value === false) {
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (typeof value === "string") {
|
|
160
|
+
const normalized = value.trim().toLowerCase();
|
|
161
|
+
if (normalized === "true") {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
if (normalized === "false") {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
throw new Error(`\`--${toKebabCase(name)}\` must be \`true\` or \`false\`.`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function readBooleanOption(parsedOptions, name) {
|
|
173
|
+
const value = readLastOptionValue(parsedOptions, name);
|
|
174
|
+
if (value === undefined) {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return parseBooleanValue(value, name);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function readFlagOption(parsedOptions, name) {
|
|
182
|
+
const value = readBooleanOption(parsedOptions, name);
|
|
183
|
+
return value === undefined ? false : value;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function validateIsoDate(value, name) {
|
|
187
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
188
|
+
if (!match) {
|
|
189
|
+
throw new Error(`\`--${toKebabCase(name)}\` must be an ISO date like \`2026-03-13\`.`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const isoValue = `${match[1]}-${match[2]}-${match[3]}`;
|
|
193
|
+
const parsed = new Date(`${isoValue}T00:00:00Z`);
|
|
194
|
+
if (Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== isoValue) {
|
|
195
|
+
throw new Error(`\`--${toKebabCase(name)}\` must be an ISO date like \`2026-03-13\`.`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return value;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function validateIsoDateTime(value, name) {
|
|
202
|
+
const isoDateTime =
|
|
203
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
204
|
+
if (!isoDateTime.test(value) || Number.isNaN(Date.parse(value))) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`\`--${toKebabCase(name)}\` must be an ISO datetime like \`2026-03-13T15:00:00Z\`.`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return value;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function readIsoDateOption(parsedOptions, name) {
|
|
214
|
+
const value = readStringOption(parsedOptions, name);
|
|
215
|
+
if (value === undefined) {
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return validateIsoDate(value, name);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function readIsoDateTimeOption(parsedOptions, name) {
|
|
223
|
+
const value = readStringOption(parsedOptions, name);
|
|
224
|
+
if (value === undefined) {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return validateIsoDateTime(value, name);
|
|
229
|
+
}
|
|
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
|
+
|
|
410
|
+
function readCommandOptions(parsedOptions, schema, positional = [], baseOptions = {}, fixed = {}) {
|
|
411
|
+
const options = { ...baseOptions };
|
|
412
|
+
|
|
413
|
+
Object.entries(schema || {}).forEach(([propertyName, optionDef]) => {
|
|
414
|
+
if (optionDef.positional !== undefined) {
|
|
415
|
+
options[propertyName] = positional[optionDef.positional];
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
switch (optionDef.kind) {
|
|
420
|
+
case "boolean":
|
|
421
|
+
options[propertyName] = readBooleanOption(parsedOptions, optionDef.option);
|
|
422
|
+
return;
|
|
423
|
+
case "flag":
|
|
424
|
+
options[propertyName] = readFlagOption(parsedOptions, optionDef.option);
|
|
425
|
+
return;
|
|
426
|
+
case "iso-date":
|
|
427
|
+
options[propertyName] = readIsoDateOption(parsedOptions, optionDef.option);
|
|
428
|
+
return;
|
|
429
|
+
case "iso-date-array":
|
|
430
|
+
options[propertyName] = readIsoDateArrayOption(parsedOptions, optionDef.option);
|
|
431
|
+
return;
|
|
432
|
+
case "iso-datetime":
|
|
433
|
+
options[propertyName] = readIsoDateTimeOption(parsedOptions, optionDef.option);
|
|
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;
|
|
444
|
+
case "string":
|
|
445
|
+
default:
|
|
446
|
+
options[propertyName] = readStringOption(parsedOptions, optionDef.option);
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
...options,
|
|
452
|
+
...fixed,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
module.exports = {
|
|
457
|
+
hasFlag,
|
|
458
|
+
parseOptions,
|
|
459
|
+
readBooleanOption,
|
|
460
|
+
readCommandOptions,
|
|
461
|
+
readFlagOption,
|
|
462
|
+
readIsoDateOption,
|
|
463
|
+
readIsoDateArrayOption,
|
|
464
|
+
readIsoDateTimeOption,
|
|
465
|
+
readIsoDateTimeArrayOption,
|
|
466
|
+
readObjectOption,
|
|
467
|
+
readOption,
|
|
468
|
+
readOptionValues,
|
|
469
|
+
readStringOption,
|
|
470
|
+
readStringArrayOption,
|
|
471
|
+
toKebabCase,
|
|
472
|
+
};
|