chore: v2.8.13 — Artifacts reinstall/edit/installs, agent steps in chat, pick a local file to attach, Windows paths as links

- Chat: an Agent step shows its task, status and tool calls and opens its conversation in place; Windows paths (E:\…, C:/…) in replies open as files
- Prompt: attach a file on this computer by path (native picker), clearer too-large messages
- Artifacts: edit title/description and a skill's files (new snapshot), Reinstall, install history, Hub-only rows, skills installed into ~/.claude/skills and ~/.agents/skills
- Files: batch upload of small files in one extract
- AI Tools: npm install falls back to --prefix ~/.local when the global prefix is not writable
This commit is contained in:
2026-10-20 16:44:55 +07:00
parent 49f87c4da3
commit 0547488f13
59 changed files with 4285 additions and 690 deletions
+78 -5
View File
@@ -5,10 +5,15 @@
* standalone build, say — is never offered an npm update: it would install a
* second copy that the PATH order shadows. npm is the LOGIN SHELL's (its PATH
* first), not the pinned Node's beside the server. Cached for a day.
* A system-wide Node (`/usr/local`, root's) leaves npm's global prefix
* unwritable to the dashboard's account, so there a global install goes to the
* account's own `~/.local` (`--prefix`, bin in `~/.local/bin`) instead, and a
* tool found under `~/.local/lib/node_modules` counts as npm-managed there.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { execFile } = require("node:child_process");
const aiTools = require("./ai-tools");
@@ -27,6 +32,8 @@ const defaultExec = (file, args, opts) =>
)
);
let exec = defaultExec;
let userPrefixImpl = () =>
process.platform === "win32" ? null : path.join(os.homedir(), ".local");
/** The PATH a job runs with: the shell's own order first, then the server's. */
const jobPath = () => [shellPath(), process.env.PATH].filter(Boolean).join(path.delimiter);
@@ -62,17 +69,63 @@ function under(prefix, file) {
}
}
/** Whether tool `id` is installed AND lives under npm's global prefix. */
async function npmManaged(id, prefix) {
/** The account's own npm prefix (`~/.local`), or null where npm has none (Windows). */
const userPrefix = () => userPrefixImpl();
/** Whether npm can write `dir` — or, when it is not there yet, its nearest existing parent. */
function writable(dir) {
let d = dir;
for (;;) {
try {
fs.accessSync(d, fs.constants.W_OK);
return true;
} catch (err) {
if (err.code !== "ENOENT") return false;
}
const up = path.dirname(d);
if (up === d) return false;
d = up;
}
}
/** Whether a global install into `prefix` would succeed: its package and bin folders. */
const prefixWritable = (prefix) =>
process.platform === "win32"
? writable(prefix)
: writable(path.join(prefix, "lib", "node_modules")) && writable(path.join(prefix, "bin"));
/**
* The `--prefix` a global install needs: none while npm's own global prefix is
* writable, else the account's `~/.local` (a system-wide Node: EACCES otherwise).
*/
async function installPrefixArgs() {
const npm = npmBin();
const prefix = npm ? await npmPrefix(npm) : null;
const up = userPrefix();
if (!prefix || !up || prefixWritable(prefix)) return [];
return ["--prefix", up];
}
/**
* Which npm prefix holds tool `id`: `"global"` (npm's own), `"user"` (the
* account's `~/.local/lib/node_modules`), or null — not installed, or not by npm.
*/
async function managedBy(id, prefix) {
const d = aiTools.detect(id);
if (!d.installed) return false;
if (!d.installed) return null;
if (prefix === undefined) {
const npm = npmBin();
prefix = npm ? await npmPrefix(npm) : null;
}
return !!prefix && under(prefix, d.path);
if (prefix && under(prefix, d.path)) return "global";
const up = userPrefix();
if (up && under(path.join(up, "lib", "node_modules"), d.path)) return "user";
return null;
}
/** Whether tool `id` is installed AND lives under npm's global or the account's prefix. */
const npmManaged = async (id, prefix) => !!(await managedBy(id, prefix));
async function compute() {
const npm = npmBin();
const prefix = npm ? await npmPrefix(npm) : null;
@@ -114,10 +167,30 @@ function invalidate() {
cache = null;
}
/** Test seam: the account's npm prefix (a path, or null for none); undefined restores it. */
function __setUserPrefix(p) {
userPrefixImpl =
p === undefined
? () => (process.platform === "win32" ? null : path.join(os.homedir(), ".local"))
: () => p;
}
/** Test seam: `fn(file, args, opts) → Promise<{stdout}>`, or null for execFile. */
function __setExec(fn) {
exec = fn || defaultExec;
cache = null;
}
module.exports = { versions, invalidate, npmManaged, npmBin, jobPath, parseVersion, __setExec };
module.exports = {
versions,
invalidate,
npmManaged,
managedBy,
installPrefixArgs,
userPrefix,
npmBin,
jobPath,
parseVersion,
__setExec,
__setUserPrefix,
};