not-manage 0.2.2 → 0.2.3

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
@@ -1,8 +1,106 @@
1
1
  const { saveTokenSet } = require("./store");
2
2
 
3
- function createError(message, responseText) {
4
- const suffix = responseText ? ` ${responseText}` : "";
5
- return new Error(`${message}.${suffix}`.trim());
3
+ function sanitizeUrlForError(url) {
4
+ try {
5
+ const parsed = new URL(url);
6
+ if (parsed.search) {
7
+ return `${parsed.origin}${parsed.pathname}?[query redacted]`;
8
+ }
9
+ return `${parsed.origin}${parsed.pathname}`;
10
+ } catch (_error) {
11
+ return "[invalid URL]";
12
+ }
13
+ }
14
+
15
+ function toSafeErrorToken(value) {
16
+ const text = String(value || "").trim();
17
+ if (!text || !/^[a-z0-9_.:-]+$/i.test(text)) {
18
+ return null;
19
+ }
20
+
21
+ return text;
22
+ }
23
+
24
+ function extractErrorCodes(payload) {
25
+ if (!isPlainObject(payload)) {
26
+ return [];
27
+ }
28
+
29
+ const codes = [];
30
+
31
+ ["error", "code", "type", "error_code"].forEach((key) => {
32
+ const safeToken = toSafeErrorToken(payload[key]);
33
+ if (safeToken) {
34
+ codes.push(safeToken);
35
+ }
36
+ });
37
+
38
+ if (Array.isArray(payload.errors)) {
39
+ payload.errors.forEach((item) => {
40
+ if (!isPlainObject(item)) {
41
+ return;
42
+ }
43
+
44
+ ["error", "code", "type", "error_code"].forEach((key) => {
45
+ const safeToken = toSafeErrorToken(item[key]);
46
+ if (safeToken) {
47
+ codes.push(safeToken);
48
+ }
49
+ });
50
+ });
51
+ }
52
+
53
+ return [...new Set(codes)];
54
+ }
55
+
56
+ function summarizeErrorPayload(payload) {
57
+ if (payload === null || payload === undefined || payload === "") {
58
+ return "";
59
+ }
60
+
61
+ const errorCodes = extractErrorCodes(payload);
62
+ if (errorCodes.length === 1) {
63
+ return `Clio error code: ${errorCodes[0]}.`;
64
+ }
65
+
66
+ if (errorCodes.length > 1) {
67
+ return `Clio error codes: ${errorCodes.join(", ")}.`;
68
+ }
69
+
70
+ if (typeof payload === "string") {
71
+ return "Response body omitted.";
72
+ }
73
+
74
+ if (Array.isArray(payload)) {
75
+ const noun = payload.length === 1 ? "item" : "items";
76
+ return `Clio returned ${payload.length} error ${noun}.`;
77
+ }
78
+
79
+ if (isPlainObject(payload)) {
80
+ if (Array.isArray(payload.errors)) {
81
+ const count = payload.errors.length;
82
+ const noun = count === 1 ? "item" : "items";
83
+ return `Clio returned ${count} error ${noun}.`;
84
+ }
85
+
86
+ return "Response body omitted.";
87
+ }
88
+
89
+ return "";
90
+ }
91
+
92
+ function createError(message, payload) {
93
+ const sanitizedMessage = message.replace(
94
+ /https?:\/\/\S+/g,
95
+ (match) => sanitizeUrlForError(match)
96
+ );
97
+ const summary = summarizeErrorPayload(payload);
98
+ const error = new Error(summary ? `${sanitizedMessage}. ${summary}` : `${sanitizedMessage}.`);
99
+ const [clioErrorCode] = extractErrorCodes(payload);
100
+ if (clioErrorCode) {
101
+ error.clioErrorCode = clioErrorCode;
102
+ }
103
+ return error;
6
104
  }
7
105
 
8
106
  function isPlainObject(value) {
@@ -33,7 +131,7 @@ async function postForm(url, formFields, headers = {}) {
33
131
  if (!response.ok) {
34
132
  throw createError(
35
133
  `HTTP ${response.status} from ${url}`,
36
- typeof payload === "string" ? payload : JSON.stringify(payload)
134
+ payload
37
135
  );
38
136
  }
39
137
 
@@ -59,7 +157,7 @@ async function getJson(url, headers = {}) {
59
157
  if (!response.ok) {
60
158
  throw createError(
61
159
  `HTTP ${response.status} from ${url}`,
62
- typeof payload === "string" ? payload : JSON.stringify(payload)
160
+ payload
63
161
  );
64
162
  }
65
163
 
@@ -91,7 +189,7 @@ async function postJson(url, body, headers = {}) {
91
189
  if (!response.ok) {
92
190
  throw createError(
93
191
  `HTTP ${response.status} from ${url}`,
94
- typeof payload === "string" ? payload : JSON.stringify(payload)
192
+ payload
95
193
  );
96
194
  }
97
195
 
@@ -411,6 +509,10 @@ module.exports = {
411
509
  getValidAccessToken,
412
510
  __private: {
413
511
  buildUrlWithQuery,
512
+ createError,
513
+ extractErrorCodes,
414
514
  parseTrustedApiUrl,
515
+ sanitizeUrlForError,
516
+ summarizeErrorPayload,
415
517
  },
416
518
  };
@@ -0,0 +1,119 @@
1
+ const { RESOURCE_ORDER, getResourceMetadata } = require("./resource-metadata");
2
+ const { version } = require("../package.json");
3
+
4
+ const SCHEMA_VERSION = 1;
5
+
6
+ function buildGlobalCommands() {
7
+ return [
8
+ {
9
+ name: "setup",
10
+ use: "not-manage setup",
11
+ description: "Run guided setup and OAuth login.",
12
+ },
13
+ {
14
+ name: "doctor",
15
+ use: "not-manage doctor [--json] [--fail-on warn|error]",
16
+ description: "Report local configuration, auth, and Clio connectivity status.",
17
+ readOnly: true,
18
+ },
19
+ {
20
+ name: "agent-context",
21
+ use: "not-manage agent-context [--json]",
22
+ description: "Emit this machine-readable command and resource catalog.",
23
+ readOnly: true,
24
+ },
25
+ {
26
+ name: "auth",
27
+ use: "not-manage auth <setup|login|status|revoke>",
28
+ description: "Manage local OAuth configuration and token state.",
29
+ },
30
+ {
31
+ name: "whoami",
32
+ use: "not-manage whoami [--json]",
33
+ description: "Call Clio's current-user endpoint.",
34
+ readOnly: true,
35
+ },
36
+ {
37
+ name: "request",
38
+ use: "not-manage request <method> <path> [--query k=v] [--write] [--dry-run] [--json]",
39
+ description: "Raw API escape hatch that reuses saved auth and redaction.",
40
+ },
41
+ ];
42
+ }
43
+
44
+ function buildResourceCommand(command) {
45
+ const metadata = getResourceMetadata(command);
46
+ const subcommands = ["list", "get"]
47
+ .filter((subcommand) => metadata.capabilities[subcommand].enabled)
48
+ .map((subcommand) => ({
49
+ name: subcommand,
50
+ use:
51
+ subcommand === "get"
52
+ ? `not-manage ${command} get <id> [options]`
53
+ : `not-manage ${command} list [options]`,
54
+ description: metadata.help[subcommand],
55
+ readOnly: true,
56
+ requiredOptions: metadata.capabilities[subcommand].requiredOptions || [],
57
+ options: Object.values(metadata.optionSchema?.[subcommand] || {})
58
+ .filter((option) => option.positional === undefined)
59
+ .map((option) => ({
60
+ name: `--${option.option}`,
61
+ kind: option.kind || "string",
62
+ }))
63
+ .concat(
64
+ [
65
+ { name: "--options-file", kind: "string" },
66
+ subcommand === "get" ? { name: "--ids-file", kind: "string" } : null,
67
+ subcommand === "get" ? { name: "--stdin", kind: "flag" } : null,
68
+ ].filter(Boolean)
69
+ ),
70
+ }));
71
+
72
+ return {
73
+ name: command,
74
+ aliases: metadata.aliases,
75
+ subcommands,
76
+ };
77
+ }
78
+
79
+ async function agentContext(options = {}) {
80
+ const payload = {
81
+ schemaVersion: SCHEMA_VERSION,
82
+ cli: {
83
+ name: "not-manage",
84
+ version,
85
+ description: "Unofficial command-line tool for Clio Manage integrations.",
86
+ },
87
+ defaults: {
88
+ dataCommandsRedacted: true,
89
+ agentFlagImplies: ["--json", "--compact", "--no-input", "--no-color", "--yes"],
90
+ },
91
+ auth: {
92
+ mode: "oauth2_loopback",
93
+ credentialStorage: "os_keychain",
94
+ tokenStorage: "os_keychain",
95
+ },
96
+ globalOptions: [
97
+ "--agent",
98
+ "--json",
99
+ "--compact",
100
+ "--no-input",
101
+ "--no-color",
102
+ "--yes",
103
+ "--force",
104
+ "--redacted",
105
+ "--unredacted",
106
+ ],
107
+ commands: buildGlobalCommands(),
108
+ resources: RESOURCE_ORDER.map(buildResourceCommand),
109
+ };
110
+
111
+ console.log(JSON.stringify(payload, null, options.compact ? 0 : 2));
112
+ }
113
+
114
+ module.exports = {
115
+ agentContext,
116
+ __private: {
117
+ buildResourceCommand,
118
+ },
119
+ };