@tailglow/cli 0.9.5 → 0.9.15

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/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
@@ -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`."