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:
parent
9523b5af19
commit
505c81a258
3 changed files with 250 additions and 3 deletions
|
|
@ -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]")
|
||||
|
|
|
|||
|
|
@ -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
114
src/schemas-loose.ts
Normal 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(),
|
||||
});
|
||||
Loading…
Reference in a new issue