not-manage 0.1.17
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/LICENSE +176 -0
- package/NOTICE +5 -0
- package/README.md +227 -0
- package/bin/not-manage.js +9 -0
- package/package.json +55 -0
- package/src/cli.js +668 -0
- package/src/clio-api.js +384 -0
- package/src/commands-activities.js +356 -0
- package/src/commands-auth.js +465 -0
- package/src/commands-billable-clients.js +152 -0
- package/src/commands-billable-matters.js +150 -0
- package/src/commands-bills.js +250 -0
- package/src/commands-contacts.js +179 -0
- package/src/commands-matters.js +214 -0
- package/src/commands-practice-areas.js +249 -0
- package/src/commands-tasks.js +213 -0
- package/src/commands-users.js +192 -0
- package/src/constants.js +50 -0
- package/src/keychain.js +63 -0
- package/src/oauth-callback.js +107 -0
- package/src/open-browser.js +33 -0
- package/src/postinstall.js +140 -0
- package/src/prompt.js +103 -0
- package/src/redaction.js +568 -0
- package/src/redirect-uri.js +53 -0
- package/src/resource-utils.js +141 -0
- package/src/store.js +178 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
function parseLimit(limitInput, max = 200) {
|
|
2
|
+
if (limitInput === undefined || limitInput === null || limitInput === "") {
|
|
3
|
+
return undefined;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const parsed = Number(limitInput);
|
|
7
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > max) {
|
|
8
|
+
throw new Error(`\`--limit\` must be an integer between 1 and ${max}.`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return parsed;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function compactQuery(query) {
|
|
15
|
+
const next = { ...query };
|
|
16
|
+
Object.keys(next).forEach((key) => {
|
|
17
|
+
if (next[key] === undefined || next[key] === null || next[key] === "") {
|
|
18
|
+
delete next[key];
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
return next;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function clip(value, maxLength) {
|
|
25
|
+
const text = String(value || "");
|
|
26
|
+
if (text.length <= maxLength) {
|
|
27
|
+
return text;
|
|
28
|
+
}
|
|
29
|
+
if (maxLength <= 3) {
|
|
30
|
+
return ".".repeat(maxLength);
|
|
31
|
+
}
|
|
32
|
+
return `${text.slice(0, maxLength - 3)}...`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readContactName(contact) {
|
|
36
|
+
if (!contact || typeof contact !== "object") {
|
|
37
|
+
return "-";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
contact.name ||
|
|
42
|
+
[contact.first_name, contact.last_name].filter(Boolean).join(" ").trim() ||
|
|
43
|
+
"-"
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readUserName(user) {
|
|
48
|
+
if (!user || typeof user !== "object") {
|
|
49
|
+
return "-";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
user.name ||
|
|
54
|
+
[user.first_name, user.last_name].filter(Boolean).join(" ").trim() ||
|
|
55
|
+
user.email ||
|
|
56
|
+
"-"
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readMatterLabel(matter) {
|
|
61
|
+
if (!matter || typeof matter !== "object") {
|
|
62
|
+
return "-";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return matter.display_number || matter.number || matter.description || String(matter.id || "-");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function formatBoolean(value) {
|
|
69
|
+
if (value === undefined || value === null) {
|
|
70
|
+
return "-";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return value ? "yes" : "no";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function formatMoney(value) {
|
|
77
|
+
if (value === undefined || value === null || value === "") {
|
|
78
|
+
return "-";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const parsed = Number(value);
|
|
82
|
+
if (!Number.isFinite(parsed)) {
|
|
83
|
+
return String(value);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return parsed.toFixed(2);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function printKeyValueRows(rows) {
|
|
90
|
+
const normalized = rows
|
|
91
|
+
.filter(([label]) => label)
|
|
92
|
+
.map(([label, value]) => [
|
|
93
|
+
label,
|
|
94
|
+
value === undefined || value === null || value === "" ? "-" : String(value),
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
if (normalized.length === 0) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const width = normalized.reduce((max, [label]) => Math.max(max, label.length), 0);
|
|
102
|
+
normalized.forEach(([label, value]) => {
|
|
103
|
+
console.log(`${label.padEnd(width, " ")} : ${value}`);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function fetchPages(fetchPage, initialQuery, fetchAllPages) {
|
|
108
|
+
const firstPage = await fetchPage(initialQuery);
|
|
109
|
+
const firstData = Array.isArray(firstPage?.data) ? firstPage.data : [];
|
|
110
|
+
const aggregatedData = [...firstData];
|
|
111
|
+
let pagesFetched = 1;
|
|
112
|
+
let nextPageUrl = firstPage?.meta?.paging?.next || null;
|
|
113
|
+
|
|
114
|
+
while (fetchAllPages && nextPageUrl) {
|
|
115
|
+
const nextPage = await fetchPage({}, nextPageUrl);
|
|
116
|
+
const nextData = Array.isArray(nextPage?.data) ? nextPage.data : [];
|
|
117
|
+
aggregatedData.push(...nextData);
|
|
118
|
+
pagesFetched += 1;
|
|
119
|
+
nextPageUrl = nextPage?.meta?.paging?.next || null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
firstPage,
|
|
124
|
+
data: fetchAllPages ? aggregatedData : firstData,
|
|
125
|
+
pagesFetched,
|
|
126
|
+
nextPageUrl: fetchAllPages ? null : firstPage?.meta?.paging?.next || null,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = {
|
|
131
|
+
clip,
|
|
132
|
+
compactQuery,
|
|
133
|
+
fetchPages,
|
|
134
|
+
formatBoolean,
|
|
135
|
+
formatMoney,
|
|
136
|
+
parseLimit,
|
|
137
|
+
printKeyValueRows,
|
|
138
|
+
readContactName,
|
|
139
|
+
readMatterLabel,
|
|
140
|
+
readUserName,
|
|
141
|
+
};
|
package/src/store.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
const {
|
|
2
|
+
DEFAULT_REDIRECT_URI,
|
|
3
|
+
DEFAULT_REGION,
|
|
4
|
+
REGIONS,
|
|
5
|
+
} = require("./constants");
|
|
6
|
+
const { deleteSecret, getSecret, setSecret } = require("./keychain");
|
|
7
|
+
const { parseLoopbackRedirectUri } = require("./redirect-uri");
|
|
8
|
+
|
|
9
|
+
const ACCOUNTS = {
|
|
10
|
+
region: "config:region",
|
|
11
|
+
clientId: "config:client_id",
|
|
12
|
+
clientSecret: "config:client_secret",
|
|
13
|
+
redirectUri: "config:redirect_uri",
|
|
14
|
+
tokens: "tokens:default",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function normalizeRegion(regionInput) {
|
|
18
|
+
const code = String(regionInput || "")
|
|
19
|
+
.trim()
|
|
20
|
+
.toLowerCase();
|
|
21
|
+
|
|
22
|
+
if (!REGIONS[code]) {
|
|
23
|
+
const valid = Object.keys(REGIONS).join(", ");
|
|
24
|
+
throw new Error(`Unsupported region "${regionInput}". Use one of: ${valid}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return code;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseRedirectUri(redirectUri) {
|
|
31
|
+
return parseLoopbackRedirectUri(redirectUri).toString();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function buildConfig(region, clientId, clientSecret, redirectUri, source) {
|
|
35
|
+
const regionCode = normalizeRegion(region || DEFAULT_REGION);
|
|
36
|
+
const regionInfo = REGIONS[regionCode];
|
|
37
|
+
|
|
38
|
+
if (!clientId || !clientSecret) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
"Client credentials are missing. Run `not-manage setup` or `not-manage auth setup`."
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
source,
|
|
46
|
+
region: regionCode,
|
|
47
|
+
regionLabel: regionInfo.label,
|
|
48
|
+
host: regionInfo.host,
|
|
49
|
+
clientId,
|
|
50
|
+
clientSecret,
|
|
51
|
+
redirectUri: parseRedirectUri(redirectUri || DEFAULT_REDIRECT_URI),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function getStoredConfig() {
|
|
56
|
+
const [region, clientId, clientSecret, redirectUri] = await Promise.all([
|
|
57
|
+
getSecret(ACCOUNTS.region),
|
|
58
|
+
getSecret(ACCOUNTS.clientId),
|
|
59
|
+
getSecret(ACCOUNTS.clientSecret),
|
|
60
|
+
getSecret(ACCOUNTS.redirectUri),
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
if (!region || !clientId || !clientSecret) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return buildConfig(
|
|
68
|
+
region,
|
|
69
|
+
clientId,
|
|
70
|
+
clientSecret,
|
|
71
|
+
redirectUri || DEFAULT_REDIRECT_URI,
|
|
72
|
+
"keychain"
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function saveConfig(configInput) {
|
|
77
|
+
const config = buildConfig(
|
|
78
|
+
configInput.region,
|
|
79
|
+
configInput.clientId,
|
|
80
|
+
configInput.clientSecret,
|
|
81
|
+
configInput.redirectUri || DEFAULT_REDIRECT_URI,
|
|
82
|
+
"keychain"
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
await Promise.all([
|
|
86
|
+
setSecret(ACCOUNTS.region, config.region),
|
|
87
|
+
setSecret(ACCOUNTS.clientId, config.clientId),
|
|
88
|
+
setSecret(ACCOUNTS.clientSecret, config.clientSecret),
|
|
89
|
+
setSecret(ACCOUNTS.redirectUri, config.redirectUri),
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
return config;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function getConfig() {
|
|
96
|
+
const config = await findConfig();
|
|
97
|
+
if (config) {
|
|
98
|
+
return config;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
throw new Error(
|
|
102
|
+
"Clio app credentials are not configured. Run `not-manage setup`."
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function findConfig() {
|
|
107
|
+
const storedConfig = await getStoredConfig();
|
|
108
|
+
if (storedConfig) {
|
|
109
|
+
return storedConfig;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function normalizeTokenSet(rawTokenSet, previousTokenSet) {
|
|
116
|
+
if (!rawTokenSet || !rawTokenSet.access_token) {
|
|
117
|
+
throw new Error("Clio token response is missing access_token.");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
121
|
+
const expiresIn = Number(rawTokenSet.expires_in || 0);
|
|
122
|
+
const expiresAt = expiresIn > 0 ? nowSeconds + expiresIn : null;
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
source: "keychain",
|
|
126
|
+
tokenType: rawTokenSet.token_type || "Bearer",
|
|
127
|
+
accessToken: rawTokenSet.access_token,
|
|
128
|
+
refreshToken: rawTokenSet.refresh_token || previousTokenSet?.refreshToken || null,
|
|
129
|
+
expiresAt,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function saveTokenSet(tokenSetInput, previousTokenSet = null) {
|
|
134
|
+
const tokenSet = normalizeTokenSet(tokenSetInput, previousTokenSet);
|
|
135
|
+
await setSecret(ACCOUNTS.tokens, JSON.stringify(tokenSet));
|
|
136
|
+
return tokenSet;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function getStoredTokenSet() {
|
|
140
|
+
const raw = await getSecret(ACCOUNTS.tokens);
|
|
141
|
+
if (!raw) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const parsed = JSON.parse(raw);
|
|
147
|
+
return {
|
|
148
|
+
source: "keychain",
|
|
149
|
+
tokenType: parsed.tokenType || "Bearer",
|
|
150
|
+
accessToken: parsed.accessToken || null,
|
|
151
|
+
refreshToken: parsed.refreshToken || null,
|
|
152
|
+
expiresAt: parsed.expiresAt || null,
|
|
153
|
+
};
|
|
154
|
+
} catch (_error) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
"Stored token data is invalid. Run `not-manage auth revoke` then `not-manage auth login`."
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function getTokenSet() {
|
|
162
|
+
return getStoredTokenSet();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function clearTokenSet() {
|
|
166
|
+
await deleteSecret(ACCOUNTS.tokens);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = {
|
|
170
|
+
clearTokenSet,
|
|
171
|
+
findConfig,
|
|
172
|
+
getConfig,
|
|
173
|
+
getTokenSet,
|
|
174
|
+
normalizeRegion,
|
|
175
|
+
parseRedirectUri,
|
|
176
|
+
saveConfig,
|
|
177
|
+
saveTokenSet,
|
|
178
|
+
};
|