diff --git a/extensions/tsconfig.package-boundary.base.json b/extensions/tsconfig.package-boundary.base.json index cb57a99d484..5f5aa24f08c 100644 --- a/extensions/tsconfig.package-boundary.base.json +++ b/extensions/tsconfig.package-boundary.base.json @@ -1,14 +1,6 @@ { - "extends": "../tsconfig.json", + "extends": "./tsconfig.package-boundary.paths.json", "compilerOptions": { - "ignoreDeprecations": "6.0", - "paths": { - "openclaw/extension-api": ["../src/extensionAPI.ts"], - "openclaw/plugin-sdk": ["../src/plugin-sdk/index.ts"], - "openclaw/plugin-sdk/*": ["../src/plugin-sdk/*.ts"], - "openclaw/plugin-sdk/account-id": ["../src/plugin-sdk/account-id.ts"], - "@openclaw/*": ["../extensions/*"], - "@openclaw/plugin-sdk/*": ["../packages/plugin-sdk/dist/packages/plugin-sdk/src/*.d.ts"] - } + "ignoreDeprecations": "6.0" } } diff --git a/extensions/tsconfig.package-boundary.paths.json b/extensions/tsconfig.package-boundary.paths.json new file mode 100644 index 00000000000..b0e1ca3909f --- /dev/null +++ b/extensions/tsconfig.package-boundary.paths.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "paths": { + "openclaw/extension-api": ["../src/extensionAPI.ts"], + "openclaw/plugin-sdk": ["../src/plugin-sdk/index.ts"], + "openclaw/plugin-sdk/*": ["../src/plugin-sdk/*.ts"], + "openclaw/plugin-sdk/account-id": ["../src/plugin-sdk/account-id.ts"], + "@openclaw/*": ["../extensions/*"], + "@openclaw/plugin-sdk/*": ["../packages/plugin-sdk/dist/packages/plugin-sdk/src/*.d.ts"] + } + } +} diff --git a/src/cli/command-path-matches.test.ts b/src/cli/command-path-matches.test.ts index 084064ff7e8..20422dc0b26 100644 --- a/src/cli/command-path-matches.test.ts +++ b/src/cli/command-path-matches.test.ts @@ -29,6 +29,14 @@ describe("command-path-matches", () => { ).toBe(false); }); + it("treats structured rules without exact as prefix matches", () => { + expect( + matchesCommandPathRule(["plugins", "update", "now"], { + pattern: ["plugins", "update"], + }), + ).toBe(true); + }); + it("matches any command path from a rule set", () => { expect( matchesAnyCommandPath( diff --git a/src/cli/command-path-matches.ts b/src/cli/command-path-matches.ts index a97a9258c2e..74ba63e94a2 100644 --- a/src/cli/command-path-matches.ts +++ b/src/cli/command-path-matches.ts @@ -5,12 +5,24 @@ export type StructuredCommandPathMatchRule = { export type CommandPathMatchRule = readonly string[] | StructuredCommandPathMatchRule; +type NormalizedCommandPathMatchRule = { + pattern: readonly string[]; + exact: boolean; +}; + function isStructuredCommandPathMatchRule( rule: CommandPathMatchRule, ): rule is StructuredCommandPathMatchRule { return !Array.isArray(rule); } +function normalizeCommandPathMatchRule(rule: CommandPathMatchRule): NormalizedCommandPathMatchRule { + if (!isStructuredCommandPathMatchRule(rule)) { + return { pattern: rule, exact: false }; + } + return { pattern: rule.pattern, exact: rule.exact ?? false }; +} + export function matchesCommandPath( commandPath: string[], pattern: readonly string[], @@ -23,10 +35,10 @@ export function matchesCommandPath( } export function matchesCommandPathRule(commandPath: string[], rule: CommandPathMatchRule): boolean { - if (!isStructuredCommandPathMatchRule(rule)) { - return matchesCommandPath(commandPath, rule); - } - return matchesCommandPath(commandPath, rule.pattern, { exact: rule.exact }); + const normalizedRule = normalizeCommandPathMatchRule(rule); + return matchesCommandPath(commandPath, normalizedRule.pattern, { + exact: normalizedRule.exact, + }); } export function matchesAnyCommandPath( diff --git a/src/cli/program/route-specs.ts b/src/cli/program/route-specs.ts index 525f77218bc..4a2d8c1aa94 100644 --- a/src/cli/program/route-specs.ts +++ b/src/cli/program/route-specs.ts @@ -4,7 +4,7 @@ import { matchesCommandPath } from "../command-path-matches.js"; import { resolveCliCommandPathPolicy } from "../command-path-policy.js"; import { routedCommandDefinitions, - type RoutedCommandDefinition, + type AnyRoutedCommandDefinition, } from "./routed-command-definitions.js"; export type RouteSpec = { @@ -22,7 +22,7 @@ function createCommandLoadPlugins(commandPath: readonly string[]): (argv: string function createParsedRoute(params: { entry: CliCommandCatalogEntry; - definition: RoutedCommandDefinition; + definition: AnyRoutedCommandDefinition; }): RouteSpec { return { match: (path) => @@ -51,6 +51,6 @@ export const routedCommands: RouteSpec[] = cliCommandCatalog .map((entry) => createParsedRoute({ entry, - definition: routedCommandDefinitions[entry.route.id] as RoutedCommandDefinition, + definition: routedCommandDefinitions[entry.route.id], }), ); diff --git a/src/cli/program/routed-command-definitions.ts b/src/cli/program/routed-command-definitions.ts index d0b34866d61..7f16aee91b4 100644 --- a/src/cli/program/routed-command-definitions.ts +++ b/src/cli/program/routed-command-definitions.ts @@ -11,16 +11,21 @@ import { parseStatusRouteArgs, } from "./route-args.js"; -export type RoutedCommandDefinition = { - parseArgs: (argv: string[]) => TArgs | null; - runParsedArgs: (args: TArgs) => Promise; +type RouteArgParser = (argv: string[]) => TArgs | null; + +type ParsedRouteArgs> = Exclude, null>; + +export type RoutedCommandDefinition> = { + parseArgs: TParse; + runParsedArgs: (args: ParsedRouteArgs) => Promise; }; -function defineRoutedCommand unknown>(definition: { - parseArgs: TParse; - runParsedArgs: (args: Exclude, null>) => Promise; -}): RoutedCommandDefinition, null>> { - return definition as RoutedCommandDefinition, null>>; +export type AnyRoutedCommandDefinition = RoutedCommandDefinition>; + +function defineRoutedCommand>( + definition: RoutedCommandDefinition, +): RoutedCommandDefinition { + return definition; } export const routedCommandDefinitions = { diff --git a/src/plugins/contracts/extension-package-project-boundaries.test.ts b/src/plugins/contracts/extension-package-project-boundaries.test.ts index 68f9631287e..de18cec2534 100644 --- a/src/plugins/contracts/extension-package-project-boundaries.test.ts +++ b/src/plugins/contracts/extension-package-project-boundaries.test.ts @@ -1,8 +1,12 @@ -import { existsSync, readFileSync } from "node:fs"; +import { existsSync, readFileSync, readdirSync } from "node:fs"; import { resolve } from "node:path"; import { describe, expect, it } from "vitest"; const REPO_ROOT = resolve(import.meta.dirname, "../../.."); +const EXTENSION_PACKAGE_BOUNDARY_PATHS_CONFIG = + "extensions/tsconfig.package-boundary.paths.json" as const; +const EXTENSION_PACKAGE_BOUNDARY_BASE_CONFIG = + "extensions/tsconfig.package-boundary.base.json" as const; type TsConfigJson = { extends?: unknown; @@ -28,10 +32,10 @@ function readJsonFile(relativePath: string): T { } describe("opt-in extension package boundaries", () => { - it("keeps the opt-in extension base on real package resolution", () => { - const tsconfig = readJsonFile("extensions/tsconfig.package-boundary.base.json"); - expect(tsconfig.extends).toBe("../tsconfig.json"); - expect(tsconfig.compilerOptions?.paths).toEqual({ + it("keeps path aliases in a dedicated shared config", () => { + const pathsConfig = readJsonFile(EXTENSION_PACKAGE_BOUNDARY_PATHS_CONFIG); + expect(pathsConfig.extends).toBe("../tsconfig.json"); + expect(pathsConfig.compilerOptions?.paths).toEqual({ "openclaw/extension-api": ["../src/extensionAPI.ts"], "openclaw/plugin-sdk": ["../src/plugin-sdk/index.ts"], "openclaw/plugin-sdk/*": ["../src/plugin-sdk/*.ts"], @@ -39,17 +43,40 @@ describe("opt-in extension package boundaries", () => { "@openclaw/*": ["../extensions/*"], "@openclaw/plugin-sdk/*": ["../packages/plugin-sdk/dist/packages/plugin-sdk/src/*.d.ts"], }); + + const baseConfig = readJsonFile(EXTENSION_PACKAGE_BOUNDARY_BASE_CONFIG); + expect(baseConfig.extends).toBe("./tsconfig.package-boundary.paths.json"); + expect(baseConfig.compilerOptions).toEqual({ + ignoreDeprecations: "6.0", + }); }); - it("roots xai inside its own package and depends on the package sdk", () => { - const tsconfig = readJsonFile("extensions/xai/tsconfig.json"); - expect(tsconfig.extends).toBe("../tsconfig.package-boundary.base.json"); - expect(tsconfig.compilerOptions?.rootDir).toBe("."); - expect(tsconfig.include).toEqual(["./*.ts", "./src/**/*.ts"]); - expect(tsconfig.exclude).toEqual(["./**/*.test.ts", "./dist/**", "./node_modules/**"]); + it("keeps every opt-in extension rooted inside its package and on the package sdk", () => { + const optInExtensions = readdirSync(resolve(REPO_ROOT, "extensions"), { + withFileTypes: true, + }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .filter((extensionName) => { + const tsconfigPath = `extensions/${extensionName}/tsconfig.json`; + if (!existsSync(resolve(REPO_ROOT, tsconfigPath))) { + return false; + } + const tsconfig = readJsonFile(tsconfigPath); + return tsconfig.extends === "../tsconfig.package-boundary.base.json"; + }); - const packageJson = readJsonFile("extensions/xai/package.json"); - expect(packageJson.devDependencies?.["@openclaw/plugin-sdk"]).toBe("workspace:*"); + expect(optInExtensions).toEqual(["xai"]); + + for (const extensionName of optInExtensions) { + const tsconfig = readJsonFile(`extensions/${extensionName}/tsconfig.json`); + expect(tsconfig.compilerOptions?.rootDir).toBe("."); + expect(tsconfig.include).toEqual(["./*.ts", "./src/**/*.ts"]); + expect(tsconfig.exclude).toEqual(["./**/*.test.ts", "./dist/**", "./node_modules/**"]); + + const packageJson = readJsonFile(`extensions/${extensionName}/package.json`); + expect(packageJson.devDependencies?.["@openclaw/plugin-sdk"]).toBe("workspace:*"); + } }); it("keeps plugin-sdk package types generated from the package build, not a hand-maintained types bridge", () => {