@tailglow/cli 0.9.6 → 0.9.16
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/package.json +2 -2
- package/src/commands.generated.js +144 -128
- package/src/config-command.js +7 -0
- package/src/config.js +33 -0
- package/src/docs-command.js +6 -0
- package/src/docs.generated.js +21 -21
- package/src/flags.js +15 -0
- package/src/help.js +4 -0
- package/src/import-command.js +111 -10
- package/src/import-file.js +47 -0
- package/src/import-worker.generated.js +5 -1
- package/src/import.generated.js +5 -1
- package/src/index.js +34 -0
- package/src/login-command.js +118 -6
- package/src/output.js +13 -2
- package/src/request.js +18 -0
- package/src/update-check.js +22 -0
package/src/request.js
CHANGED
|
@@ -41,11 +41,14 @@ export async function buildRequestHeaders({ secret, team_id, env = process.env }
|
|
|
41
41
|
|
|
42
42
|
// fetch throws on some header characters, such as a line break, so only a plain name is sent.
|
|
43
43
|
const terminal = env.TERM_PROGRAM?.trim();
|
|
44
|
+
|
|
44
45
|
if (terminal && /^[\w .+-]{1,32}$/.test(terminal)) {
|
|
45
46
|
headers["x-tailglow-client-terminal"] = terminal;
|
|
46
47
|
}
|
|
48
|
+
|
|
47
49
|
if (secret) headers.Authorization = `Bearer ${secret}`;
|
|
48
50
|
if (team_id) headers["x-tailglow-team-id"] = team_id;
|
|
51
|
+
|
|
49
52
|
return headers;
|
|
50
53
|
}
|
|
51
54
|
|
|
@@ -79,6 +82,7 @@ export async function sendRequest({ method, url, headers, body, fetch_impl = fet
|
|
|
79
82
|
|
|
80
83
|
/** @type {import("./types.d.ts").ApiResponse | null} */
|
|
81
84
|
let payload = null;
|
|
85
|
+
|
|
82
86
|
if (text.trim() !== "") {
|
|
83
87
|
try {
|
|
84
88
|
payload = readEnvelope(text);
|
|
@@ -86,6 +90,7 @@ export async function sendRequest({ method, url, headers, body, fetch_impl = fet
|
|
|
86
90
|
payload = null;
|
|
87
91
|
}
|
|
88
92
|
}
|
|
93
|
+
|
|
89
94
|
return { status: response.status, ok: response.ok, payload };
|
|
90
95
|
}
|
|
91
96
|
|
|
@@ -104,11 +109,13 @@ export function formatValidationIssues(message) {
|
|
|
104
109
|
|
|
105
110
|
/** @type {unknown} */
|
|
106
111
|
let issues;
|
|
112
|
+
|
|
107
113
|
try {
|
|
108
114
|
issues = JSON.parse(message);
|
|
109
115
|
} catch {
|
|
110
116
|
return null;
|
|
111
117
|
}
|
|
118
|
+
|
|
112
119
|
if (!Array.isArray(issues) || issues.length === 0) return null;
|
|
113
120
|
|
|
114
121
|
// Each piece is sanitized on its own, because the assembled string carries real newlines that the
|
|
@@ -122,6 +129,7 @@ export function formatValidationIssues(message) {
|
|
|
122
129
|
const detail = sanitizeText(
|
|
123
130
|
typeof issue?.message === "string" ? issue.message : JSON.stringify(issue)
|
|
124
131
|
);
|
|
132
|
+
|
|
125
133
|
return where ? ` ${where}: ${detail}` : ` ${detail}`;
|
|
126
134
|
});
|
|
127
135
|
|
|
@@ -148,14 +156,17 @@ function describeFailure({ status, message, method, url, code = null, is_login =
|
|
|
148
156
|
// Parsed BEFORE sanitizing: escaping the newlines the API embedded would stop it parsing at all,
|
|
149
157
|
// which is how the first attempt at this silently fell through to the raw dump.
|
|
150
158
|
const issues = formatValidationIssues(String(message));
|
|
159
|
+
|
|
151
160
|
if (issues) return `${status} ${issues}`;
|
|
152
161
|
|
|
153
162
|
const text = sanitizeText(String(message));
|
|
163
|
+
|
|
154
164
|
if (status === 404) {
|
|
155
165
|
return `${status} ${text}\nNothing answers ${method} ${sanitizeText(url.pathname)}.`;
|
|
156
166
|
}
|
|
157
167
|
|
|
158
168
|
const advice = is_login && code ? LOGIN_REFUSAL_ADVICE[code] : null;
|
|
169
|
+
|
|
159
170
|
if (advice && (status === 401 || status === 403)) return `${status} ${text}\n${advice}`;
|
|
160
171
|
|
|
161
172
|
return `${status} ${text}`;
|
|
@@ -173,6 +184,7 @@ function describeFailure({ status, message, method, url, code = null, is_login =
|
|
|
173
184
|
function readEnvelope(text) {
|
|
174
185
|
/** @type {unknown} */
|
|
175
186
|
const value = JSON.parse(text);
|
|
187
|
+
|
|
176
188
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
177
189
|
|
|
178
190
|
return /** @type {import("./types.d.ts").ApiResponse} */ (value);
|
|
@@ -201,8 +213,10 @@ export async function requestApi({
|
|
|
201
213
|
path_team_id = null
|
|
202
214
|
}) {
|
|
203
215
|
const url = new URL(path, resolveApiUrl({ flags, config }));
|
|
216
|
+
|
|
204
217
|
for (const [key, value] of Object.entries(query)) {
|
|
205
218
|
if (value === undefined || value === null) continue;
|
|
219
|
+
|
|
206
220
|
url.searchParams.set(key, typeof value === "object" ? JSON.stringify(value) : String(value));
|
|
207
221
|
}
|
|
208
222
|
|
|
@@ -221,6 +235,7 @@ export async function requestApi({
|
|
|
221
235
|
/** @type {import("./types.d.ts").ApiResponse | null} */
|
|
222
236
|
let payload = null;
|
|
223
237
|
let parsed = true;
|
|
238
|
+
|
|
224
239
|
if (body_text.trim() !== "") {
|
|
225
240
|
try {
|
|
226
241
|
payload = readEnvelope(body_text);
|
|
@@ -231,6 +246,7 @@ export async function requestApi({
|
|
|
231
246
|
|
|
232
247
|
if (!response.ok) {
|
|
233
248
|
const message = payload?.error?.message || payload?.message || response.statusText;
|
|
249
|
+
|
|
234
250
|
throw new Error(
|
|
235
251
|
describeFailure({
|
|
236
252
|
status: response.status,
|
|
@@ -311,9 +327,11 @@ export async function requestAllPages({
|
|
|
311
327
|
});
|
|
312
328
|
|
|
313
329
|
const page = Array.isArray(payload?.data) ? payload.data : [];
|
|
330
|
+
|
|
314
331
|
collected.push(...page);
|
|
315
332
|
|
|
316
333
|
const next = payload?.pagination?.next_cursor ?? null;
|
|
334
|
+
|
|
317
335
|
if (!next || page.length === 0) return { data: collected, truncated: false, next_cursor: null };
|
|
318
336
|
|
|
319
337
|
// Checked BEFORE the cap. A page that both reaches the cap and hands back a cursor already used
|
package/src/update-check.js
CHANGED
|
@@ -20,10 +20,13 @@ function parseVersion(version) {
|
|
|
20
20
|
export function isNewerVersion(candidate, installed) {
|
|
21
21
|
const next = parseVersion(candidate);
|
|
22
22
|
const current = parseVersion(installed);
|
|
23
|
+
|
|
23
24
|
if (!next || !current) return false;
|
|
25
|
+
|
|
24
26
|
for (let index = 0; index < 3; index += 1) {
|
|
25
27
|
if (next[index] !== current[index]) return next[index] > current[index];
|
|
26
28
|
}
|
|
29
|
+
|
|
27
30
|
return false;
|
|
28
31
|
}
|
|
29
32
|
|
|
@@ -37,13 +40,20 @@ export function isNewerVersion(candidate, installed) {
|
|
|
37
40
|
async function readUpdateCheck(path) {
|
|
38
41
|
/** @type {import("node:fs/promises").FileHandle | null} */
|
|
39
42
|
let handle = null;
|
|
43
|
+
|
|
40
44
|
try {
|
|
41
45
|
handle = await fs.open(path, constants.O_RDONLY | constants.O_NONBLOCK);
|
|
46
|
+
|
|
42
47
|
const stats = await handle.stat();
|
|
48
|
+
|
|
43
49
|
if (!stats.isFile() || stats.size > CLI_CONFIGS.UPDATE_CHECK_MAX_BYTES) return null;
|
|
50
|
+
|
|
44
51
|
const stored = JSON.parse(await handle.readFile("utf-8"));
|
|
52
|
+
|
|
45
53
|
if (typeof stored?.checked_at !== "number") return null;
|
|
54
|
+
|
|
46
55
|
const latest_version = typeof stored.latest_version === "string" ? stored.latest_version : null;
|
|
56
|
+
|
|
47
57
|
return { checked_at: stored.checked_at, latest_version };
|
|
48
58
|
} catch {
|
|
49
59
|
return null;
|
|
@@ -56,6 +66,7 @@ async function readUpdateCheck(path) {
|
|
|
56
66
|
function readManifestVersion(body) {
|
|
57
67
|
try {
|
|
58
68
|
const manifest = JSON.parse(body);
|
|
69
|
+
|
|
59
70
|
return typeof manifest?.version === "string" ? manifest.version : null;
|
|
60
71
|
} catch {
|
|
61
72
|
return null;
|
|
@@ -76,18 +87,23 @@ function fetchLatestVersion() {
|
|
|
76
87
|
{ signal: AbortSignal.timeout(CLI_CONFIGS.UPDATE_CHECK_TIMEOUT_MS) },
|
|
77
88
|
(response) => {
|
|
78
89
|
let body = "";
|
|
90
|
+
|
|
79
91
|
response.setEncoding("utf-8");
|
|
80
92
|
response.on("data", (chunk) => {
|
|
81
93
|
body += chunk;
|
|
94
|
+
|
|
82
95
|
if (body.length > CLI_CONFIGS.UPDATE_CHECK_MAX_BYTES) request.destroy();
|
|
83
96
|
});
|
|
97
|
+
|
|
84
98
|
response.on("end", () =>
|
|
85
99
|
resolve(response.statusCode === 200 ? readManifestVersion(body) : null)
|
|
86
100
|
);
|
|
101
|
+
|
|
87
102
|
// Settles an aborted or destroyed response; after `end` the promise has already resolved.
|
|
88
103
|
response.on("close", () => resolve(null));
|
|
89
104
|
}
|
|
90
105
|
);
|
|
106
|
+
|
|
91
107
|
request.on("error", () => resolve(null));
|
|
92
108
|
// A request can also end with no response and no error, such as an upgrade nothing accepts.
|
|
93
109
|
request.on("close", () => resolve(null));
|
|
@@ -114,19 +130,25 @@ export async function readUpdateNotice({
|
|
|
114
130
|
path = CLI_CONFIGS.UPDATE_CHECK_PATH
|
|
115
131
|
} = {}) {
|
|
116
132
|
if (env.CI || env.TAILGLOW_NO_UPDATE_CHECK) return null;
|
|
133
|
+
|
|
117
134
|
try {
|
|
118
135
|
let check = await readUpdateCheck(path);
|
|
136
|
+
|
|
119
137
|
if (!check || now - check.checked_at >= CLI_CONFIGS.UPDATE_CHECK_INTERVAL_MS) {
|
|
120
138
|
const previous = check?.latest_version ?? null;
|
|
139
|
+
|
|
121
140
|
// Recorded before npm is asked: an attempt that cannot be recorded is not made, so a machine
|
|
122
141
|
// that cannot write the file is never slowed down on every command.
|
|
123
142
|
await saveConfig({ checked_at: now, latest_version: previous }, path);
|
|
124
143
|
check = { checked_at: now, latest_version: (await fetch_latest_version()) ?? previous };
|
|
144
|
+
|
|
125
145
|
if (check.latest_version !== previous) await saveConfig(check, path);
|
|
126
146
|
}
|
|
127
147
|
|
|
128
148
|
const installed = await readCliVersion();
|
|
149
|
+
|
|
129
150
|
if (!check.latest_version || !isNewerVersion(check.latest_version, installed)) return null;
|
|
151
|
+
|
|
130
152
|
return (
|
|
131
153
|
`tglow ${check.latest_version} is available (you have ${installed}). ` +
|
|
132
154
|
"Update with `npm install -g @tailglow/cli@latest`, or run `npx @tailglow/cli@latest`."
|