@tasksai/install 0.1.4 → 0.1.6

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 +2 -0
  2. package/package.json +2 -1
  3. package/src/index.js +59 -10
package/README.md CHANGED
@@ -28,6 +28,8 @@ The installer:
28
28
  - stores credentials outside MCP client config
29
29
  - configures supported MCP clients
30
30
  - checks required write access before changing local files
31
+ - prints customer-facing recovery instructions when an AI assistant is blocked
32
+ from writing local config files
31
33
  - supports Claude Desktop, Cursor, Windsurf, and Codex
32
34
  - runs a local health check
33
35
 
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@tasksai/install",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Shared TasksAI MCP installer CLI",
5
5
  "type": "module",
6
6
  "bin": {
7
+ "install": "src/index.js",
7
8
  "tasksai-install": "src/index.js"
8
9
  },
9
10
  "files": [
package/src/index.js CHANGED
@@ -9,7 +9,7 @@ import readline from "node:readline/promises";
9
9
  import { spawnSync } from "node:child_process";
10
10
  import { stdin as input, stdout as output } from "node:process";
11
11
 
12
- const INSTALLER_VERSION = "0.1.4";
12
+ const INSTALLER_VERSION = "0.1.5";
13
13
  const DEFAULT_SOURCES = {
14
14
  lawtasksai: "https://github.com/laudoluxDev/lawtasksai-mcp",
15
15
  farmer: "https://github.com/laudoluxDev/farmertasksai-mcp",
@@ -163,7 +163,7 @@ async function install(options, { updateOnly = false } = {}) {
163
163
  const runtimeDir = path.join(installDir, "runtime");
164
164
  const vendorDir = path.join(installDir, "python");
165
165
  const clients = updateOnly ? [] : resolveClients(options.client);
166
- await preflightWriteAccess({ operation: updateOnly ? "update" : "install", installDir, clients });
166
+ await preflightWriteAccess({ operation: updateOnly ? "update" : "install", installDir, clients, options, vertical, source });
167
167
 
168
168
  await fsp.mkdir(runtimeDir, { recursive: true });
169
169
  await fsp.mkdir(path.join(installDir, "logs"), { recursive: true });
@@ -251,7 +251,7 @@ async function doctor(options, { quietSuccess = false } = {}) {
251
251
  async function uninstall(options) {
252
252
  const installDir = getInstallDir(options.productId, options);
253
253
  const clients = resolveClients(options.client);
254
- await preflightWriteAccess({ operation: "uninstall", installDir, clients });
254
+ await preflightWriteAccess({ operation: "uninstall", installDir, clients, options });
255
255
 
256
256
  for (const client of clients) {
257
257
  const configPath = client.configPath();
@@ -407,7 +407,7 @@ function resolveClients(clientOption) {
407
407
  return [client];
408
408
  }
409
409
 
410
- async function preflightWriteAccess({ operation, installDir, clients }) {
410
+ async function preflightWriteAccess({ operation, installDir, clients, options, vertical, source }) {
411
411
  const checks = [
412
412
  {
413
413
  label: "TasksAI install directory",
@@ -440,13 +440,62 @@ async function preflightWriteAccess({ operation, installDir, clients }) {
440
440
 
441
441
  if (!failures.length) return;
442
442
 
443
- const detail = failures
443
+ throw new Error(formatPermissionRecovery({ operation, failures, options, vertical, source }));
444
+ }
445
+
446
+ function formatPermissionRecovery({ operation, failures, options, vertical, source }) {
447
+ const productName = vertical?.display_name || options.productId;
448
+ const clientNames = resolveClients(options.client).map((client) => client.displayName).join(", ");
449
+ const blockedPaths = failures
444
450
  .map((failure) => `- ${failure.label}: ${failure.targetPath} (${failure.reason})`)
445
451
  .join("\n");
446
- throw new Error(
447
- `TasksAI cannot ${operation} until it has write access to the required local paths:\n${detail}\n` +
448
- "Grant write access to these paths, or use --install-dir /path/to/product-dir for the TasksAI runtime."
449
- );
452
+ const installCommand = buildInstallCommand({ options, source });
453
+ const doctorCommand = buildDoctorCommand(options);
454
+ const installPage = vertical?.website_url ? `${vertical.website_url.replace(/\/$/, "")}/install.html` : null;
455
+ const supportPage = vertical?.website_url ? `${vertical.website_url.replace(/\/$/, "")}/support.html` : null;
456
+
457
+ return [
458
+ `Your AI assistant could not finish the ${productName} ${operation} because it cannot write to required local setup files.`,
459
+ "",
460
+ "This is a normal security restriction in some AI apps and sandboxed environments. No account data was changed.",
461
+ "",
462
+ "Blocked paths:",
463
+ blockedPaths,
464
+ "",
465
+ "To finish setup, run this official installer command directly in Terminal:",
466
+ installCommand,
467
+ "",
468
+ "Then restart your AI app and run this health check:",
469
+ doctorCommand,
470
+ "",
471
+ `Selected client: ${clientNames}`,
472
+ installPage ? `Install help: ${installPage}` : null,
473
+ supportPage ? `Support: ${supportPage}` : null,
474
+ "",
475
+ "If you still want the AI assistant to finish automatically, grant it write access to the blocked paths and rerun the same install prompt."
476
+ ].filter(Boolean).join("\n");
477
+ }
478
+
479
+ function buildInstallCommand({ options, source }) {
480
+ const parts = ["npx", "@tasksai/install", options.productId];
481
+ const sourceUrl = source?.repoUrl || options.source;
482
+ if (sourceUrl) parts.push("--source", sourceUrl);
483
+ if (options.client) parts.push("--client", options.client);
484
+ if (options.installDir) parts.push("--install-dir", resolveUserPath(options.installDir));
485
+ return parts.map(shellToken).join(" ");
486
+ }
487
+
488
+ function buildDoctorCommand(options) {
489
+ const parts = ["npx", "@tasksai/install", options.productId, "doctor"];
490
+ if (options.client) parts.push("--client", options.client);
491
+ if (options.installDir) parts.push("--install-dir", resolveUserPath(options.installDir));
492
+ return parts.map(shellToken).join(" ");
493
+ }
494
+
495
+ function shellToken(value) {
496
+ const text = String(value);
497
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(text)) return text;
498
+ return `'${text.replace(/'/g, "'\\''")}'`;
450
499
  }
451
500
 
452
501
  async function checkWritable({ label, targetPath, kind }) {
@@ -848,7 +897,7 @@ function formatError(error) {
848
897
 
849
898
  function redact(text) {
850
899
  return String(text)
851
- .replace(/\b[a-z]{2,4}_[A-Za-z0-9_-]{8,}\b/g, (match) => `${match.split("_")[0]}_[REDACTED]`)
900
+ .replace(/\b(?:lt|ft|pa|rt|tt|th|mt|ct|at|ch|cu|dt|ds|el|ep|fu|hr|in|ll|ms|mo|mu|nt|pt|pp|pl|pr|re|sl|ta|vt)_[A-Za-z0-9_-]{8,}\b/g, (match) => `${match.split("_")[0]}_[REDACTED]`)
852
901
  .replace(/Bearer\s+[A-Za-z0-9._-]+/gi, "Bearer [REDACTED]")
853
902
  .replace(/gh[opsu]_[A-Za-z0-9_]+/g, "gh_[REDACTED]");
854
903
  }