not-manage 0.2.0 → 0.2.2

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/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
- Object.entries(query).forEach(([key, value]) => {
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
- if (item !== undefined && item !== null && item !== "") {
147
- url.searchParams.append(key, String(item));
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.set(key, String(value));
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
  };
@@ -1,14 +1,13 @@
1
1
  const { fetchResourceById, fetchResourcePage } = require("./clio-api");
2
2
  const {
3
- clip,
3
+ createDetailPrinter,
4
+ createListPrinter,
5
+ } = require("./resource-display");
6
+ const { buildListQueryFromResource } = require("./resource-query-builder");
7
+ const {
4
8
  compactQuery,
5
9
  fetchPages,
6
- formatBoolean,
7
- formatMoney,
8
- parseLimit,
9
- printKeyValueRows,
10
- readMatterLabel,
11
- readUserName,
10
+ readHours,
12
11
  } = require("./resource-utils");
13
12
  const {
14
13
  buildSummaryMessage,
@@ -21,122 +20,12 @@ const { getResourceMetadata } = require("./resource-metadata");
21
20
  const ACTIVITY_RESOURCE = getResourceMetadata("activities");
22
21
  const MATTER_RESOURCE = getResourceMetadata("matters");
23
22
 
24
- function readHours(activity) {
25
- const quantityInHours = Number(activity?.quantity_in_hours);
26
- if (Number.isFinite(quantityInHours)) {
27
- return quantityInHours.toFixed(2);
28
- }
29
-
30
- const quantity = Number(activity?.quantity);
31
- if (Number.isFinite(quantity)) {
32
- return (quantity / 3600).toFixed(2);
33
- }
34
-
35
- return "-";
36
- }
37
-
38
23
  function buildActivityQuery(options) {
39
- return compactQuery({
40
- activity_description_id: options.activityDescriptionId || undefined,
41
- created_since: options.createdSince || undefined,
42
- end_date: options.endDate || undefined,
43
- fields: options.fields || ACTIVITY_RESOURCE.defaultFields.list,
44
- flat_rate:
45
- options.flatRate === undefined || options.flatRate === null
46
- ? undefined
47
- : Boolean(options.flatRate),
48
- limit: parseLimit(options.limit),
49
- matter_id: options.matterId || undefined,
50
- only_unaccounted_for: options.onlyUnaccountedFor ? true : undefined,
51
- order: options.order || undefined,
52
- page_token: options.pageToken || undefined,
53
- query: options.query || undefined,
54
- start_date: options.startDate || undefined,
55
- status: options.status || undefined,
56
- task_id: options.taskId || undefined,
57
- type: options.type || undefined,
58
- updated_since: options.updatedSince || undefined,
59
- user_id: options.userId || undefined,
60
- });
24
+ return buildListQueryFromResource(ACTIVITY_RESOURCE, options, ACTIVITY_RESOURCE.listQuery);
61
25
  }
62
26
 
63
27
  function formatActivityRow(activity) {
64
- return {
65
- billed: formatBoolean(activity.billed),
66
- date: String(activity.date || "-"),
67
- hours: readHours(activity),
68
- id: String(activity.id || "-"),
69
- matter: readMatterLabel(activity.matter),
70
- note: String(activity.note || "-"),
71
- total: formatMoney(activity.total),
72
- type: String(activity.type || "-"),
73
- };
74
- }
75
-
76
- function printActivityList(rows, options) {
77
- if (rows.length === 0) {
78
- console.log("No activities found for the selected filters.");
79
- return;
80
- }
81
-
82
- const visibleRows = rows.slice(0, 50);
83
- console.log("ID TYPE DATE HOURS TOTAL BILLED MATTER NOTE");
84
- console.log("-------- ----------- ---------- ----- ---------- ------ -------------------- ------------------------------");
85
-
86
- visibleRows.forEach((row) => {
87
- const line = [
88
- clip(row.id, 8).padEnd(8, " "),
89
- clip(row.type, 11).padEnd(11, " "),
90
- clip(row.date, 10).padEnd(10, " "),
91
- clip(row.hours, 5).padEnd(5, " "),
92
- clip(row.total, 10).padEnd(10, " "),
93
- clip(row.billed, 6).padEnd(6, " "),
94
- clip(row.matter, 20).padEnd(20, " "),
95
- clip(row.note, 30),
96
- ].join(" ");
97
-
98
- console.log(line);
99
- });
100
-
101
- if (rows.length > visibleRows.length) {
102
- console.log(`Showing ${visibleRows.length} of ${rows.length} activities. Use --json for full output.`);
103
- }
104
-
105
- if (!options.all && options.nextPageUrl) {
106
- console.log("");
107
- console.log("More results are available.");
108
- if (options.pageTokenSupported === false) {
109
- console.log("Run again with `--all` to fetch every matching activity.");
110
- return;
111
- }
112
- console.log("Run again with `--all` or pass `--page-token` from `--json` output.");
113
- }
114
- }
115
-
116
- function printActivity(activity) {
117
- printKeyValueRows([
118
- ["ID", activity.id],
119
- ["Type", activity.type],
120
- ["Date", activity.date],
121
- ["Hours", readHours(activity)],
122
- ["Price", formatMoney(activity.price)],
123
- ["Total", formatMoney(activity.total)],
124
- ["Billed", formatBoolean(activity.billed)],
125
- ["On Bill", formatBoolean(activity.on_bill)],
126
- ["Non-Billable", formatBoolean(activity.non_billable)],
127
- ["No Charge", formatBoolean(activity.no_charge)],
128
- ["Flat Rate", formatBoolean(activity.flat_rate)],
129
- ["Contingency Fee", formatBoolean(activity.contingency_fee)],
130
- ["User", readUserName(activity.user)],
131
- ["Matter", readMatterLabel(activity.matter)],
132
- ["Activity Description", activity.activity_description?.name],
133
- ["Bill", activity.bill?.number],
134
- ["Bill State", activity.bill?.state],
135
- ["Reference", activity.reference],
136
- ["Note", activity.note],
137
- ["Created", activity.created_at],
138
- ["Updated", activity.updated_at],
139
- ]);
28
+ return ACTIVITY_RESOURCE.display.list.formatRow(activity);
140
29
  }
141
30
 
142
31
  async function fetchMatterIdsForClient(config, accessToken, clientId) {
@@ -302,6 +191,9 @@ function printActivitySummary({ result, rows }) {
302
191
  );
303
192
  }
304
193
 
194
+ const printActivityList = createListPrinter(ACTIVITY_RESOURCE.display.list);
195
+ const printActivity = createDetailPrinter(ACTIVITY_RESOURCE.display.get);
196
+
305
197
  const activitiesList = createListCommand({
306
198
  apiPath: ACTIVITY_RESOURCE.apiPath,
307
199
  buildJsonMeta: buildActivityJsonMeta,
@@ -1,10 +1,9 @@
1
1
  const { fetchResourceById } = require("./clio-api");
2
2
  const {
3
- clip,
4
- compactQuery,
5
- parseLimit,
6
- printKeyValueRows,
7
- } = require("./resource-utils");
3
+ createDetailPrinter,
4
+ createListPrinter,
5
+ } = require("./resource-display");
6
+ const { buildListQueryFromResource } = require("./resource-query-builder");
8
7
  const {
9
8
  createGetCommand,
10
9
  createListCommand,
@@ -17,16 +16,11 @@ const PRACTICE_AREA_RESOURCE = getResourceMetadata("practice-areas");
17
16
  const MATTER_LOOKUP_FIELDS = "id,practice_area{id}";
18
17
 
19
18
  function buildPracticeAreaQuery(options) {
20
- return compactQuery({
21
- code: options.code || undefined,
22
- created_since: options.createdSince || undefined,
23
- fields: options.fields || PRACTICE_AREA_RESOURCE.defaultFields.list,
24
- limit: parseLimit(options.limit),
25
- name: options.name || undefined,
26
- order: options.order || undefined,
27
- page_token: options.pageToken || undefined,
28
- updated_since: options.updatedSince || undefined,
29
- });
19
+ return buildListQueryFromResource(
20
+ PRACTICE_AREA_RESOURCE,
21
+ options,
22
+ PRACTICE_AREA_RESOURCE.listQuery
23
+ );
30
24
  }
31
25
 
32
26
  function matchesTimestampOnOrAfter(value, threshold) {
@@ -120,57 +114,7 @@ async function practiceAreasListForMatter(config, accessToken, options = {}) {
120
114
  }
121
115
 
122
116
  function formatPracticeAreaRow(practiceArea) {
123
- return {
124
- category: String(practiceArea.category || "-"),
125
- code: String(practiceArea.code || "-"),
126
- id: String(practiceArea.id || "-"),
127
- name: String(practiceArea.name || "-"),
128
- };
129
- }
130
-
131
- function printPracticeAreaList(rows, options) {
132
- if (rows.length === 0) {
133
- console.log("No practice areas found for the selected filters.");
134
- return;
135
- }
136
-
137
- const visibleRows = rows.slice(0, 50);
138
- console.log("ID CODE NAME CATEGORY");
139
- console.log("-------- ------------ ---------------------------- ------------------------------");
140
-
141
- visibleRows.forEach((row) => {
142
- const line = [
143
- clip(row.id, 8).padEnd(8, " "),
144
- clip(row.code, 12).padEnd(12, " "),
145
- clip(row.name, 28).padEnd(28, " "),
146
- clip(row.category, 30),
147
- ].join(" ");
148
-
149
- console.log(line);
150
- });
151
-
152
- if (rows.length > visibleRows.length) {
153
- console.log(
154
- `Showing ${visibleRows.length} of ${rows.length} practice areas. Use --json for full output.`
155
- );
156
- }
157
-
158
- if (!options.all && options.nextPageUrl) {
159
- console.log("");
160
- console.log("More results are available.");
161
- console.log("Run again with `--all` or pass `--page-token` from `--json` output.");
162
- }
163
- }
164
-
165
- function printPracticeArea(practiceArea) {
166
- printKeyValueRows([
167
- ["ID", practiceArea.id],
168
- ["Code", practiceArea.code],
169
- ["Name", practiceArea.name],
170
- ["Category", practiceArea.category],
171
- ["Created", practiceArea.created_at],
172
- ["Updated", practiceArea.updated_at],
173
- ]);
117
+ return PRACTICE_AREA_RESOURCE.display.list.formatRow(practiceArea);
174
118
  }
175
119
 
176
120
  async function fetchPracticeAreaListResult({ accessToken, apiPath, config, options, query }) {
@@ -187,6 +131,9 @@ async function fetchPracticeAreaListResult({ accessToken, apiPath, config, optio
187
131
  });
188
132
  }
189
133
 
134
+ const printPracticeAreaList = createListPrinter(PRACTICE_AREA_RESOURCE.display.list);
135
+ const printPracticeArea = createDetailPrinter(PRACTICE_AREA_RESOURCE.display.get);
136
+
190
137
  const practiceAreasList = createListCommand({
191
138
  apiPath: PRACTICE_AREA_RESOURCE.apiPath,
192
139
  buildQuery: buildPracticeAreaQuery,
@@ -1,36 +1,131 @@
1
+ const SET_KEYS = [
2
+ "clientObjectKeys",
3
+ "freeTextFields",
4
+ "labelFields",
5
+ "matterLabelFields",
6
+ "safeIdentityObjectKeys",
7
+ ];
8
+
1
9
  const DEFAULT_POLICY = {
2
10
  clientObjectKeys: new Set(["client", "clients", "contact", "contacts"]),
3
11
  contactLikeResource: false,
4
- freeTextFields: new Set(["description", "memo", "note", "reference", "subject"]),
5
- labelFields: new Set(["display_number", "number", "identifier", "title"]),
12
+ freeTextFields: new Set([
13
+ "body",
14
+ "caption",
15
+ "comment",
16
+ "content",
17
+ "description",
18
+ "detail",
19
+ "display_value",
20
+ "field_value",
21
+ "instructions",
22
+ "location",
23
+ "memo",
24
+ "message",
25
+ "note",
26
+ "primary_detail",
27
+ "reference",
28
+ "secondary_detail",
29
+ "snippet",
30
+ "subject",
31
+ "summary",
32
+ "text",
33
+ "value",
34
+ "value_text",
35
+ ]),
36
+ labelFields: new Set([
37
+ "display_value",
38
+ "display_number",
39
+ "file_name",
40
+ "filename",
41
+ "identifier",
42
+ "name",
43
+ "number",
44
+ "option",
45
+ "secondary_identifier",
46
+ "summary",
47
+ "tertiary_identifier",
48
+ "title",
49
+ ]),
6
50
  matterLabelFields: new Set(["display_number", "number"]),
7
51
  safeIdentityObjectKeys: new Set([
52
+ "author",
8
53
  "user",
9
54
  "assignee",
10
55
  "assigner",
56
+ "calendar_owner",
57
+ "created_by",
58
+ "creator",
11
59
  "responsible_attorney",
12
60
  "responsible_staff",
13
61
  "originating_attorney",
62
+ "updated_by",
14
63
  ]),
15
64
  safeIdentityResource: false,
16
65
  };
17
66
 
18
67
  const RESOURCE_POLICY_OVERRIDES = {
68
+ "calendar-entry": {
69
+ clientObjectKeys: new Set(["attendees"]),
70
+ freeTextFields: new Set(["summary", "location"]),
71
+ labelFields: new Set(["summary"]),
72
+ },
19
73
  "billable-client": {
20
74
  contactLikeResource: true,
21
75
  },
76
+ communication: {
77
+ clientObjectKeys: new Set(["senders", "receivers"]),
78
+ freeTextFields: new Set(["body", "content", "detail", "message"]),
79
+ },
22
80
  contact: {
23
81
  contactLikeResource: true,
24
82
  },
83
+ conversation: {
84
+ freeTextFields: new Set(["body", "content", "message", "snippet"]),
85
+ },
86
+ "conversation-message": {
87
+ clientObjectKeys: new Set(["receivers", "sender"]),
88
+ freeTextFields: new Set(["body", "content", "message", "text"]),
89
+ },
90
+ "custom-field": {
91
+ freeTextFields: new Set(["value", "display_value", "value_text", "field_value"]),
92
+ labelFields: new Set(["name", "title"]),
93
+ },
94
+ document: {
95
+ freeTextFields: new Set(["name", "summary"]),
96
+ labelFields: new Set(["file_name", "filename", "name", "title"]),
97
+ },
98
+ note: {
99
+ freeTextFields: new Set(["detail", "message", "text"]),
100
+ },
101
+ "my-event": {
102
+ freeTextFields: new Set([
103
+ "description",
104
+ "message",
105
+ "primary_detail",
106
+ "secondary_detail",
107
+ "title",
108
+ ]),
109
+ labelFields: new Set(["primary_detail", "secondary_detail", "title"]),
110
+ },
25
111
  user: {
26
112
  safeIdentityResource: true,
27
113
  },
28
114
  };
29
115
 
30
116
  function getRedactionPolicy(resourceType) {
117
+ const overrides = RESOURCE_POLICY_OVERRIDES[resourceType] || {};
118
+
31
119
  return {
32
120
  ...DEFAULT_POLICY,
33
- ...(RESOURCE_POLICY_OVERRIDES[resourceType] || {}),
121
+ ...overrides,
122
+ ...SET_KEYS.reduce((merged, key) => {
123
+ merged[key] = new Set([
124
+ ...(DEFAULT_POLICY[key] || []),
125
+ ...(overrides[key] || []),
126
+ ]);
127
+ return merged;
128
+ }, {}),
34
129
  };
35
130
  }
36
131
 
package/src/redaction.js CHANGED
@@ -400,6 +400,10 @@ function isMatterLabelContext(policy, path, key) {
400
400
  return path[path.length - 1] === "matter" && policy.matterLabelFields.has(key);
401
401
  }
402
402
 
403
+ function isLabelContext(policy, key) {
404
+ return policy.labelFields.has(key);
405
+ }
406
+
403
407
  function redactStringValue(
404
408
  policy,
405
409
  text,
@@ -414,7 +418,7 @@ function redactStringValue(
414
418
  output = replaceKnownSensitiveValues(output, replacements);
415
419
  output = redactPatternPii(output);
416
420
 
417
- if (isMatterLabelContext(policy, path, key)) {
421
+ if (isMatterLabelContext(policy, path, key) || isLabelContext(policy, key)) {
418
422
  output = replaceMatterLabelDerivedNames(output, derivedLabelReplacements);
419
423
  }
420
424
 
@@ -0,0 +1,100 @@
1
+ const { clip, printKeyValueRows } = require("./resource-utils");
2
+
3
+ function resolveDescriptorValue(item, descriptor) {
4
+ if (typeof descriptor.value === "function") {
5
+ return descriptor.value(item);
6
+ }
7
+
8
+ return item?.[descriptor.key];
9
+ }
10
+
11
+ function createListPrinter(config) {
12
+ const {
13
+ columns,
14
+ emptyMessage,
15
+ moreResults,
16
+ noun,
17
+ rowLimit = 50,
18
+ } = config;
19
+
20
+ const headerLine = columns
21
+ .map((column) => {
22
+ if (column.width) {
23
+ return String(column.header).padEnd(column.width, " ");
24
+ }
25
+
26
+ return String(column.header);
27
+ })
28
+ .join(" ");
29
+
30
+ const dividerLine = columns
31
+ .map((column) => "-".repeat(column.width || String(column.header).length))
32
+ .join(" ");
33
+
34
+ return function printList(rows, options = {}) {
35
+ if (rows.length === 0) {
36
+ console.log(emptyMessage);
37
+ return;
38
+ }
39
+
40
+ const visibleRows = rows.slice(0, rowLimit);
41
+ console.log(headerLine);
42
+ console.log(dividerLine);
43
+
44
+ visibleRows.forEach((row) => {
45
+ const line = columns
46
+ .map((column) => {
47
+ const rawValue = row?.[column.key];
48
+ const text = clip(
49
+ rawValue === undefined || rawValue === null ? "-" : String(rawValue),
50
+ column.width || String(rawValue || "-").length
51
+ );
52
+
53
+ if (!column.width || column.pad === false) {
54
+ return text;
55
+ }
56
+
57
+ return text.padEnd(column.width, " ");
58
+ })
59
+ .join(" ");
60
+
61
+ console.log(line);
62
+ });
63
+
64
+ if (rows.length > visibleRows.length) {
65
+ console.log(`Showing ${visibleRows.length} of ${rows.length} ${noun}. Use --json for full output.`);
66
+ }
67
+
68
+ if (!options.all && options.nextPageUrl) {
69
+ const lines =
70
+ typeof moreResults === "function"
71
+ ? moreResults(options)
72
+ : [
73
+ "More results are available.",
74
+ "Run again with `--all` or pass `--page-token` from `--json` output.",
75
+ ];
76
+
77
+ if (Array.isArray(lines) && lines.length > 0) {
78
+ console.log("");
79
+ lines.forEach((line) => {
80
+ console.log(line);
81
+ });
82
+ }
83
+ }
84
+ };
85
+ }
86
+
87
+ function createDetailPrinter(config) {
88
+ const { fields } = config;
89
+
90
+ return function printItem(item = {}) {
91
+ printKeyValueRows(
92
+ fields.map((field) => [field.label, resolveDescriptorValue(item, field)])
93
+ );
94
+ };
95
+ }
96
+
97
+ module.exports = {
98
+ createDetailPrinter,
99
+ createListPrinter,
100
+ };
@@ -0,0 +1,81 @@
1
+ const { activitiesGet, activitiesList } = require("./commands-activities");
2
+ const { practiceAreasGet, practiceAreasList } = require("./commands-practice-areas");
3
+ const {
4
+ createDetailPrinter,
5
+ createListPrinter,
6
+ } = require("./resource-display");
7
+ const { buildListQueryFromResource } = require("./resource-query-builder");
8
+ const { createGetCommand, createListCommand } = require("./resource-command-runner");
9
+
10
+ const RESOURCE_HANDLERS = {
11
+ activities: {
12
+ get: activitiesGet,
13
+ list: activitiesList,
14
+ },
15
+ "practice-areas": {
16
+ get: practiceAreasGet,
17
+ list: practiceAreasList,
18
+ },
19
+ };
20
+
21
+ const GENERIC_HANDLER_CACHE = new Map();
22
+
23
+ function createGenericHandlers(resourceMetadata) {
24
+ if (!resourceMetadata || !resourceMetadata.display) {
25
+ return null;
26
+ }
27
+
28
+ const cached = GENERIC_HANDLER_CACHE.get(resourceMetadata.handlerKey);
29
+ if (cached) {
30
+ return cached;
31
+ }
32
+
33
+ const handlers = {};
34
+
35
+ if (resourceMetadata.capabilities.list.enabled && resourceMetadata.display.list) {
36
+ const printList = createListPrinter(resourceMetadata.display.list);
37
+ handlers.list = createListCommand({
38
+ apiPath: resourceMetadata.apiPath,
39
+ buildQuery: (options) =>
40
+ buildListQueryFromResource(resourceMetadata, options, resourceMetadata.listQuery),
41
+ formatRow: resourceMetadata.display.list.formatRow,
42
+ pluralLabel: resourceMetadata.summaryLabels.plural,
43
+ printList,
44
+ redactionResourceType: resourceMetadata.redaction.resourceType,
45
+ singularLabel: resourceMetadata.summaryLabels.singular,
46
+ });
47
+ }
48
+
49
+ if (resourceMetadata.capabilities.get.enabled && resourceMetadata.display.get) {
50
+ const printItem = createDetailPrinter(resourceMetadata.display.get);
51
+ handlers.get = createGetCommand({
52
+ apiPath: resourceMetadata.apiPath,
53
+ defaultFields: resourceMetadata.defaultFields.get,
54
+ printItem,
55
+ redactionResourceType: resourceMetadata.redaction.resourceType,
56
+ usage: `Usage: not-manage ${resourceMetadata.handlerKey} get <id> [--fields ...] [--json]`,
57
+ });
58
+ }
59
+
60
+ GENERIC_HANDLER_CACHE.set(resourceMetadata.handlerKey, handlers);
61
+ return handlers;
62
+ }
63
+
64
+ function getResourceHandler(resourceMetadata, subcommand) {
65
+ if (!resourceMetadata) {
66
+ return null;
67
+ }
68
+
69
+ const explicitHandler = RESOURCE_HANDLERS[resourceMetadata.handlerKey]?.[subcommand];
70
+ if (explicitHandler) {
71
+ return explicitHandler;
72
+ }
73
+
74
+ const genericHandlers = createGenericHandlers(resourceMetadata);
75
+ return genericHandlers?.[subcommand] || null;
76
+ }
77
+
78
+ module.exports = {
79
+ RESOURCE_HANDLERS,
80
+ getResourceHandler,
81
+ };