@tasksai/install 0.1.2 → 0.1.3
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/README.md +13 -0
- package/package.json +1 -1
- package/src/index.js +126 -13
package/README.md
CHANGED
|
@@ -8,6 +8,18 @@ Users normally run this through a product-specific GitHub manifest, for example:
|
|
|
8
8
|
npx @tasksai/install lawtasksai --source https://github.com/laudoluxDev/lawtasksai-mcp
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
In restricted agent environments, grant write access to the default TasksAI
|
|
12
|
+
application data folder and the selected MCP client config. If the runtime must
|
|
13
|
+
be installed elsewhere, pass an exact product install directory:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx @tasksai/install farmer \
|
|
17
|
+
--source https://github.com/laudoluxDev/farmertasksai-mcp \
|
|
18
|
+
--install-dir /tmp/tasksai/farmer
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`TASKSAI_INSTALL_DIR=/tmp/tasksai/farmer` is equivalent to `--install-dir`.
|
|
22
|
+
|
|
11
23
|
The installer:
|
|
12
24
|
|
|
13
25
|
- verifies the official vertical manifest
|
|
@@ -15,6 +27,7 @@ The installer:
|
|
|
15
27
|
- connects the user's TasksAI account through browser approval when available
|
|
16
28
|
- stores credentials outside MCP client config
|
|
17
29
|
- configures supported MCP clients
|
|
30
|
+
- checks required write access before changing local files
|
|
18
31
|
- runs a local health check
|
|
19
32
|
|
|
20
33
|
TasksAI servers handle authentication, credits, catalog/search metadata, and
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -9,10 +9,11 @@ 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.
|
|
12
|
+
const INSTALLER_VERSION = "0.1.3";
|
|
13
13
|
const DEFAULT_SOURCES = {
|
|
14
14
|
lawtasksai: "https://github.com/laudoluxDev/lawtasksai-mcp",
|
|
15
|
-
farmer: "https://github.com/laudoluxDev/farmertasksai-mcp"
|
|
15
|
+
farmer: "https://github.com/laudoluxDev/farmertasksai-mcp",
|
|
16
|
+
priorauthai: "https://github.com/laudoluxDev/priorauthai-mcp"
|
|
16
17
|
};
|
|
17
18
|
|
|
18
19
|
const CLIENTS = {
|
|
@@ -84,6 +85,7 @@ function parseArgs(argv) {
|
|
|
84
85
|
ref: "main",
|
|
85
86
|
client: "claude-desktop",
|
|
86
87
|
auth: "browser",
|
|
88
|
+
installDir: process.env.TASKSAI_INSTALL_DIR || null,
|
|
87
89
|
noBrowser: false,
|
|
88
90
|
yes: false,
|
|
89
91
|
skipPythonDeps: false
|
|
@@ -96,6 +98,7 @@ function parseArgs(argv) {
|
|
|
96
98
|
else if (arg === "--ref") options.ref = argv[++i];
|
|
97
99
|
else if (arg === "--client") options.client = argv[++i];
|
|
98
100
|
else if (arg === "--auth") options.auth = argv[++i];
|
|
101
|
+
else if (arg === "--install-dir") options.installDir = argv[++i];
|
|
99
102
|
else if (arg === "--no-browser") options.noBrowser = true;
|
|
100
103
|
else if (arg === "--yes" || arg === "-y") options.yes = true;
|
|
101
104
|
else if (arg === "--skip-python-deps") options.skipPythonDeps = true;
|
|
@@ -118,7 +121,7 @@ function parseArgs(argv) {
|
|
|
118
121
|
throw new Error(`Unsupported auth mode: ${options.auth}. Supported modes: browser, license-key`);
|
|
119
122
|
}
|
|
120
123
|
options.source ||= DEFAULT_SOURCES[options.productId];
|
|
121
|
-
if (!options.source) {
|
|
124
|
+
if (!options.source && ["install", "update"].includes(options.command)) {
|
|
122
125
|
throw new Error(`No default source is known for product: ${options.productId}`);
|
|
123
126
|
}
|
|
124
127
|
return options;
|
|
@@ -126,15 +129,19 @@ function parseArgs(argv) {
|
|
|
126
129
|
|
|
127
130
|
function printUsage() {
|
|
128
131
|
console.log(`Usage:
|
|
129
|
-
tasksai-install <product-id> [install] [--source <repo-url>] [--ref <branch>] [--client claude-desktop|cursor|windsurf|all] [--auth browser|license-key]
|
|
130
|
-
tasksai-install <product-id> doctor [--client claude-desktop|cursor|windsurf|all]
|
|
131
|
-
tasksai-install <product-id> update
|
|
132
|
-
tasksai-install <product-id> uninstall [--client claude-desktop|cursor|windsurf|all]
|
|
132
|
+
tasksai-install <product-id> [install] [--source <repo-url>] [--ref <branch>] [--client claude-desktop|cursor|windsurf|all] [--auth browser|license-key] [--install-dir <path>]
|
|
133
|
+
tasksai-install <product-id> doctor [--client claude-desktop|cursor|windsurf|all] [--install-dir <path>]
|
|
134
|
+
tasksai-install <product-id> update [--install-dir <path>]
|
|
135
|
+
tasksai-install <product-id> uninstall [--client claude-desktop|cursor|windsurf|all] [--install-dir <path>]
|
|
133
136
|
|
|
134
137
|
Examples:
|
|
135
138
|
tasksai-install lawtasksai --source https://github.com/laudoluxDev/lawtasksai-mcp
|
|
136
139
|
tasksai-install farmer --source https://github.com/laudoluxDev/farmertasksai-mcp
|
|
140
|
+
tasksai-install farmer --install-dir /tmp/tasksai/farmer
|
|
137
141
|
tasksai-install lawtasksai doctor
|
|
142
|
+
|
|
143
|
+
Environment:
|
|
144
|
+
TASKSAI_INSTALL_DIR may be used instead of --install-dir.
|
|
138
145
|
`);
|
|
139
146
|
}
|
|
140
147
|
|
|
@@ -144,9 +151,12 @@ async function install(options, { updateOnly = false } = {}) {
|
|
|
144
151
|
const vertical = await loadJson(source, "vertical.json");
|
|
145
152
|
verifySource({ options, source, manifest, vertical });
|
|
146
153
|
|
|
147
|
-
const installDir = getInstallDir(options.productId);
|
|
154
|
+
const installDir = getInstallDir(options.productId, options);
|
|
148
155
|
const runtimeDir = path.join(installDir, "runtime");
|
|
149
156
|
const vendorDir = path.join(installDir, "python");
|
|
157
|
+
const clients = updateOnly ? [] : resolveClients(options.client);
|
|
158
|
+
await preflightWriteAccess({ operation: updateOnly ? "update" : "install", installDir, clients });
|
|
159
|
+
|
|
150
160
|
await fsp.mkdir(runtimeDir, { recursive: true });
|
|
151
161
|
await fsp.mkdir(path.join(installDir, "logs"), { recursive: true });
|
|
152
162
|
await logEvent(installDir, "install_start", {
|
|
@@ -181,7 +191,6 @@ async function install(options, { updateOnly = false } = {}) {
|
|
|
181
191
|
return;
|
|
182
192
|
}
|
|
183
193
|
|
|
184
|
-
const clients = resolveClients(options.client);
|
|
185
194
|
for (const client of clients) {
|
|
186
195
|
await configureMcpClient({ client, vertical, installDir, runtimeDir, vendorDir });
|
|
187
196
|
}
|
|
@@ -196,7 +205,7 @@ async function install(options, { updateOnly = false } = {}) {
|
|
|
196
205
|
}
|
|
197
206
|
|
|
198
207
|
async function doctor(options, { quietSuccess = false } = {}) {
|
|
199
|
-
const installDir = getInstallDir(options.productId);
|
|
208
|
+
const installDir = getInstallDir(options.productId, options);
|
|
200
209
|
const verticalPath = path.join(installDir, "vertical.json");
|
|
201
210
|
const envPath = path.join(installDir, ".env");
|
|
202
211
|
const serverPath = path.join(installDir, "runtime", "server.py");
|
|
@@ -224,7 +233,7 @@ async function doctor(options, { quietSuccess = false } = {}) {
|
|
|
224
233
|
throw new Error(`TasksAI doctor found issues:\n- ${problems.join("\n- ")}`);
|
|
225
234
|
}
|
|
226
235
|
|
|
227
|
-
await
|
|
236
|
+
await safeLogEvent(installDir, "doctor_passed", {
|
|
228
237
|
productId: options.productId,
|
|
229
238
|
clients: clients.map((client) => client.id)
|
|
230
239
|
});
|
|
@@ -232,8 +241,9 @@ async function doctor(options, { quietSuccess = false } = {}) {
|
|
|
232
241
|
}
|
|
233
242
|
|
|
234
243
|
async function uninstall(options) {
|
|
235
|
-
const installDir = getInstallDir(options.productId);
|
|
244
|
+
const installDir = getInstallDir(options.productId, options);
|
|
236
245
|
const clients = resolveClients(options.client);
|
|
246
|
+
await preflightWriteAccess({ operation: "uninstall", installDir, clients });
|
|
237
247
|
|
|
238
248
|
for (const client of clients) {
|
|
239
249
|
const configPath = client.configPath();
|
|
@@ -299,6 +309,79 @@ function resolveClients(clientOption) {
|
|
|
299
309
|
return [client];
|
|
300
310
|
}
|
|
301
311
|
|
|
312
|
+
async function preflightWriteAccess({ operation, installDir, clients }) {
|
|
313
|
+
const checks = [
|
|
314
|
+
{
|
|
315
|
+
label: "TasksAI install directory",
|
|
316
|
+
targetPath: installDir,
|
|
317
|
+
kind: "directory"
|
|
318
|
+
}
|
|
319
|
+
];
|
|
320
|
+
|
|
321
|
+
for (const client of clients) {
|
|
322
|
+
const configPath = client.configPath();
|
|
323
|
+
checks.push({
|
|
324
|
+
label: `${client.displayName} config directory`,
|
|
325
|
+
targetPath: path.dirname(configPath),
|
|
326
|
+
kind: "directory"
|
|
327
|
+
});
|
|
328
|
+
if (fs.existsSync(configPath)) {
|
|
329
|
+
checks.push({
|
|
330
|
+
label: `${client.displayName} config file`,
|
|
331
|
+
targetPath: configPath,
|
|
332
|
+
kind: "file"
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const failures = [];
|
|
338
|
+
for (const check of checks) {
|
|
339
|
+
const problem = await checkWritable(check);
|
|
340
|
+
if (problem) failures.push(problem);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (!failures.length) return;
|
|
344
|
+
|
|
345
|
+
const detail = failures
|
|
346
|
+
.map((failure) => `- ${failure.label}: ${failure.targetPath} (${failure.reason})`)
|
|
347
|
+
.join("\n");
|
|
348
|
+
throw new Error(
|
|
349
|
+
`TasksAI cannot ${operation} until it has write access to the required local paths:\n${detail}\n` +
|
|
350
|
+
"Grant write access to these paths, or use --install-dir /path/to/product-dir for the TasksAI runtime."
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function checkWritable({ label, targetPath, kind }) {
|
|
355
|
+
const resolvedPath = resolveUserPath(targetPath);
|
|
356
|
+
try {
|
|
357
|
+
if (fs.existsSync(resolvedPath)) {
|
|
358
|
+
const stat = await fsp.stat(resolvedPath);
|
|
359
|
+
if (kind === "directory" && !stat.isDirectory()) {
|
|
360
|
+
return { label, targetPath: resolvedPath, reason: "expected a directory" };
|
|
361
|
+
}
|
|
362
|
+
if (kind === "file" && !stat.isFile()) {
|
|
363
|
+
return { label, targetPath: resolvedPath, reason: "expected a file" };
|
|
364
|
+
}
|
|
365
|
+
await fsp.access(resolvedPath, fs.constants.W_OK);
|
|
366
|
+
if (kind === "file") {
|
|
367
|
+
await fsp.access(path.dirname(resolvedPath), fs.constants.W_OK);
|
|
368
|
+
}
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const basePath = kind === "file" ? path.dirname(resolvedPath) : resolvedPath;
|
|
373
|
+
const ancestor = findExistingAncestor(basePath);
|
|
374
|
+
await fsp.access(ancestor, fs.constants.W_OK);
|
|
375
|
+
return null;
|
|
376
|
+
} catch (error) {
|
|
377
|
+
return {
|
|
378
|
+
label,
|
|
379
|
+
targetPath: resolvedPath,
|
|
380
|
+
reason: error.code === "EACCES" || error.code === "EPERM" ? "permission denied" : error.message
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
302
385
|
async function downloadRuntime(source, runtimeDir) {
|
|
303
386
|
const serverText = await loadText(source, "server.py");
|
|
304
387
|
const requirementsText = await loadText(source, "requirements.txt");
|
|
@@ -552,7 +635,10 @@ function delay(ms) {
|
|
|
552
635
|
});
|
|
553
636
|
}
|
|
554
637
|
|
|
555
|
-
function getInstallDir(productId) {
|
|
638
|
+
function getInstallDir(productId, options = {}) {
|
|
639
|
+
if (options.installDir) {
|
|
640
|
+
return resolveUserPath(options.installDir);
|
|
641
|
+
}
|
|
556
642
|
if (process.platform === "darwin") {
|
|
557
643
|
return path.join(os.homedir(), "Library", "Application Support", "TasksAI", productId);
|
|
558
644
|
}
|
|
@@ -562,6 +648,25 @@ function getInstallDir(productId) {
|
|
|
562
648
|
return path.join(os.homedir(), ".local", "share", "TasksAI", productId);
|
|
563
649
|
}
|
|
564
650
|
|
|
651
|
+
function resolveUserPath(value) {
|
|
652
|
+
if (!value) return value;
|
|
653
|
+
if (value === "~") return os.homedir();
|
|
654
|
+
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
655
|
+
return path.join(os.homedir(), value.slice(2));
|
|
656
|
+
}
|
|
657
|
+
return path.resolve(value);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function findExistingAncestor(targetPath) {
|
|
661
|
+
let current = resolveUserPath(targetPath);
|
|
662
|
+
while (!fs.existsSync(current)) {
|
|
663
|
+
const parent = path.dirname(current);
|
|
664
|
+
if (parent === current) return current;
|
|
665
|
+
current = parent;
|
|
666
|
+
}
|
|
667
|
+
return current;
|
|
668
|
+
}
|
|
669
|
+
|
|
565
670
|
async function withLock(lockPath, callback) {
|
|
566
671
|
let handle;
|
|
567
672
|
try {
|
|
@@ -626,6 +731,14 @@ async function logEvent(installDir, event, details = {}) {
|
|
|
626
731
|
await fsp.appendFile(path.join(logDir, "installer.log"), line, "utf8");
|
|
627
732
|
}
|
|
628
733
|
|
|
734
|
+
async function safeLogEvent(installDir, event, details = {}) {
|
|
735
|
+
try {
|
|
736
|
+
await logEvent(installDir, event, details);
|
|
737
|
+
} catch {
|
|
738
|
+
// Health checks should report installation health, not fail on log writes.
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
629
742
|
function formatError(error) {
|
|
630
743
|
const message = error?.message || String(error);
|
|
631
744
|
return `TasksAI installer failed: ${redact(message)}`;
|