not-manage 0.2.1 → 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/README.md +48 -12
- package/bin/not-manage.js +15 -2
- package/package.json +1 -2
- package/src/agent-input.js +101 -0
- package/src/cli-errors.js +109 -0
- package/src/cli.js +835 -64
- package/src/clio-api.js +108 -6
- package/src/commands-agent-context.js +119 -0
- package/src/commands-auth.js +336 -148
- package/src/commands-doctor.js +178 -0
- package/src/commands-request.js +144 -0
- package/src/compact-output.js +89 -0
- package/src/prompt.js +48 -0
- package/src/redaction.js +62 -25
- package/src/resource-command-runner.js +75 -4
- package/src/resource-handlers.js +15 -6
- package/src/resource-metadata.js +18 -0
- package/src/store.js +1 -1
- package/src/postinstall.js +0 -140
package/src/clio-api.js
CHANGED
|
@@ -1,8 +1,106 @@
|
|
|
1
1
|
const { saveTokenSet } = require("./store");
|
|
2
2
|
|
|
3
|
-
function
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
};
|