feat: add clean command to clean manifest and trip dev deps for node (#67)

* feat: add clean command to clean manifest and trip dev deps for node

* fix: lazy import no loop

* Update src/cli/cli.ts

Co-authored-by: Felix Rieseberg <f@anthropic.com>

---------

Co-authored-by: Felix Rieseberg <f@anthropic.com>
This commit is contained in:
Samuel Attard 2025-07-11 15:30:08 -07:00 • committed by GitHub
commit 505c81a258
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 250 additions and 3 deletions

View file

@ -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 <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]")

View file

@ -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 {
@ -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
src/schemas-loose.ts Normal file
View file

@ -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(),
});