@tasksai/install 0.1.16 → 0.1.18

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.
Files changed (3) hide show
  1. package/README.md +20 -1
  2. package/package.json +2 -2
  3. package/src/index.js +318 -20
package/README.md CHANGED
@@ -31,9 +31,28 @@ The installer:
31
31
  - prints customer-facing recovery instructions when an AI assistant is blocked
32
32
  from writing local config files
33
33
  - supports Claude Desktop, Cursor, Windsurf, and Codex
34
- - runs a local health check
34
+ - runs local runtime, API, and authenticated license health checks
35
+ - reports only a content-free `doctor_passed` activation event after core checks
36
+ succeed; activation-reporting outages do not fail an otherwise healthy doctor
35
37
  - exposes a local save-document tool for finished skill outputs
36
38
 
39
+ For known production products, wrapper and remote runtime files are accepted
40
+ only from the registered production source. RealtorTasksAI, FarmerTasksAI,
41
+ LawTasksAI, and TeacherTasksAI are pinned to the `main` branch and product path
42
+ in `TasksAI-Official/tasksai-mcp-wrappers`. Local paths, alternate branches,
43
+ unregistered products, and other repositories are rejected by default.
44
+
45
+ Developers who intentionally need one of those sources must opt in explicitly:
46
+
47
+ ```bash
48
+ tasksai-install realtor \
49
+ --source /absolute/path/to/local/realtor-wrapper \
50
+ --allow-untrusted-development-source
51
+ ```
52
+
53
+ The override is development-only and emits a visible warning. It must not be
54
+ used for customer installs.
55
+
37
56
  TasksAI servers handle authentication, credits, catalog/search metadata, and
38
57
  licensed skill delivery. TasksAI does not process user task content.
39
58
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tasksai/install",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Shared TasksAI MCP installer CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,6 +33,6 @@
33
33
  },
34
34
  "scripts": {
35
35
  "check": "node --check src/index.js",
36
- "test": "python3 -m unittest discover -s tests"
36
+ "test": "node --test tests/*.test.mjs && python3 -m unittest discover -s tests"
37
37
  }
38
38
  }
package/src/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  import fs from "node:fs";
4
4
  import fsp from "node:fs/promises";
5
+ import { randomUUID } from "node:crypto";
5
6
  import https from "node:https";
6
7
  import os from "node:os";
7
8
  import path from "node:path";
@@ -10,15 +11,35 @@ import { spawnSync } from "node:child_process";
10
11
  import { stdin as input, stdout as output } from "node:process";
11
12
  import { fileURLToPath } from "node:url";
12
13
 
13
- const INSTALLER_VERSION = "0.1.15";
14
14
  const INSTALLER_DIR = path.dirname(fileURLToPath(import.meta.url));
15
+ const INSTALLER_PACKAGE = JSON.parse(fs.readFileSync(path.resolve(INSTALLER_DIR, "..", "package.json"), "utf8"));
16
+ const INSTALLER_VERSION = INSTALLER_PACKAGE.version;
15
17
  const BUNDLED_RUNTIME_DIR = path.resolve(INSTALLER_DIR, "..", "runtime");
