From c0c01d31b9063554dfef73ac7bf4767dc741d5ab Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Fri, 11 Jul 2025 15:24:33 -0700 Subject: [PATCH 1/3] feat: add clean command to clean manifest and trip dev deps for node --- package.json | 4 +- src/cli/cli.ts | 12 ++++- src/node/validate.ts | 76 +++++++++++++++++++++++++++++ src/schemas-loose.ts | 114 +++++++++++++++++++++++++++++++++++++++++++ yarn.lock | 47 +++++++++++++++++- 5 files changed, 250 insertions(+), 3 deletions(-) create mode 100644 src/schemas-loose.ts diff --git a/package.json b/package.json index 3e18740..171a859 100644 --- a/package.json +++ b/package.json @@ -69,12 +69,14 @@ "@inquirer/prompts": "^6.0.1", "commander": "^13.1.0", "fflate": "^0.8.2", + "galactus": "^1.0.0", "ignore": "^7.0.5", "node-forge": "^1.3.1", + "pretty-bytes": "^5.6.0", "zod": "^3.25.67" }, "resolutions": { "@babel/helpers": "7.27.1", "@babel/parser": "7.27.3" } -} \ No newline at end of file +} diff --git a/src/cli/cli.ts b/src/cli/cli.ts index f4c6e37..cb83b99 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -7,7 +7,7 @@ import { basename, dirname, join, resolve } from "path"; import { fileURLToPath } from "url"; import { signDxtFile, unsignDxtFile, verifyDxtFile } from "../node/sign.js"; -import { validateManifest } from "../node/validate.js"; +import { cleanDxt, validateManifest } from "../node/validate.js"; import { initExtension } from "./init.js"; import { packExtension } from "./pack.js"; import { unpackExtension } from "./unpack.js"; @@ -74,6 +74,16 @@ program process.exit(success ? 0 : 1); }); +// Clean command +program + .command("clean ") + .description( + "Cleans a DXT file, validates the manifest and minimizes bundle size", + ) + .action(async (dxtFile: string) => { + await cleanDxt(dxtFile); + }); + // Pack command program .command("pack [directory] [output]") diff --git a/src/node/validate.ts b/src/node/validate.ts index 09afa7e..c25cada 100644 --- a/src/node/validate.ts +++ b/src/node/validate.ts @@ -1,7 +1,14 @@ import { existsSync, readFileSync, statSync } from "fs"; +import * as fs from "fs/promises"; +import { DestroyerOfModules } from "galactus"; +import * as os from "os"; import { join, resolve } from "path"; +import prettyBytes from "pretty-bytes"; +import { packExtension } from "../cli.js"; +import { unpackExtension } from "../cli/unpack.js"; import { DxtManifestSchema } from "../schemas.js"; +import { DxtManifestSchema as LooseDxtManifestSchema } from "../schemas-loose.js"; export function validateManifest(inputPath: string): boolean { try { @@ -50,3 +57,72 @@ export function validateManifest(inputPath: string): boolean { return false; } } + +export async function cleanDxt(inputPath: string) { + const tmpDir = await fs.mkdtemp(resolve(os.tmpdir(), "dxt-clean-")); + const dxtPath = resolve(tmpDir, "in.dxt"); + const unpackPath = resolve(tmpDir, "out"); + + console.log(" -- Cleaning DXT..."); + + try { + await fs.copyFile(inputPath, dxtPath); + console.log(" -- Unpacking DXT..."); + await unpackExtension({ dxtPath, silent: true, outputDir: unpackPath }); + + const manifestPath = resolve(unpackPath, "manifest.json"); + + const originalManifest = await fs.readFile(manifestPath, "utf-8"); + const manifestData = JSON.parse(originalManifest); + + const result = LooseDxtManifestSchema.safeParse(manifestData); + + if (!result.success) { + throw new Error( + `Unrecoverable manifest issues, please run "dxt validate"`, + ); + } + await fs.writeFile(manifestPath, JSON.stringify(result.data, null, 2)); + + if ( + originalManifest.trim() !== + (await fs.readFile(manifestPath, "utf8")).trim() + ) { + console.log(" -- Update manifest to be valid per DXT schema"); + } else { + console.log(" -- Manifest already valid per DXT schema"); + } + + const nodeModulesPath = resolve(unpackPath, "node_modules"); + if (existsSync(nodeModulesPath)) { + console.log(" -- node_modules found, running galactus"); + + const destroyer = new DestroyerOfModules({ + rootDirectory: unpackPath, + }); + await destroyer.destroy(); + + console.log(" -- Galactus pruned node_modules"); + } else { + console.log(" -- No node_modules, not pruning"); + } + + const before = await fs.stat(inputPath); + await packExtension({ + extensionPath: unpackPath, + outputPath: inputPath, + silent: true, + }); + + const after = await fs.stat(inputPath); + + console.log("\nClean Complete:"); + console.log("Before:", prettyBytes(before.size)); + console.log("After:", prettyBytes(after.size)); + } finally { + await fs.rm(tmpDir, { + recursive: true, + force: true, + }); + } +} diff --git a/src/schemas-loose.ts b/src/schemas-loose.ts new file mode 100644 index 0000000..151286f --- /dev/null +++ b/src/schemas-loose.ts @@ -0,0 +1,114 @@ +import * as z from "zod"; + +export const McpServerConfigSchema = z.object({ + command: z.string(), + args: z.array(z.string()).optional(), + env: z.record(z.string(), z.string()).optional(), +}); + +export const DxtManifestAuthorSchema = z.object({ + name: z.string(), + email: z.string().email().optional(), + url: z.string().url().optional(), +}); + +export const DxtManifestRepositorySchema = z.object({ + type: z.string(), + url: z.string().url(), +}); + +export const DxtManifestPlatformOverrideSchema = + McpServerConfigSchema.partial(); + +export const DxtManifestMcpConfigSchema = McpServerConfigSchema.extend({ + platform_overrides: z + .record(z.string(), DxtManifestPlatformOverrideSchema) + .optional(), +}); + +export const DxtManifestServerSchema = z.object({ + type: z.enum(["python", "node", "binary"]), + entry_point: z.string(), + mcp_config: DxtManifestMcpConfigSchema, +}); + +export const DxtManifestCompatibilitySchema = z + .object({ + claude_desktop: z.string().optional(), + platforms: z.array(z.enum(["darwin", "win32", "linux"])).optional(), + runtimes: z + .object({ + python: z.string().optional(), + node: z.string().optional(), + }) + .optional(), + }) + .passthrough(); + +export const DxtManifestToolSchema = z.object({ + name: z.string(), + description: z.string().optional(), +}); + +export const DxtManifestPromptSchema = z.object({ + name: z.string(), + description: z.string().optional(), + arguments: z.array(z.string()).optional(), + text: z.string(), +}); + +export const DxtUserConfigurationOptionSchema = z.object({ + type: z.enum(["string", "number", "boolean", "directory", "file"]), + title: z.string(), + description: z.string(), + required: z.boolean().optional(), + default: z + .union([z.string(), z.number(), z.boolean(), z.array(z.string())]) + .optional(), + multiple: z.boolean().optional(), + sensitive: z.boolean().optional(), + min: z.number().optional(), + max: z.number().optional(), +}); + +export const DxtUserConfigValuesSchema = z.record( + z.string(), + z.union([z.string(), z.number(), z.boolean(), z.array(z.string())]), +); + +export const DxtManifestSchema = z.object({ + $schema: z.string().optional(), + dxt_version: z.string(), + name: z.string(), + display_name: z.string().optional(), + version: z.string(), + description: z.string(), + long_description: z.string().optional(), + author: DxtManifestAuthorSchema, + repository: DxtManifestRepositorySchema.optional(), + homepage: z.string().url().optional(), + documentation: z.string().url().optional(), + support: z.string().url().optional(), + icon: z.string().optional(), + screenshots: z.array(z.string()).optional(), + server: DxtManifestServerSchema, + tools: z.array(DxtManifestToolSchema).optional(), + tools_generated: z.boolean().optional(), + prompts: z.array(DxtManifestPromptSchema).optional(), + prompts_generated: z.boolean().optional(), + keywords: z.array(z.string()).optional(), + license: z.string().optional(), + compatibility: DxtManifestCompatibilitySchema.optional(), + user_config: z + .record(z.string(), DxtUserConfigurationOptionSchema) + .optional(), +}); + +export const DxtSignatureInfoSchema = z.object({ + status: z.enum(["signed", "unsigned", "self-signed"]), + publisher: z.string().optional(), + issuer: z.string().optional(), + valid_from: z.string().optional(), + valid_to: z.string().optional(), + fingerprint: z.string().optional(), +}); diff --git a/yarn.lock b/yarn.lock index fa4ff4c..a115735 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2120,6 +2120,14 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358" integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== +flora-colossus@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/flora-colossus/-/flora-colossus-2.0.0.tgz#af1e85db0a8256ef05f3fb531c1235236c97220a" + integrity sha512-dz4HxH6pOvbUzZpZ/yXhafjbR2I8cenK5xL0KtBFb7U2ADsR+OwXifnxZjij/pZWF775uSCMzWVd+jDik2H2IA== + dependencies: + debug "^4.3.4" + fs-extra "^10.1.0" + for-each@^0.3.3, for-each@^0.3.5: version "0.3.5" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47" @@ -2127,6 +2135,15 @@ for-each@^0.3.3, for-each@^0.3.5: dependencies: is-callable "^1.2.7" +fs-extra@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -2159,6 +2176,15 @@ functions-have-names@^1.2.3: resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== +galactus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/galactus/-/galactus-1.0.0.tgz#c2615182afa0c6d0859b92e56ae36d052827db7e" + integrity sha512-R1fam6D4CyKQGNlvJne4dkNF+PvUUl7TAJInvTGa9fti9qAv95quQz29GXapA4d8Ec266mJJxFVh82M4GIIGDQ== + dependencies: + debug "^4.3.4" + flora-colossus "^2.0.0" + fs-extra "^10.1.0" + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -2282,7 +2308,7 @@ gopd@^1.0.1, gopd@^1.2.0: resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== -graceful-fs@^4.2.9: +graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -3107,6 +3133,15 @@ json5@^2.2.3: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + keyv@^4.5.3: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" @@ -3501,6 +3536,11 @@ prettier@^3.3.3: resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.6.1.tgz#cc3bce21c09a477b1e987b76ce9663925d86ae44" integrity sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A== +pretty-bytes@^5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" + integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== + pretty-format@^29.0.0, pretty-format@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" @@ -4090,6 +4130,11 @@ undici-types@~7.8.0: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.8.0.tgz#de00b85b710c54122e44fbfd911f8d70174cd294" integrity sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw== +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + unrs-resolver@^1.6.2: version "1.9.2" resolved "https://registry.yarnpkg.com/unrs-resolver/-/unrs-resolver-1.9.2.tgz#1a7c73335a5e510643664d7bb4bb6f5c28782e36" From 7567aeb9401edd132e71e6a850a2540d6d351520 Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Fri, 11 Jul 2025 15:27:18 -0700 Subject: [PATCH 2/3] fix: lazy import no loop --- src/node/validate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/validate.ts b/src/node/validate.ts index c25cada..bf00437 100644 --- a/src/node/validate.ts +++ b/src/node/validate.ts @@ -5,7 +5,6 @@ import * as os from "os"; import { join, resolve } from "path"; import prettyBytes from "pretty-bytes"; -import { packExtension } from "../cli.js"; import { unpackExtension } from "../cli/unpack.js"; import { DxtManifestSchema } from "../schemas.js"; import { DxtManifestSchema as LooseDxtManifestSchema } from "../schemas-loose.js"; @@ -108,6 +107,7 @@ export async function cleanDxt(inputPath: string) { } const before = await fs.stat(inputPath); + const { packExtension } = await import("../cli/pack.js"); await packExtension({ extensionPath: unpackPath, outputPath: inputPath, From d375ce3c42ea7e8ed442efaf53c1d9ec17f678a8 Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Fri, 11 Jul 2025 15:29:18 -0700 Subject: [PATCH 3/3] Update src/cli/cli.ts Co-authored-by: Felix Rieseberg --- src/cli/cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/cli.ts b/src/cli/cli.ts index cb83b99..b9a7fd0 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -78,7 +78,7 @@ program program .command("clean ") .description( - "Cleans a DXT file, validates the manifest and minimizes bundle size", + "Cleans a DXT file, validates the manifest, and minimizes bundle size", ) .action(async (dxtFile: string) => { await cleanDxt(dxtFile);