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,192 @@
|
|
|
1
|
+
const {
|
|
2
|
+
fetchUser,
|
|
3
|
+
fetchUsersPage,
|
|
4
|
+
getValidAccessToken,
|
|
5
|
+
} = require("./clio-api");
|
|
6
|
+
const { getConfig, getTokenSet } = require("./store");
|
|
7
|
+
const {
|
|
8
|
+
clip,
|
|
9
|
+
compactQuery,
|
|
10
|
+
fetchPages,
|
|
11
|
+
formatBoolean,
|
|
12
|
+
parseLimit,
|
|
13
|
+
printKeyValueRows,
|
|
14
|
+
readUserName,
|
|
15
|
+
} = require("./resource-utils");
|
|
16
|
+
const { maybeRedactData, maybeRedactPayload } = require("./redaction");
|
|
17
|
+
|
|
18
|
+
const DEFAULT_LIST_FIELDS =
|
|
19
|
+
"id,name,first_name,last_name,email,enabled,roles,subscription_type,phone_number,time_zone,rate,account_owner,clio_connect,court_rules_default_attendee,created_at,updated_at";
|
|
20
|
+
const DEFAULT_GET_FIELDS =
|
|
21
|
+
"id,name,first_name,last_name,email,enabled,roles,subscription_type,phone_number,time_zone,rate,account_owner,clio_connect,court_rules_default_attendee,created_at,updated_at";
|
|
22
|
+
|
|
23
|
+
function readRoleList(user) {
|
|
24
|
+
const roles = Array.isArray(user?.roles) ? user.roles : [];
|
|
25
|
+
if (roles.length === 0) {
|
|
26
|
+
return "-";
|
|
27
|
+
}
|
|
28
|
+
return roles.join(", ");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function buildUserQuery(options) {
|
|
32
|
+
return compactQuery({
|
|
33
|
+
created_since: options.createdSince || undefined,
|
|
34
|
+
enabled:
|
|
35
|
+
options.enabled === undefined || options.enabled === null
|
|
36
|
+
? undefined
|
|
37
|
+
: Boolean(options.enabled),
|
|
38
|
+
fields: options.fields || DEFAULT_LIST_FIELDS,
|
|
39
|
+
include_co_counsel: options.includeCoCounsel ? true : undefined,
|
|
40
|
+
limit: parseLimit(options.limit, 2000),
|
|
41
|
+
name: options.name || undefined,
|
|
42
|
+
order: options.order || undefined,
|
|
43
|
+
page_token: options.pageToken || undefined,
|
|
44
|
+
pending_setup:
|
|
45
|
+
options.pendingSetup === undefined || options.pendingSetup === null
|
|
46
|
+
? undefined
|
|
47
|
+
: Boolean(options.pendingSetup),
|
|
48
|
+
role: options.role || undefined,
|
|
49
|
+
subscription_type: options.subscriptionType || undefined,
|
|
50
|
+
updated_since: options.updatedSince || undefined,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function formatUserRow(user) {
|
|
55
|
+
return {
|
|
56
|
+
id: String(user.id || "-"),
|
|
57
|
+
name: readUserName(user),
|
|
58
|
+
email: String(user.email || "-"),
|
|
59
|
+
enabled: formatBoolean(user.enabled),
|
|
60
|
+
roles: readRoleList(user),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function printUserList(rows, options) {
|
|
65
|
+
if (rows.length === 0) {
|
|
66
|
+
console.log("No users found for the selected filters.");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const visibleRows = rows.slice(0, 50);
|
|
71
|
+
console.log("ID NAME EMAIL ENABLED ROLES");
|
|
72
|
+
console.log("-------- ---------------------------- ---------------------------- ------- ------------------------------");
|
|
73
|
+
|
|
74
|
+
visibleRows.forEach((row) => {
|
|
75
|
+
const line = [
|
|
76
|
+
clip(row.id, 8).padEnd(8, " "),
|
|
77
|
+
clip(row.name, 28).padEnd(28, " "),
|
|
78
|
+
clip(row.email, 28).padEnd(28, " "),
|
|
79
|
+
clip(row.enabled, 7).padEnd(7, " "),
|
|
80
|
+
clip(row.roles, 30),
|
|
81
|
+
].join(" ");
|
|
82
|
+
|
|
83
|
+
console.log(line);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (rows.length > visibleRows.length) {
|
|
87
|
+
console.log(`Showing ${visibleRows.length} of ${rows.length} users. Use --json for full output.`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!options.all && options.nextPageUrl) {
|
|
91
|
+
console.log("");
|
|
92
|
+
console.log("More results are available.");
|
|
93
|
+
console.log("Run again with `--all` or pass `--page-token` from `--json` output.");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function printUser(user) {
|
|
98
|
+
printKeyValueRows([
|
|
99
|
+
["ID", user.id],
|
|
100
|
+
["Name", readUserName(user)],
|
|
101
|
+
["Email", user.email],
|
|
102
|
+
["Enabled", formatBoolean(user.enabled)],
|
|
103
|
+
["Roles", readRoleList(user)],
|
|
104
|
+
["Subscription", user.subscription_type],
|
|
105
|
+
["Phone", user.phone_number],
|
|
106
|
+
["Time Zone", user.time_zone],
|
|
107
|
+
["Rate", user.rate],
|
|
108
|
+
["Account Owner", formatBoolean(user.account_owner)],
|
|
109
|
+
["Clio Connect", formatBoolean(user.clio_connect)],
|
|
110
|
+
["Court Rules Default Attendee", formatBoolean(user.court_rules_default_attendee)],
|
|
111
|
+
["Created", user.created_at],
|
|
112
|
+
["Updated", user.updated_at],
|
|
113
|
+
]);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function getAuthContext() {
|
|
117
|
+
const config = await getConfig();
|
|
118
|
+
const tokenSet = await getTokenSet();
|
|
119
|
+
const accessToken = await getValidAccessToken(config, tokenSet);
|
|
120
|
+
return { config, accessToken };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function usersList(options = {}) {
|
|
124
|
+
const query = buildUserQuery(options);
|
|
125
|
+
const { config, accessToken } = await getAuthContext();
|
|
126
|
+
const result = await fetchPages(
|
|
127
|
+
(pageQuery, nextPageUrl) => fetchUsersPage(config, accessToken, pageQuery, nextPageUrl),
|
|
128
|
+
query,
|
|
129
|
+
Boolean(options.all)
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
if (options.json) {
|
|
133
|
+
const firstPage = maybeRedactPayload(result.firstPage, options, "user");
|
|
134
|
+
if (!options.all) {
|
|
135
|
+
console.log(JSON.stringify(firstPage, null, 2));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const data = maybeRedactData(result.data, options, "user");
|
|
140
|
+
console.log(
|
|
141
|
+
JSON.stringify(
|
|
142
|
+
{
|
|
143
|
+
data,
|
|
144
|
+
meta: {
|
|
145
|
+
pages_fetched: result.pagesFetched,
|
|
146
|
+
returned_count: data.length,
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
null,
|
|
150
|
+
2
|
|
151
|
+
)
|
|
152
|
+
);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const rows = maybeRedactData(result.data, options, "user").map(formatUserRow);
|
|
157
|
+
printUserList(rows, { all: Boolean(options.all), nextPageUrl: result.nextPageUrl });
|
|
158
|
+
console.log("");
|
|
159
|
+
console.log(
|
|
160
|
+
`Returned ${rows.length} user${rows.length === 1 ? "" : "s"} across ${result.pagesFetched} page${result.pagesFetched === 1 ? "" : "s"}.`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function usersGet(options = {}) {
|
|
165
|
+
if (!options.id) {
|
|
166
|
+
throw new Error("Usage: not-manage users get <id> [--fields ...] [--json]");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const { config, accessToken } = await getAuthContext();
|
|
170
|
+
const payload = await fetchUser(config, accessToken, options.id, {
|
|
171
|
+
fields: options.fields || DEFAULT_GET_FIELDS,
|
|
172
|
+
});
|
|
173
|
+
const redactedPayload = maybeRedactPayload(payload, options, "user");
|
|
174
|
+
|
|
175
|
+
if (options.json) {
|
|
176
|
+
console.log(JSON.stringify(redactedPayload, null, 2));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
printUser(redactedPayload?.data || {});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = {
|
|
184
|
+
usersGet,
|
|
185
|
+
usersList,
|
|
186
|
+
__private: {
|
|
187
|
+
buildUserQuery,
|
|
188
|
+
formatUserRow,
|
|
189
|
+
printUser,
|
|
190
|
+
printUserList,
|
|
191
|
+
},
|
|
192
|
+
};
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const SERVICE_NAME = "com.notoperations.not-manage-cli";
|
|
2
|
+
const LEGACY_SERVICE_NAME = "com.notoperations.clio-manage-cli";
|
|
3
|
+
const DEFAULT_REGION = "us";
|
|
4
|
+
const DEFAULT_REDIRECT_URI = "http://127.0.0.1:53123/callback";
|
|
5
|
+
const OAUTH_TIMEOUT_MS = 5 * 60 * 1000;
|
|
6
|
+
const CLIO_APP_CREATION_GUIDE_URL =
|
|
7
|
+
"https://docs.developers.clio.com/api-docs/clio-manage/applications/";
|
|
8
|
+
const CLIO_AUTHORIZATION_GUIDE_URL =
|
|
9
|
+
"https://docs.developers.clio.com/api-docs/clio-manage/authorization/";
|
|
10
|
+
const CLIO_DEVELOPER_ACCOUNT_GUIDE_URL =
|
|
11
|
+
"https://docs.developers.clio.com/handbook/getting-started/get-a-developer-account/";
|
|
12
|
+
|
|
13
|
+
const REGIONS = {
|
|
14
|
+
us: {
|
|
15
|
+
code: "us",
|
|
16
|
+
label: "United States",
|
|
17
|
+
developerPortalUrl: "https://developers.clio.com",
|
|
18
|
+
host: "app.clio.com",
|
|
19
|
+
},
|
|
20
|
+
ca: {
|
|
21
|
+
code: "ca",
|
|
22
|
+
label: "Canada",
|
|
23
|
+
developerPortalUrl: "https://ca.developers.clio.com",
|
|
24
|
+
host: "ca.app.clio.com",
|
|
25
|
+
},
|
|
26
|
+
eu: {
|
|
27
|
+
code: "eu",
|
|
28
|
+
label: "Europe",
|
|
29
|
+
developerPortalUrl: "https://eu.developers.clio.com",
|
|
30
|
+
host: "eu.app.clio.com",
|
|
31
|
+
},
|
|
32
|
+
au: {
|
|
33
|
+
code: "au",
|
|
34
|
+
label: "Australia",
|
|
35
|
+
developerPortalUrl: "https://au.developers.clio.com",
|
|
36
|
+
host: "au.app.clio.com",
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
module.exports = {
|
|
41
|
+
SERVICE_NAME,
|
|
42
|
+
LEGACY_SERVICE_NAME,
|
|
43
|
+
DEFAULT_REGION,
|
|
44
|
+
DEFAULT_REDIRECT_URI,
|
|
45
|
+
OAUTH_TIMEOUT_MS,
|
|
46
|
+
CLIO_APP_CREATION_GUIDE_URL,
|
|
47
|
+
CLIO_AUTHORIZATION_GUIDE_URL,
|
|
48
|
+
CLIO_DEVELOPER_ACCOUNT_GUIDE_URL,
|
|
49
|
+
REGIONS,
|
|
50
|
+
};
|
package/src/keychain.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const { LEGACY_SERVICE_NAME, SERVICE_NAME } = require("./constants");
|
|
2
|
+
|
|
3
|
+
function loadKeytar() {
|
|
4
|
+
try {
|
|
5
|
+
return require("keytar");
|
|
6
|
+
} catch (error) {
|
|
7
|
+
throw new Error(
|
|
8
|
+
"Secure keychain is unavailable. Install dependencies with `npm install` and ensure your OS keychain is enabled."
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalizeKeychainError(action, error) {
|
|
14
|
+
const rawMessage = error && error.message ? error.message : String(error || "");
|
|
15
|
+
const message = rawMessage && rawMessage !== "[object Object]" ? rawMessage : "unknown error";
|
|
16
|
+
return new Error(
|
|
17
|
+
`OS keychain ${action} failed (${message}). This CLI requires a working OS keychain.`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function setSecret(account, value) {
|
|
22
|
+
try {
|
|
23
|
+
const keytar = loadKeytar();
|
|
24
|
+
await keytar.setPassword(SERVICE_NAME, account, value);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
throw normalizeKeychainError("write", error);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function getSecret(account) {
|
|
31
|
+
try {
|
|
32
|
+
const keytar = loadKeytar();
|
|
33
|
+
// Read the renamed service first, then fall back to the legacy one.
|
|
34
|
+
for (const serviceName of [SERVICE_NAME, LEGACY_SERVICE_NAME]) {
|
|
35
|
+
const value = await keytar.getPassword(serviceName, account);
|
|
36
|
+
if (value) {
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw normalizeKeychainError("read", error);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function deleteSecret(account) {
|
|
47
|
+
try {
|
|
48
|
+
const keytar = loadKeytar();
|
|
49
|
+
await Promise.all(
|
|
50
|
+
[SERVICE_NAME, LEGACY_SERVICE_NAME].map((serviceName) =>
|
|
51
|
+
keytar.deletePassword(serviceName, account)
|
|
52
|
+
)
|
|
53
|
+
);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
throw normalizeKeychainError("delete", error);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = {
|
|
60
|
+
setSecret,
|
|
61
|
+
getSecret,
|
|
62
|
+
deleteSecret,
|
|
63
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const http = require("node:http");
|
|
2
|
+
const crypto = require("node:crypto");
|
|
3
|
+
const { OAUTH_TIMEOUT_MS } = require("./constants");
|
|
4
|
+
const {
|
|
5
|
+
getLoopbackBindHost,
|
|
6
|
+
parseLoopbackRedirectUri,
|
|
7
|
+
} = require("./redirect-uri");
|
|
8
|
+
|
|
9
|
+
function writeTextResponse(res, statusCode, body, contentType = "text/plain; charset=utf-8") {
|
|
10
|
+
res.statusCode = statusCode;
|
|
11
|
+
res.setHeader("cache-control", "no-store, max-age=0");
|
|
12
|
+
res.setHeader("pragma", "no-cache");
|
|
13
|
+
res.setHeader("x-content-type-options", "nosniff");
|
|
14
|
+
res.setHeader("content-type", contentType);
|
|
15
|
+
res.end(body);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function stateMatches(expectedState, state) {
|
|
19
|
+
if (!expectedState || !state) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const expected = Buffer.from(String(expectedState));
|
|
24
|
+
const actual = Buffer.from(String(state));
|
|
25
|
+
if (expected.length !== actual.length) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return crypto.timingSafeEqual(expected, actual);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function waitForOAuthCallback(redirectUri, expectedState) {
|
|
33
|
+
const redirect = parseLoopbackRedirectUri(redirectUri);
|
|
34
|
+
const hostname = getLoopbackBindHost(redirect.hostname);
|
|
35
|
+
const port = Number(redirect.port);
|
|
36
|
+
const path = redirect.pathname || "/";
|
|
37
|
+
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const timeoutId = setTimeout(() => {
|
|
40
|
+
server.close();
|
|
41
|
+
reject(new Error("Timed out waiting for OAuth callback."));
|
|
42
|
+
}, OAUTH_TIMEOUT_MS);
|
|
43
|
+
|
|
44
|
+
const server = http.createServer((req, res) => {
|
|
45
|
+
const reqUrl = new URL(req.url || "/", `http://${hostname}:${port}`);
|
|
46
|
+
if (reqUrl.pathname !== path) {
|
|
47
|
+
writeTextResponse(res, 404, "Not found");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const state = reqUrl.searchParams.get("state");
|
|
52
|
+
const code = reqUrl.searchParams.get("code");
|
|
53
|
+
const error = reqUrl.searchParams.get("error");
|
|
54
|
+
const errorDescription = reqUrl.searchParams.get("error_description");
|
|
55
|
+
|
|
56
|
+
if (error) {
|
|
57
|
+
writeTextResponse(res, 400, "Clio authorization failed. You can close this window.");
|
|
58
|
+
clearTimeout(timeoutId);
|
|
59
|
+
server.close();
|
|
60
|
+
reject(
|
|
61
|
+
new Error(
|
|
62
|
+
`Authorization failed: ${error}${errorDescription ? ` (${errorDescription})` : ""}`
|
|
63
|
+
)
|
|
64
|
+
);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!code) {
|
|
69
|
+
writeTextResponse(res, 400, "Missing authorization code. You can close this window.");
|
|
70
|
+
clearTimeout(timeoutId);
|
|
71
|
+
server.close();
|
|
72
|
+
reject(new Error("OAuth callback did not include an authorization code."));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!stateMatches(expectedState, state)) {
|
|
77
|
+
writeTextResponse(res, 400, "Invalid state. You can close this window.");
|
|
78
|
+
clearTimeout(timeoutId);
|
|
79
|
+
server.close();
|
|
80
|
+
reject(new Error("State validation failed for OAuth callback."));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
writeTextResponse(
|
|
85
|
+
res,
|
|
86
|
+
200,
|
|
87
|
+
"<html><body><h3>Clio auth complete</h3><p>You can close this tab and return to the terminal.</p></body></html>",
|
|
88
|
+
"text/html; charset=utf-8"
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
clearTimeout(timeoutId);
|
|
92
|
+
server.close();
|
|
93
|
+
resolve({ code, state });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
server.on("error", (error) => {
|
|
97
|
+
clearTimeout(timeoutId);
|
|
98
|
+
reject(error);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
server.listen(port, hostname);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = {
|
|
106
|
+
waitForOAuthCallback,
|
|
107
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const { spawn } = require("node:child_process");
|
|
2
|
+
|
|
3
|
+
function openBrowser(url) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
let command = "";
|
|
6
|
+
let args = [];
|
|
7
|
+
|
|
8
|
+
if (process.platform === "darwin") {
|
|
9
|
+
command = "open";
|
|
10
|
+
args = [url];
|
|
11
|
+
} else if (process.platform === "win32") {
|
|
12
|
+
command = "cmd";
|
|
13
|
+
args = ["/c", "start", "", url];
|
|
14
|
+
} else {
|
|
15
|
+
command = "xdg-open";
|
|
16
|
+
args = [url];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const child = spawn(command, args, {
|
|
20
|
+
stdio: "ignore",
|
|
21
|
+
detached: true,
|
|
22
|
+
windowsHide: true,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
child.on("error", reject);
|
|
26
|
+
child.unref();
|
|
27
|
+
resolve();
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
openBrowser,
|
|
33
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
const { setupWizard } = require("./commands-auth");
|
|
2
|
+
const { ask, withPrompt } = require("./prompt");
|
|
3
|
+
const { findConfig } = require("./store");
|
|
4
|
+
|
|
5
|
+
const SKIP_POSTINSTALL_ENV_VARS = [
|
|
6
|
+
"NOT_MANAGE_SKIP_POSTINSTALL_SETUP",
|
|
7
|
+
"CLIO_MANAGE_SKIP_POSTINSTALL_SETUP",
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
function printConfidentialityNotice(log = console.log) {
|
|
11
|
+
log("Confidentiality notice:");
|
|
12
|
+
log(" not-manage can display client-identifying, confidential, or privileged matter data.");
|
|
13
|
+
log(" `--redacted` is best-effort only and may miss identifiers in labels, custom fields, or free text.");
|
|
14
|
+
log(" Review all output before sharing it with AI tools, tickets, chats, or other third parties.");
|
|
15
|
+
log(" Use only with workflows and vendors your firm has approved.");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function shouldShowPostinstallNotice(options = {}) {
|
|
19
|
+
const env = options.env || process.env;
|
|
20
|
+
|
|
21
|
+
if (SKIP_POSTINSTALL_ENV_VARS.some((name) => env[name] === "1")) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (env.CI) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (env.npm_config_global !== "true") {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function shouldRunPostinstallOnboarding(options = {}) {
|
|
37
|
+
const stdin = options.stdin || process.stdin;
|
|
38
|
+
const stdout = options.stdout || process.stdout;
|
|
39
|
+
|
|
40
|
+
if (!shouldShowPostinstallNotice(options)) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!stdin.isTTY || !stdout.isTTY) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function printPostinstallIntro(log = console.log) {
|
|
52
|
+
log("");
|
|
53
|
+
log("+===========================================+");
|
|
54
|
+
log("| NOT MANAGE IS INSTALLED |");
|
|
55
|
+
log("+===========================================+");
|
|
56
|
+
log("| Start first-time setup from npm? |");
|
|
57
|
+
log("+===========================================+");
|
|
58
|
+
log("");
|
|
59
|
+
log("This prompt only appears on fresh interactive global installs.");
|
|
60
|
+
log("If you skip it now, run `not-manage setup` whenever you are ready.");
|
|
61
|
+
log("");
|
|
62
|
+
printConfidentialityNotice(log);
|
|
63
|
+
log("");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function printPostinstallInstalledNotice(log = console.log) {
|
|
67
|
+
log("");
|
|
68
|
+
log("not-manage is installed.");
|
|
69
|
+
printConfidentialityNotice(log);
|
|
70
|
+
log("Run `not-manage setup` whenever you are ready.");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function printPostinstallWelcomeBack(log = console.log) {
|
|
74
|
+
log("");
|
|
75
|
+
log("Welcome back. Clio is already configured on this machine.");
|
|
76
|
+
log("Run `not-manage auth status` to verify the current connection, or `not-manage setup` to reconfigure.");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function maybeRunPostinstallOnboarding(options = {}) {
|
|
80
|
+
const log = options.log || console.log;
|
|
81
|
+
const findConfigFn = options.findConfig || findConfig;
|
|
82
|
+
const setupWizardFn = options.setupWizard || setupWizard;
|
|
83
|
+
const withPromptFn = options.withPrompt || withPrompt;
|
|
84
|
+
const askFn = options.ask || ask;
|
|
85
|
+
|
|
86
|
+
if (!shouldShowPostinstallNotice(options)) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const config = await findConfigFn();
|
|
91
|
+
if (config) {
|
|
92
|
+
printPostinstallWelcomeBack(log);
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!shouldRunPostinstallOnboarding(options)) {
|
|
97
|
+
printPostinstallInstalledNotice(log);
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
printPostinstallIntro(log);
|
|
102
|
+
|
|
103
|
+
const answer = String(
|
|
104
|
+
await withPromptFn((rl) => askFn(rl, "Start guided Clio setup now", "yes"))
|
|
105
|
+
)
|
|
106
|
+
.trim()
|
|
107
|
+
.toLowerCase();
|
|
108
|
+
|
|
109
|
+
if (["n", "no", "skip"].includes(answer)) {
|
|
110
|
+
log("Skipping setup for now. Run `not-manage setup` when you are ready.");
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await setupWizardFn();
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function main(options = {}) {
|
|
119
|
+
const log = options.log || console.log;
|
|
120
|
+
const errorLog = options.errorLog || console.error;
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
await maybeRunPostinstallOnboarding(options);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
errorLog(`Post-install setup was skipped: ${error.message}`);
|
|
126
|
+
log("Run `not-manage setup` when you are ready.");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (require.main === module) {
|
|
131
|
+
main();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = {
|
|
135
|
+
main,
|
|
136
|
+
maybeRunPostinstallOnboarding,
|
|
137
|
+
printConfidentialityNotice,
|
|
138
|
+
shouldShowPostinstallNotice,
|
|
139
|
+
shouldRunPostinstallOnboarding,
|
|
140
|
+
};
|
package/src/prompt.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const { Writable } = require("node:stream");
|
|
2
|
+
const { createInterface } = require("node:readline/promises");
|
|
3
|
+
const { stdin, stdout } = require("node:process");
|
|
4
|
+
|
|
5
|
+
class PromptOutput extends Writable {
|
|
6
|
+
constructor(target) {
|
|
7
|
+
super();
|
|
8
|
+
this.target = target;
|
|
9
|
+
this.muted = false;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
_write(chunk, encoding, callback) {
|
|
13
|
+
if (!this.muted) {
|
|
14
|
+
writePromptChunk(this.target, chunk, encoding);
|
|
15
|
+
callback();
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const text = decodePromptChunk(chunk, encoding);
|
|
20
|
+
if (text === "\n" || text === "\r\n") {
|
|
21
|
+
writePromptChunk(this.target, chunk, encoding);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
callback();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
writeVisible(text) {
|
|
28
|
+
this.target.write(text);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function decodePromptChunk(chunk, encoding) {
|
|
33
|
+
if (typeof chunk === "string") {
|
|
34
|
+
return chunk;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (encoding && encoding !== "buffer") {
|
|
38
|
+
return chunk.toString(encoding);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return chunk.toString();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function writePromptChunk(target, chunk, encoding) {
|
|
45
|
+
if (encoding && encoding !== "buffer") {
|
|
46
|
+
target.write(chunk, encoding);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
target.write(chunk);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function withPrompt(callback) {
|
|
54
|
+
const output = new PromptOutput(stdout);
|
|
55
|
+
const rl = createInterface({
|
|
56
|
+
input: stdin,
|
|
57
|
+
output,
|
|
58
|
+
terminal: true,
|
|
59
|
+
});
|
|
60
|
+
rl.__promptOutput = output;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
return await callback(rl);
|
|
64
|
+
} finally {
|
|
65
|
+
rl.close();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function ask(rl, label, fallback = null) {
|
|
70
|
+
const suffix = fallback ? ` [${fallback}]` : "";
|
|
71
|
+
const answer = await rl.question(`${label}${suffix}: `);
|
|
72
|
+
const trimmed = answer.trim();
|
|
73
|
+
if (trimmed) {
|
|
74
|
+
return trimmed;
|
|
75
|
+
}
|
|
76
|
+
return fallback;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function askSecret(rl, label) {
|
|
80
|
+
const output = rl && rl.__promptOutput;
|
|
81
|
+
if (!output) {
|
|
82
|
+
return ask(rl, label);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
output.writeVisible(`${label}: `);
|
|
86
|
+
output.muted = true;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const answer = await rl.question("");
|
|
90
|
+
const trimmed = answer.trim();
|
|
91
|
+
return trimmed || null;
|
|
92
|
+
} finally {
|
|
93
|
+
output.muted = false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = {
|
|
98
|
+
PromptOutput,
|
|
99
|
+
ask,
|
|
100
|
+
askSecret,
|
|
101
|
+
decodePromptChunk,
|
|
102
|
+
withPrompt,
|
|
103
|
+
};
|