18
+ const OFFICIAL_WRAPPER_REPO = "https://github.com/TasksAI-Official/tasksai-mcp-wrappers";
19
+ const TRUSTED_PRODUCTION_SOURCES = new Map([
20
+ ["lawtasksai", { repo: OFFICIAL_WRAPPER_REPO, ref: "main", path: "verticals/lawtasksai" }],
21
+ ["farmer", { repo: OFFICIAL_WRAPPER_REPO, ref: "main", path: "verticals/farmer" }],
22
+ ["realtor", { repo: OFFICIAL_WRAPPER_REPO, ref: "main", path: "verticals/realtor" }],
23
+ ["teacher", { repo: OFFICIAL_WRAPPER_REPO, ref: "main", path: "verticals/teacher" }],
24
+ ["contractor", { repo: "https://github.com/TasksAI-Official/contractortasksai-mcp", ref: "main", path: "", legacyManifestPathOptional: true }],
25
+ ["therapist", { repo: "https://github.com/TasksAI-Official/therapisttasksai-mcp", ref: "main", path: "", legacyManifestPathOptional: true }],
26
+ ["marketing", { repo: "https://github.com/TasksAI-Official/marketingtasksai-mcp", ref: "main", path: "", legacyManifestPathOptional: true }],
27
+ ["priorauthai", {
28
+ repo: "https://github.com/TasksAI-Official/priorauthai-mcp",
29
+ ref: "main",
30
+ path: "",
31
+ legacyManifestPathOptional: true
32
+ }]
33
+ ]);
16
34
  const DEFAULT_SOURCES = {
17
35
  lawtasksai: "https://github.com/TasksAI-Official/tasksai-mcp-wrappers/tree/main/verticals/lawtasksai",
18
36
  farmer: "https://github.com/TasksAI-Official/tasksai-mcp-wrappers/tree/main/verticals/farmer",
19
37
  realtor: "https://github.com/TasksAI-Official/tasksai-mcp-wrappers/tree/main/verticals/realtor",
20
38
  teacher: "https://github.com/TasksAI-Official/tasksai-mcp-wrappers/tree/main/verticals/teacher",
21
- priorauthai: "https://github.com/laudoluxDev/priorauthai-mcp"
39
+ contractor: "https://github.com/TasksAI-Official/contractortasksai-mcp",
40
+ therapist: "https://github.com/TasksAI-Official/therapisttasksai-mcp",
41
+ marketing: "https://github.com/TasksAI-Official/marketingtasksai-mcp",
42
+ priorauthai: "https://github.com/TasksAI-Official/priorauthai-mcp"
22
43
  };
23
44
  const LEGACY_MCP_SERVER_IDS = {
24
45
  farmer: ["farmertasksai"],
@@ -64,10 +85,12 @@ const CLIENTS = {
64
85
  }
65
86
  };
66
87
 
67
- main().catch((error) => {
68
- console.error(formatError(error));
69
- process.exit(1);
70
- });
88
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
89
+ main().catch((error) => {
90
+ console.error(formatError(error));
91
+ process.exit(1);
92
+ });
93
+ }
71
94
 
