Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
12 changes: 11 additions & 1 deletion src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -74,6 +74,16 @@ program
process.exit(success ? 0 : 1);
});

// Clean command
program
.command("clean <dxt>")
.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]")
Expand Down
76 changes: 76 additions & 0 deletions src/node/validate.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
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 { 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 {
Expand Down Expand Up @@ -50,3 +56,73 @@ 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);
const { packExtension } = await import("../cli/pack.js");
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,
});
}
}
114 changes: 114 additions & 0 deletions src/schemas-loose.ts
Original file line number Diff line number Diff line change
@@ -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(),
});
47 changes: 46 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2120,13 +2120,30 @@ 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"
integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==
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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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==
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
Loading