From 4f936e313a51208ba5cb7c5b2d2d4d6aaeccee69 Mon Sep 17 00:00:00 2001 From: Krylix Date: Sun, 11 Jan 2026 15:53:37 -0800 Subject: [PATCH] added toolset compatibility Users can define toolsets in the global tools.json file (~/.enact/tools.json) in the format: { "tools": { "testuser/hello-js": "1.0.0", "examples/hello-simple": "0.1.0", "enact/test/remote-sign-demo": "0.1.1", "enact/context7/docs": "1.0.0", "enact/text-summarizer": "1.0.1", "enact/hello-simple": "0.1.5", "enact/hello-js": "1.0.2", "enact/hello-python": "1.0.0" }, "toolsets": { "test": ["enact/hello-python"] }, "activeToolset": "test" } --- packages/cli/src/commands/index.ts | 1 + packages/cli/src/commands/run/index.ts | 41 ++++++++++++++++ packages/cli/src/commands/search/index.ts | 48 +++++++++++++++--- packages/cli/src/commands/set/index.ts | 59 +++++++++++++++++++++++ packages/cli/src/index.ts | 5 ++ 5 files changed, 146 insertions(+), 8 deletions(-) create mode 100644 packages/cli/src/commands/set/index.ts diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 8d68d41..fefe6ac 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -26,6 +26,7 @@ export { configureCacheCommand } from "./cache"; export { configureSignCommand } from "./sign"; export { configureReportCommand } from "./report"; export { configureInspectCommand } from "./inspect"; +export { configureSetCommand } from "./set"; // API v2 migration commands export { configureYankCommand } from "./yank"; diff --git a/packages/cli/src/commands/run/index.ts b/packages/cli/src/commands/run/index.ts index 60959e6..9241468 100644 --- a/packages/cli/src/commands/run/index.ts +++ b/packages/cli/src/commands/run/index.ts @@ -772,6 +772,47 @@ function displayResult(result: ExecutionResult, options: RunOptions): void { * Run command handler */ async function runHandler(tool: string, options: RunOptions, ctx: CommandContext): Promise { + // === Active toolset restriction logic (global ~/.enact/tools.json) === + // Only applies if resolving by name (not by path) + const isPath = + tool.startsWith("/") || + tool.startsWith("./") || + tool.startsWith("../") || + tool.includes("\\") || + existsSync(tool); + + if (!isPath && !options.remote) { + try { + const fs = require("node:fs"); + const path = require("node:path"); + const os = require("node:os"); + const homeDir = os.homedir(); + const toolsJsonPath = path.join(homeDir, ".enact", "tools.json"); + if (fs.existsSync(toolsJsonPath)) { + const toolsJson = JSON.parse(fs.readFileSync(toolsJsonPath, "utf-8")); + const activeToolset = toolsJson.activeToolset; + if ( + activeToolset && + toolsJson.toolsets && + Array.isArray(toolsJson.toolsets[activeToolset]) + ) { + const allowedTools = toolsJson.toolsets[activeToolset].map((t: string) => + t.toLowerCase().replace(/\\/g, "/").trim() + ); + const requestedTool = tool.toLowerCase().replace(/\\/g, "/").trim(); + if (!allowedTools.includes(requestedTool)) { + error( + `Tool '${tool}' is not in the active toolset '${activeToolset}'. Allowed: ${allowedTools.join(", ")}` + ); + process.exit(EXIT_EXECUTION_ERROR); + } + } + } + } catch (_err) { + // Ignore errors in toolset restriction logic, fallback to normal resolution + } + } + let resolution: ToolResolution | null = null; let resolveResult: ReturnType | null = null; diff --git a/packages/cli/src/commands/search/index.ts b/packages/cli/src/commands/search/index.ts index ad85f14..c06ba4c 100644 --- a/packages/cli/src/commands/search/index.ts +++ b/packages/cli/src/commands/search/index.ts @@ -75,7 +75,7 @@ interface LocalToolInfo { version: string; description: string; location: string; - scope: "project" | "global"; + scope: "project" | "global" | "toolset"; } /** @@ -83,13 +83,13 @@ interface LocalToolInfo { */ function searchLocalTools( query: string, - scope: "project" | "global", + scope: "project" | "global" | "toolset", cwd: string ): LocalToolInfo[] { const tools: LocalToolInfo[] = []; const queryLower = query.toLowerCase(); - if (scope === "global") { + if (scope === "global" || scope === "toolset") { // Search global tools via tools.json const installedTools = listInstalledTools("global"); @@ -109,7 +109,7 @@ function searchLocalTools( version: tool.version, description: loaded?.manifest.description ?? "-", location: tool.cachePath, - scope: "global", + scope: scope, }); } } @@ -151,7 +151,7 @@ function searchLocalTools( version: manifest.version ?? "-", description: manifest.description ?? "-", location: entryPath, - scope: "project", + scope: scope, }); } } else { @@ -177,9 +177,41 @@ async function searchHandler( ctx: CommandContext ): Promise { // Handle local search (--local or -g) - if (options.local || options.global) { - const scope = options.global ? "global" : "project"; - const results = searchLocalTools(query, scope, ctx.cwd); + let activeToolsetAvailable = false; + let restrictToToolset: string[] | undefined = undefined; + + try { + const fs = require("node:fs"); + const path = require("node:path"); + const os = require("node:os"); + const homeDir = os.homedir(); + const toolsJsonPath = path.join(homeDir, ".enact", "tools.json"); + if (fs.existsSync(toolsJsonPath)) { + const toolsJson = JSON.parse(fs.readFileSync(toolsJsonPath, "utf-8")); + const activeToolset = toolsJson.activeToolset; + if (activeToolset && toolsJson.toolsets && Array.isArray(toolsJson.toolsets[activeToolset])) { + restrictToToolset = toolsJson.toolsets[activeToolset].map((t: string) => + t.toLowerCase().replace(/\\/g, "/").trim() + ); + activeToolsetAvailable = true; + } + } + } catch (_err) { + // Ignore errors on loading toolsets for filter + } + + if (options.local || options.global || activeToolsetAvailable) { + let scope: "project" | "global" | "toolset" = options.global ? "global" : "project"; + if (activeToolsetAvailable) { + scope = "toolset"; + } + + let results = searchLocalTools(query, scope, ctx.cwd); + if (restrictToToolset) { + results = results.filter((tool) => + restrictToToolset!.includes(tool.name.toLowerCase().replace(/\\/g, "/").trim()) + ); + } // JSON output if (options.json) { diff --git a/packages/cli/src/commands/set/index.ts b/packages/cli/src/commands/set/index.ts new file mode 100644 index 0000000..8ec3ca4 --- /dev/null +++ b/packages/cli/src/commands/set/index.ts @@ -0,0 +1,59 @@ +/** + * enact set command + * + * Set the active toolset by name in ~/.enact/tools.json + */ + +import type { Command } from "commander"; +import { error, success } from "../../utils"; + +/** + * Handler for the set command + */ +async function setHandler(toolset: string): Promise { + try { + const fs = require("node:fs"); + const path = require("node:path"); + const os = require("node:os"); + const homeDir = os.homedir(); + const toolsJsonPath = path.join(homeDir, ".enact", "tools.json"); + + if (!fs.existsSync(toolsJsonPath)) { + error(`tools.json not found at ${toolsJsonPath}`); + process.exit(1); + } + + const toolsJson = JSON.parse(fs.readFileSync(toolsJsonPath, "utf-8")); + + if (toolset === "NONE") { + toolsJson.activeToolset = undefined; + fs.writeFileSync(toolsJsonPath, JSON.stringify(toolsJson, null, 2)); + success(`Active toolset unset in ${toolsJsonPath}`); + return; + } + + if (!toolsJson.toolsets || !toolsJson.toolsets[toolset]) { + error(`Toolset '${toolset}' not found in tools.json`); + process.exit(1); + } + + toolsJson.activeToolset = toolset; + fs.writeFileSync(toolsJsonPath, JSON.stringify(toolsJson, null, 2)); + success(`Active toolset set to '${toolset}' in ${toolsJsonPath}`); + } catch (err) { + error(`Failed to set active toolset: ${err}`); + process.exit(1); + } +} + +/** + * Configure the set command + */ +export function configureSetCommand(program: Command): void { + program + .command("set ") + .description( + "Set the active toolset by name (writes to ~/.enact/tools.json). Use 'NONE' to unset." + ) + .action(setHandler); +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 2adc5ea..b494620 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -26,6 +26,7 @@ import { configureReportCommand, configureRunCommand, configureSearchCommand, + configureSetCommand, configureSetupCommand, configureSignCommand, configureTrustCommand, @@ -54,6 +55,7 @@ async function main() { .version(version, "-v, --version", "output the version number"); // Configure all commands + configureSetupCommand(program); configureInitCommand(program); configureRunCommand(program); @@ -86,6 +88,9 @@ async function main() { // MCP integration commands configureMcpCommand(program); + // Set active toolset command + configureSetCommand(program); + // Validation command configureValidateCommand(program);