72
95
  async function main() {
73
96
  const options = parseArgs(process.argv.slice(2));
@@ -106,7 +129,8 @@ function parseArgs(argv) {
106
129
  installDir: process.env.TASKSAI_INSTALL_DIR || null,
107
130
  noBrowser: false,
108
131
  yes: false,
109
- skipPythonDeps: false
132
+ skipPythonDeps: false,
133
+ allowUntrustedDevelopmentSource: false
110
134
  };
111
135
 
112
136
  const positionals = [];
@@ -120,6 +144,7 @@ function parseArgs(argv) {
120
144
  else if (arg === "--no-browser") options.noBrowser = true;
121
145
  else if (arg === "--yes" || arg === "-y") options.yes = true;
122
146
  else if (arg === "--skip-python-deps") options.skipPythonDeps = true;
147
+ else if (arg === "--allow-untrusted-development-source") options.allowUntrustedDevelopmentSource = true;
123
148
  else if (arg === "--help" || arg === "-h") {
124
149
  printUsage();
125
150
  process.exit(0);
@@ -161,6 +186,10 @@ Examples:
161
186
 
162
187
  Environment:
163
188
  TASKSAI_INSTALL_DIR may be used instead of --install-dir.
189
+
190
+ Development only:
191
+ --allow-untrusted-development-source permits a local, non-main, or non-official
192
+ source for a known production product. Never use it for a customer install.
164
193
  `);
165
194
  }
166
195
 
@@ -169,6 +198,9 @@ async function install(options, { updateOnly = false } = {}) {
169
198
  const manifest = await loadJson(source, "agent-install.json");
170
199
  const vertical = await loadJson(source, "vertical.json");
171
200
  verifySource({ options, source, manifest, vertical });
201
+ if (options.allowUntrustedDevelopmentSource) {
202
+ console.warn("WARNING: untrusted development source override enabled; this install is not production-trusted.");
203
+ }
172
204
 
173
205
  const installDir = getInstallDir(options.productId, options);
174
206
  const runtimeDir = path.join(installDir, "runtime");
@@ -190,6 +222,11 @@ async function install(options, { updateOnly = false } = {}) {
190
222
  await downloadRuntime(source, runtimeDir, manifest);
191
223
 
192
224
  const licenseKey = await resolveLicenseKey(options, vertical, installDir);
225
+ const existingEnvPath = path.join(installDir, ".env");
226
+ const existingEnv = fs.existsSync(existingEnvPath)
227
+ ? parseEnv(await fsp.readFile(existingEnvPath, "utf8"))
228
+ : {};
229
+ const installId = firstValue(existingEnv.TASKSAI_INSTALL_ID, process.env.TASKSAI_INSTALL_ID) || randomUUID();
193
230
 
194
231
  if (!options.skipPythonDeps) {
195
232
  installPythonDeps(runtimeDir, vendorDir);
@@ -200,7 +237,8 @@ async function install(options, { updateOnly = false } = {}) {
200
237
  LAWTASKSAI_LICENSE_KEY: licenseKey,
201
238
  TASKSAI_PRODUCT_ID: vertical.product_id,
202
239
  TASKSAI_API_BASE: vertical.api_base_url,
203
- LAWTASKSAI_API_BASE: vertical.api_base_url
240
+ LAWTASKSAI_API_BASE: vertical.api_base_url,
241
+ TASKSAI_INSTALL_ID: installId
204
242
  };
205
243
  await writeEnvFile(path.join(installDir, ".env"), envEntries);
206
244
  await writeEnvFile(path.join(runtimeDir, ".env"), envEntries);
@@ -224,18 +262,27 @@ async function install(options, { updateOnly = false } = {}) {
224
262
  console.log(`After restart, ask: "${vertical.first_prompt}"`);
225
263
  }
226
264
 
227
- async function doctor(options, { quietSuccess = false } = {}) {
265
+ async function doctor(options, {
266
+ quietSuccess = false,
267
+ clientsOverride = null,
268
+ requestJsonImpl = requestJson,
269
+ runtimeHealthImpl = verifyRuntimeHealth
270
+ } = {}) {
228
271
  const installDir = getInstallDir(options.productId, options);
229
272
  const verticalPath = path.join(installDir, "vertical.json");
230
273
  const envPath = path.join(installDir, ".env");
231
274
  const serverPath = path.join(installDir, "runtime", "server.py");
232
- const clients = resolveClients(options.client);
275
+ const requirementsPath = path.join(installDir, "runtime", "requirements.txt");
276
+ const vendorDir = path.join(installDir, "python");
277
+ const clients = clientsOverride || resolveClients(options.client);
233
278
 
234
279
  const problems = [];
235
280
  if (!fs.existsSync(verticalPath)) problems.push(`Missing ${verticalPath}`);
236
281
  if (!fs.existsSync(envPath)) problems.push(`Missing ${envPath}`);
237
282
  if (!fs.existsSync(serverPath)) problems.push(`Missing ${serverPath}`);
283
+ if (!fs.existsSync(requirementsPath)) problems.push(`Missing ${requirementsPath}`);
238
284
  const vertical = fs.existsSync(verticalPath) ? await readJson(verticalPath) : { product_id: options.productId };
285
+ const env = fs.existsSync(envPath) ? parseEnv(await fsp.readFile(envPath, "utf8")) : {};
239
286
 
240
287
  for (const client of clients) {
241
288
  const configPath = client.configPath();
@@ -249,15 +296,98 @@ async function doctor(options, { quietSuccess = false } = {}) {
249
296
  }
250
297
  }
251
298
 
299
+ if (fs.existsSync(serverPath) && fs.existsSync(requirementsPath)) {
300
+ try {
301
+ await runtimeHealthImpl({ serverPath, vendorDir });
302
+ } catch (error) {
303
+ problems.push(`Runtime health check failed (${healthErrorSummary(error)})`);
304
+ }
305
+ }
306
+
307
+ const productId = String(vertical.product_id || options.productId || "").trim().toLowerCase();
308
+ if (productId !== String(options.productId || "").trim().toLowerCase()) {
309
+ problems.push(`Installed product ${vertical.product_id} does not match requested product ${options.productId}`);
310
+ }
311
+ const configuredProductId = firstValue(process.env.TASKSAI_PRODUCT_ID, env.TASKSAI_PRODUCT_ID);
312
+ if (configuredProductId && configuredProductId.trim().toLowerCase() !== productId) {
313
+ problems.push(`Runtime product ${configuredProductId} does not match installed product ${productId}`);
314
+ }
315
+
316
+ const apiBase = firstValue(
317
+ process.env.TASKSAI_API_BASE,
318
+ process.env.LAWTASKSAI_API_BASE,
319
+ env.TASKSAI_API_BASE,
320
+ env.LAWTASKSAI_API_BASE,
321
+ vertical.api_base_url
322
+ )?.replace(/\/$/, "");
323
+ const licenseKey = firstValue(
324
+ process.env.TASKSAI_LICENSE_KEY,
325
+ process.env.LAWTASKSAI_LICENSE_KEY,
326
+ env.TASKSAI_LICENSE_KEY,
327
+ env.LAWTASKSAI_LICENSE_KEY
328
+ );
329
+ const installId = firstValue(process.env.TASKSAI_INSTALL_ID, env.TASKSAI_INSTALL_ID);
330
+
331
+ let apiHealthy = false;
332
+ if (!apiBase) {
333
+ problems.push("No TasksAI API base URL is configured");
334
+ } else {
335
+ try {
336
+ const apiHealth = await requestJsonImpl(`${apiBase}/health`, {
337
+ headers: { "User-Agent": `tasksai-install/${INSTALLER_VERSION}` }
338
+ });
339
+ const status = String(apiHealth?.status || "").trim().toLowerCase();
340
+ if (["error", "failed", "unhealthy"].includes(status)) {
341
+ problems.push("API health check failed (unhealthy)");
342
+ } else {
343
+ apiHealthy = true;
344
+ }
345
+ } catch (error) {
346
+ problems.push(`API health check failed (${healthErrorSummary(error)})`);
347
+ }
348
+ }
349
+
350
+ if (!licenseKey) {
351
+ problems.push("No TasksAI license key is configured");
352
+ } else if (apiBase && apiHealthy) {
353
+ try {
354
+ const account = await requestJsonImpl(`${apiBase}/v1/me?product_id=${encodeURIComponent(productId)}`, {
355
+ headers: authenticatedHeaders({ licenseKey, productId })
356
+ });
357
+ const licensedProduct = String(account?.product_id || account?.product?.product_id || "").trim().toLowerCase();
358
+ if (!licensedProduct) {
359
+ problems.push("License health response did not identify a product");
360
+ } else if (licensedProduct !== productId) {
361
+ problems.push(`License is for ${licensedProduct}, not ${productId}`);
362
+ }
363
+ } catch (error) {
364
+ problems.push(`License health check failed (${healthErrorSummary(error)})`);
365
+ }
366
+ }
367
+
252
368
  if (problems.length) {
253
369
  throw new Error(`TasksAI doctor found issues:\n- ${problems.join("\n- ")}`);
254
370
  }
255
371
 
372
+ const activationReported = await safeReportDoctorPassed({
373
+ apiBase,
374
+ licenseKey,
375
+ productId,
376
+ installId,
377
+ client: options.client,
378
+ requestJsonImpl
379
+ });
256
380
  await safeLogEvent(installDir, "doctor_passed", {
257
- productId: options.productId,
258
- clients: clients.map((client) => client.id)
381
+ productId,
382
+ clients: clients.map((client) => client.id),
383
+ activationReported
259
384
  });
260
- if (!quietSuccess) console.log("TasksAI doctor passed.");
385
+ if (!quietSuccess) {
386
+ console.log("TasksAI doctor passed.");
387
+ if (!activationReported) {
388
+ console.log("Activation reporting is temporarily unavailable; core doctor checks still passed.");
389
+ }
390
+ }
261
391
  }
262
392
 
263
393
  async function uninstall(options) {
@@ -729,6 +859,7 @@ function verifySource({ options, source, manifest, vertical }) {
729
859
  if (vertical.product_id !== options.productId) {
730
860
  throw new Error(`Vertical product_id ${vertical.product_id} does not match requested product ${options.productId}.`);
731
861
  }
862
+ verifyProductionSource({ options, source, manifest });
732
863
  if (manifest.official_package !== "@tasksai/install") {
733
864
  throw new Error(`Unexpected installer package: ${manifest.official_package}`);
734
865
  }
@@ -754,6 +885,46 @@ function verifySource({ options, source, manifest, vertical }) {
754
885
  }
755
886
  }
756
887
 
888
+ function verifyProductionSource({ options, source, manifest }) {
889
+ if (options.allowUntrustedDevelopmentSource) return;
890
+ const expected = TRUSTED_PRODUCTION_SOURCES.get(options.productId);
891
+ if (!expected) {
892
+ throw new Error(
893
+ `${options.productId} is not registered as a trusted production source. `
894
+ + "For deliberate local or untrusted development only, add --allow-untrusted-development-source."
895
+ );
896
+ }
897
+
898
+ const expectedManifestPath = joinGitPath(expected.path, "agent-install.json");
899
+ const manifestPathTrusted = expected.legacyManifestPathOptional && !manifest.official_manifest_path
900
+ ? true
901
+ : normalizeGitPath(manifest.official_manifest_path) === expectedManifestPath;
902
+ const trusted = source.kind === "github"
903
+ && sameRepository(source.repoUrl, expected.repo)
904
+ && source.ref === expected.ref
905
+ && normalizeGitPath(source.basePath) === expected.path
906
+ && sameRepository(manifest.official_github_repo, expected.repo)
907
+ && manifestPathTrusted;
908
+
909
+ if (!trusted) {
910
+ throw new Error(
911
+ `${options.productId} production installs must use ${formatTrustedSource(expected)}. `
912
+ + "For deliberate local or untrusted development only, add --allow-untrusted-development-source."
913
+ );
914
+ }
915
+ }
916
+
917
+ function formatTrustedSource(expected) {
918
+ return expected.path
919
+ ? `${expected.repo}/tree/${expected.ref}/${expected.path}`
920
+ : `${expected.repo} at ref ${expected.ref}`;
921
+ }
922
+
923
+ function sameRepository(left, right) {
924
+ return String(left || "").replace(/\.git$/i, "").replace(/\/$/, "").toLowerCase()
925
+ === String(right || "").replace(/\.git$/i, "").replace(/\/$/, "").toLowerCase();
926
+ }
927
+
757
928
  function getInstaller(manifest) {
758
929
  if (manifest.installer?.npm_exec) return manifest.installer.npm_exec;
759
930
  if (manifest.installer?.npx) return manifest.installer.npx;
@@ -862,21 +1033,27 @@ function fetchText(url) {
862
1033
  });
863
1034
  }
864
1035
 
865
- function postJson(url, body) {
866
- const payload = JSON.stringify(body || {});
1036
+ function requestJson(url, { method = "GET", body, headers = {}, timeoutMs = 7000 } = {}) {
1037
+ const payload = body === undefined ? null : JSON.stringify(body);
867
1038
  return new Promise((resolve, reject) => {
868
1039
  const request = https.request(url, {
869
- method: "POST",
1040
+ method,
870
1041
  headers: {
871
- "Content-Type": "application/json",
872
- "Content-Length": Buffer.byteLength(payload),
873
- "User-Agent": `tasksai-install/${INSTALLER_VERSION}`
1042
+ "User-Agent": `tasksai-install/${INSTALLER_VERSION}`,
1043
+ ...(payload === null ? {} : {
1044
+ "Content-Type": "application/json",
1045
+ "Content-Length": Buffer.byteLength(payload)
1046
+ }),
1047
+ ...headers
874
1048
  }
875
1049
  }, (response) => {
876
1050
  let responseBody = "";
877
1051
  response.setEncoding("utf8");
878
1052
  response.on("data", (chunk) => {
879
1053
  responseBody += chunk;
1054
+ if (responseBody.length > 1_000_000) {
1055
+ request.destroy(new Error("TasksAI API response exceeded the safe size limit."));
1056
+ }
880
1057
  });
881
1058
  response.on("end", () => {
882
1059
  let data = {};
@@ -897,12 +1074,120 @@ function postJson(url, body) {
897
1074
  resolve(data);
898
1075
  });
899
1076
  });
1077
+ request.setTimeout(timeoutMs, () => {
1078
+ const error = new Error("TasksAI API request timed out.");
1079
+ error.code = "ETIMEDOUT";
1080
+ request.destroy(error);
1081
+ });
900
1082
  request.on("error", reject);
901
- request.write(payload);
1083
+ if (payload !== null) request.write(payload);
902
1084
  request.end();
903
1085
  });
904
1086
  }
905
1087
 
1088
+ function postJson(url, body) {
1089
+ return requestJson(url, { method: "POST", body });
1090
+ }
1091
+
1092
+ function authenticatedHeaders({ licenseKey, productId }) {
1093
+ return {
1094
+ Authorization: `Bearer ${licenseKey}`,
1095
+ "Content-Type": "application/json",
1096
+ "X-Product-ID": productId,
1097
+ "X-Client-Type": "tasksai-installer",
1098
+ "X-Client-Version": INSTALLER_VERSION
1099
+ };
1100
+ }
1101
+
1102
+ async function safeReportDoctorPassed({
1103
+ apiBase,
1104
+ licenseKey,
1105
+ productId,
1106
+ installId,
1107
+ client,
1108
+ requestJsonImpl = requestJson
1109
+ }) {
1110
+ if (!apiBase || !licenseKey || !productId) return false;
1111
+ const normalizedClient = String(client || "")
1112
+ .trim()
1113
+ .toLowerCase()
1114
+ .replaceAll("-", "_");
1115
+ const reportableClients = new Set([
1116
+ "claude_desktop", "cursor", "windsurf", "codex", "cline", "openclaw"
1117
+ ]);
1118
+ const payload = {
1119
+ event_name: "doctor_passed",
1120
+ metadata: {
1121
+ source: "installer_doctor",
1122
+ tool_version: INSTALLER_VERSION
1123
+ }
1124
+ };
1125
+ if (reportableClients.has(normalizedClient)) {
1126
+ payload.metadata.client = normalizedClient;
1127
+ }
1128
+ if (installId) payload.install_id = installId;
1129
+
1130
+ try {
1131
+ await requestJsonImpl(`${apiBase}/v1/events/activation`, {
1132
+ method: "POST",
1133
+ body: payload,
1134
+ headers: authenticatedHeaders({ licenseKey, productId })
1135
+ });
1136
+ return true;
1137
+ } catch {
1138
+ return false;
1139
+ }
1140
+ }
1141
+
1142
+ async function verifyRuntimeHealth({ serverPath, vendorDir }) {
1143
+ const python = findPython();
1144
+ if (!python) throw new Error("Python 3 is unavailable");
1145
+ const pythonPath = [vendorDir, process.env.PYTHONPATH].filter(Boolean).join(path.delimiter);
1146
+ const check = [
1147
+ "import ast, pathlib, sys",
1148
+ "ast.parse(pathlib.Path(sys.argv[1]).read_text(encoding='utf-8'), filename=sys.argv[1])",
1149
+ "import httpx, dotenv, mcp, docx"
1150
+ ].join("; ");
1151
+ const result = spawnSync(python, ["-c", check, serverPath], {
1152
+ encoding: "utf8",
1153
+ timeout: 10000,
1154
+ env: { ...process.env, PYTHONPATH: pythonPath }
1155
+ });
1156
+ if (result.error) throw result.error;
1157
+ if (result.status !== 0) {
1158
+ throw new Error("runtime syntax or Python dependencies are unavailable");
1159
+ }
1160
+ }
1161
+
1162
+ function parseEnv(text) {
1163
+ const values = {};
1164
+ for (const rawLine of String(text || "").split(/\r?\n/)) {
1165
+ const line = rawLine.trim();
1166
+ if (!line || line.startsWith("#")) continue;
1167
+ const match = line.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
1168
+ if (!match) continue;
1169
+ let value = match[2].trim();
1170
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
1171
+ value = value.slice(1, -1);
1172
+ }
1173
+ values[match[1]] = value;
1174
+ }
1175
+ return values;
1176
+ }
1177
+
1178
+ function firstValue(...values) {
1179
+ return values.find((value) => typeof value === "string" && value.trim())?.trim() || "";
1180
+ }
1181
+
1182
+ function healthErrorSummary(error) {
1183
+ if (Number.isInteger(error?.statusCode)) return `HTTP ${error.statusCode}`;
1184
+ if (error?.code === "ETIMEDOUT") return "timeout";
1185
+ if (["ECONNREFUSED", "ENOTFOUND", "EAI_AGAIN", "ECONNRESET"].includes(error?.code)) {
1186
+ return "unreachable";
1187
+ }
1188
+ return "unavailable";
1189
+ }
1190
+
906
1191
  function openExternalUrl(url) {
907
1192
  const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
908
1193
  const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
@@ -1034,3 +1319,16 @@ function redact(text) {
1034
1319
  .replace(/Bearer\s+[A-Za-z0-9._-]+/gi, "Bearer [REDACTED]")
1035
1320
  .replace(/gh[opsu]_[A-Za-z0-9_]+/g, "gh_[REDACTED]");
1036
1321
  }
1322
+
1323
+ export {
1324
+ INSTALLER_VERSION,
1325
+ authenticatedHeaders,
1326
+ doctor,
1327
+ parseArgs,
1328
+ parseEnv,
1329
+ parseSource,
1330
+ safeReportDoctorPassed,
1331
+ verifyProductionSource,
1332
+ verifyRuntimeHealth,
1333
+ verifySource
1334
+ };