From 7b74c3fdca328f5141ff2a7bdb334b220ed43305 Mon Sep 17 00:00:00 2001 From: ykb Date: Tue, 22 Sep 2026 20:26:24 +0800 Subject: [PATCH] fix: isolate npx global mode and release v1.0.31 --- cmd/update/update.go | 3 ++- cmd/update/update_test.go | 5 +++-- package-lock.json | 4 ++-- package.json | 2 +- scripts/skills.js | 3 ++- scripts/skills.test.js | 32 ++++++++++++++++++++++++++++++++ 6 files changed, 42 insertions(+), 7 deletions(-) diff --git a/cmd/update/update.go b/cmd/update/update.go index 4d84715..e83ec8a 100644 --- a/cmd/update/update.go +++ b/cmd/update/update.go @@ -101,7 +101,8 @@ func installSkills(root string, stderr io.Writer) error { } else if !info.IsDir() { return fmt.Errorf("%s 不是目录", filepath.Join(root, "skills")) } - if err := runInherit(stderr, "npx", "-y", "skills", "add", root, "-g", "-y", "--skill", "*"); err != nil { + // Override inherited npm global mode without changing skills add's -g scope. + if err := runInherit(stderr, "npx", "--global=false", "-y", "skills", "add", root, "-g", "-y", "--skill", "*"); err != nil { return err } return cleanupLegacyGlobalSkills(defaultGlobalSkillsDir()) diff --git a/cmd/update/update_test.go b/cmd/update/update_test.go index 31e2bc1..150a0f6 100644 --- a/cmd/update/update_test.go +++ b/cmd/update/update_test.go @@ -26,13 +26,14 @@ func TestInstallSkillsInstallsAllBundledSkills(t *testing.T) { binDir := t.TempDir() capturePath := filepath.Join(t.TempDir(), "args.txt") npxPath := filepath.Join(binDir, "npx") - script := "#!/bin/sh\nfor arg in \"$@\"; do printf '%s\\n' \"$arg\"; done > \"$CAPTURE_ARGS\"\n" + script := "#!/bin/sh\nif [ \"$npm_config_global\" = true ] && [ \"$1\" != --global=false ]; then exit 1; fi\nfor arg in \"$@\"; do printf '%s\\n' \"$arg\"; done > \"$CAPTURE_ARGS\"\n" if err := os.WriteFile(npxPath, []byte(script), 0o755); err != nil { t.Fatal(err) } t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH")) t.Setenv("CAPTURE_ARGS", capturePath) + t.Setenv("npm_config_global", "true") t.Setenv("HOME", t.TempDir()) var stderr bytes.Buffer @@ -45,7 +46,7 @@ func TestInstallSkillsInstallsAllBundledSkills(t *testing.T) { t.Fatal(err) } got := strings.Split(strings.TrimSpace(string(gotBytes)), "\n") - want := []string{"-y", "skills", "add", root, "-g", "-y", "--skill", "*"} + want := []string{"--global=false", "-y", "skills", "add", root, "-g", "-y", "--skill", "*"} if strings.Join(got, "\n") != strings.Join(want, "\n") { t.Fatalf("npx args mismatch\ngot: %#v\nwant: %#v", got, want) } diff --git a/package-lock.json b/package-lock.json index 37d0c12..d424e5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@pippit-dev/cli", - "version": "1.0.30", + "version": "1.0.31", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pippit-dev/cli", - "version": "1.0.30", + "version": "1.0.31", "hasInstallScript": true, "license": "MIT", "bin": { diff --git a/package.json b/package.json index 67b1c15..2174d51 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pippit-dev/cli", - "version": "1.0.30", + "version": "1.0.31", "description": "Pippit CLI", "bin": { "pippit-tool-cli": "scripts/run.js" diff --git a/scripts/skills.js b/scripts/skills.js index 6efff85..755da6d 100644 --- a/scripts/skills.js +++ b/scripts/skills.js @@ -28,7 +28,8 @@ function installSkillsFromRoot(root, opts = {}) { if (!fs.existsSync(skillsDir)) { throw new Error(`skills directory not found: ${skillsDir}`); } - run("npx", ["-y", "skills", "add", source, "-g", "-y", "--skill", "*"], { + // Override npm install -g's inherited mode; the later -g belongs to skills. + run("npx", ["--global=false", "-y", "skills", "add", source, "-g", "-y", "--skill", "*"], { timeout: opts.timeout || 120000, }); cleanupLegacyGlobalSkills(opts.globalSkillsDir); diff --git a/scripts/skills.test.js b/scripts/skills.test.js index 80148ab..038a6fd 100644 --- a/scripts/skills.test.js +++ b/scripts/skills.test.js @@ -2,6 +2,7 @@ const assert = require("assert"); const fs = require("fs"); const os = require("os"); const path = require("path"); +const vm = require("vm"); const { cleanupLegacyGlobalSkills } = require("./skills"); const repoRoot = path.resolve(__dirname, ".."); @@ -10,6 +11,37 @@ const shortDramaSkillPath = path.join(repoRoot, "skills", "short-drama", "SKILL. const marketingSkillPath = path.join(repoRoot, "skills", "xyq-marketing-skill", "SKILL.md"); const readmePath = path.join(repoRoot, "README.md"); +// An npm lifecycle can inherit global=true; npx must override it before the +// command name while retaining skills add's own global installation flag. +const installTestRoot = fs.mkdtempSync(path.join(os.tmpdir(), "pippit skills install ")); +try { + const source = path.join(installTestRoot, "package"); + fs.mkdirSync(path.join(source, "skills"), { recursive: true }); + const inheritedEnv = { npm_config_global: "true", NPM_CONFIG_GLOBAL: "true" }; + const loaded = { exports: {} }; + let calls = 0; + vm.runInNewContext(fs.readFileSync(path.join(__dirname, "skills.js"), "utf8"), { + module: loaded, + process: { env: inheritedEnv }, + require(name) { + if (name !== "./platform") return require(name); + return { + run(command, args, options) { + calls++; + assert.strictEqual(command, "npx"); + assert.deepStrictEqual(Array.from(args), ["--global=false", "-y", "skills", "add", source, "-g", "-y", "--skill", "*"]); + assert.strictEqual(options.timeout, 120000); + }, + }; + }, + }, { filename: "skills.js" }); + loaded.exports.installSkillsFromRoot(source, { globalSkillsDir: path.join(installTestRoot, "global-skills") }); + assert.strictEqual(calls, 1); + assert.deepStrictEqual(inheritedEnv, { npm_config_global: "true", NPM_CONFIG_GLOBAL: "true" }); +} finally { + fs.rmSync(installTestRoot, { recursive: true, force: true }); +} + function readRequiredFile(filePath) { assert.strictEqual(fs.existsSync(filePath), true, `missing required file: ${filePath}`); return fs.readFileSync(filePath, "utf8");