Skip to content

Commit 505c81a

Browse files
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>
1 parent 9523b5a commit 505c81a

File tree

5 files changed

+250
-3
lines changed

5 files changed

+250
-3
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,14 @@
6969
"@inquirer/prompts": "^6.0.1",
7070
"commander": "^13.1.0",
7171
"fflate": "^0.8.2",
72+
"galactus": "^1.0.0",
7273
"ignore": "^7.0.5",
7374
"node-forge": "^1.3.1",
75+
"pretty-bytes": "^5.6.0",
7476
"zod": "^3.25.67"
7577
},
7678
"resolutions": {
7779
"@babel/helpers": "7.27.1",
7880
"@babel/parser": "7.27.3"
7981
}
80-
}
82+
}

src/cli/cli.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { basename, dirname, join, resolve } from "path";
77
import { fileURLToPath } from "url";
88

99
import { signDxtFile, unsignDxtFile, verifyDxtFile } from "../node/sign.js";
10-
import { validateManifest } from "../node/validate.js";
10+
import { cleanDxt, validateManifest } from "../node/validate.js";
1111
import { initExtension } from "./init.js";
1212
import { packExtension } from "./pack.js";
1313
import { unpackExtension } from "./unpack.js";
@@ -74,6 +74,16 @@ program
7474
process.exit(success ? 0 : 1);
7575
});
7676

77+
// Clean command
78+
program
79+
.command("clean <dxt>")
80+
.description(
81+
"Cleans a DXT file, validates the manifest, and minimizes bundle size",
82+
)
83+
.action(async (dxtFile: string) => {
84+
await cleanDxt(dxtFile);
85+
});
86+
7787
// Pack command
7888
program
7989
.command("pack [directory] [output]")

src/node/validate.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { existsSync, readFileSync, statSync } from "fs";
2+
import * as fs from "fs/promises";
3+
import { DestroyerOfModules } from "galactus";
4+
import * as os from "os";
25
import { join, resolve } from "path";
6+
import prettyBytes from "pretty-bytes";
37

8+
import { unpackExtension } from "../cli/unpack.js";
49
import { DxtManifestSchema } from "../schemas.js";
10+
import { DxtManifestSchema as LooseDxtManifestSchema } from "../schemas-loose.js";
511

612
export function validateManifest(inputPath: string): boolean {
713
try {
@@ -50,3 +56,73 @@ export function validateManifest(inputPath: string): boolean {
5056
return false;
5157
}
5258
}
59+
60+
export async function cleanDxt(inputPath: string) {
61+
const tmpDir = await fs.mkdtemp(resolve(os.tmpdir(), "dxt-clean-"));
62+
const dxtPath = resolve(tmpDir, "in.dxt");
63+
const unpackPath = resolve(tmpDir, "out");
64+
65+
console.log(" -- Cleaning DXT...");
66+
67+
try {
68+
await fs.copyFile(inputPath, dxtPath);
69+
console.log(" -- Unpacking DXT...");
70+
await unpackExtension({ dxtPath, silent: true, outputDir: unpackPath });
71+
72+
const manifestPath = resolve(unpackPath, "manifest.json");
73+
74+
const originalManifest = await fs.readFile(manifestPath, "utf-8");
75+
const manifestData = JSON.parse(originalManifest);
76+
77+
const result = LooseDxtManifestSchema.safeParse(manifestData);
78+
79+
if (!result.success) {
80+
throw new Error(
81+
`Unrecoverable manifest issues, please run "dxt validate"`,
82+
);
83+
}
84+
await fs.writeFile(manifestPath, JSON.stringify(result.data, null, 2));
85+
86+
if (
87+
originalManifest.trim() !==
88+
(await fs.readFile(manifestPath, "utf8")).trim()
89+
) {
90+
console.log(" -- Update manifest to be valid per DXT schema");
91+
} else {
92+
console.log(" -- Manifest already valid per DXT schema");
93+
}
94+
95+
const nodeModulesPath = resolve(unpackPath, "node_modules");
96+
if (existsSync(nodeModulesPath)) {
97+
console.log(" -- node_modules found, running galactus");
98+
99+
const destroyer = new DestroyerOfModules({
100+
rootDirectory: unpackPath,
101+
});
102+
await destroyer.destroy();
103+
104+
console.log(" -- Galactus pruned node_modules");
105+
} else {
106+
console.log(" -- No node_modules, not pruning");
107+
}
108+
109+
const before = await fs.stat(inputPath);
110+
const { packExtension } = await import("../cli/pack.js");
111+
await packExtension({
112+
extensionPath: unpackPath,
113+
outputPath: inputPath,
114+
silent: true,
115+
});
116+
117+
const after = await fs.stat(inputPath);
118+
119+
console.log("\nClean Complete:");
120+
console.log("Before:", prettyBytes(before.size));
121+
console.log("After:", prettyBytes(after.size));
122+
} finally {
123+
await fs.rm(tmpDir, {
124+
recursive: true,
125+
force: true,
126+
});
127+
}
128+
}

src/schemas-loose.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import * as z from "zod";
2+
3+
export const McpServerConfigSchema = z.object({
4+
command: z.string(),
5+
args: z.array(z.string()).optional(),
6+
env: z.record(z.string(), z.string()).optional(),
7+
});
8+
9+
export const DxtManifestAuthorSchema = z.object({
10+
name: z.string(),
11+
email: z.string().email().optional(),
12+
url: z.string().url().optional(),
13+
});
14+
15+
export const DxtManifestRepositorySchema = z.object({
16+
type: z.string(),
17+
url: z.string().url(),
18+
});
19+
20+
export const DxtManifestPlatformOverrideSchema =
21+
McpServerConfigSchema.partial();
22+
23+
export const DxtManifestMcpConfigSchema = McpServerConfigSchema.extend({
24+
platform_overrides: z
25+
.record(z.string(), DxtManifestPlatformOverrideSchema)
26+
.optional(),
27+
});
28+
29+
export const DxtManifestServerSchema = z.object({
30+
type: z.enum(["python", "node", "binary"]),
31+
entry_point: z.string(),
32+
mcp_config: DxtManifestMcpConfigSchema,
33+
});
34+
35+
export const DxtManifestCompatibilitySchema = z
36+
.object({
37+
claude_desktop: z.string().optional(),
38+
platforms: z.array(z.enum(["darwin", "win32", "linux"])).optional(),
39+
runtimes: z
40+
.object({
41+
python: z.string().optional(),
42+
node: z.string().optional(),
43+
})
44+
.optional(),
45+
})
46+
.passthrough();
47+
48+
export const DxtManifestToolSchema = z.object({
49+
name: z.string(),
50+
description: z.string().optional(),
51+
});
52+
53+
export const DxtManifestPromptSchema = z.object({
54+
name: z.string(),
55+
description: z.string().optional(),
56+
arguments: z.array(z.string()).optional(),
57+
text: z.string(),
58+
});
59+
60+
export const DxtUserConfigurationOptionSchema = z.object({
61+
type: z.enum(["string", "number", "boolean", "directory", "file"]),
62+
title: z.string(),
63+
description: z.string(),
64+
required: z.boolean().optional(),
65+
default: z
66+
.union([z.string(), z.number(), z.boolean(), z.array(z.string())])
67+
.optional(),
68+
multiple: z.boolean().optional(),
69+
sensitive: z.boolean().optional(),
70+
min: z.number().optional(),
71+
max: z.number().optional(),
72+
});
73+
74+
export const DxtUserConfigValuesSchema = z.record(
75+
z.string(),
76+
z.union([z.string(), z.number(), z.boolean(), z.array(z.string())]),
77+
);
78+
79+
export const DxtManifestSchema = z.object({
80+
$schema: z.string().optional(),
81+
dxt_version: z.string(),
82+
name: z.string(),
83+
display_name: z.string().optional(),
84+
version: z.string(),
85+
description: z.string(),
86+
long_description: z.string().optional(),
87+
author: DxtManifestAuthorSchema,
88+
repository: DxtManifestRepositorySchema.optional(),
89+
homepage: z.string().url().optional(),
90+
documentation: z.string().url().optional(),
91+
support: z.string().url().optional(),
92+
icon: z.string().optional(),
93+
screenshots: z.array(z.string()).optional(),
94+
server: DxtManifestServerSchema,
95+
tools: z.array(DxtManifestToolSchema).optional(),
96+
tools_generated: z.boolean().optional(),
97+
prompts: z.array(DxtManifestPromptSchema).optional(),
98+
prompts_generated: z.boolean().optional(),
99+
keywords: z.array(z.string()).optional(),
100+
license: z.string().optional(),
101+
compatibility: DxtManifestCompatibilitySchema.optional(),
102+
user_config: z
103+
.record(z.string(), DxtUserConfigurationOptionSchema)
104+
.optional(),
105+
});
106+
107+
export const DxtSignatureInfoSchema = z.object({
108+
status: z.enum(["signed", "unsigned", "self-signed"]),
109+
publisher: z.string().optional(),
110+
issuer: z.string().optional(),
111+
valid_from: z.string().optional(),
112+
valid_to: z.string().optional(),
113+
fingerprint: z.string().optional(),
114+
});

yarn.lock

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2120,13 +2120,30 @@ flatted@^3.2.9:
21202120
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358"
21212121
integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==
21222122

2123+
flora-colossus@^2.0.0:
2124+
version "2.0.0"
2125+
resolved "https://registry.yarnpkg.com/flora-colossus/-/flora-colossus-2.0.0.tgz#af1e85db0a8256ef05f3fb531c1235236c97220a"
2126+
integrity sha512-dz4HxH6pOvbUzZpZ/yXhafjbR2I8cenK5xL0KtBFb7U2ADsR+OwXifnxZjij/pZWF775uSCMzWVd+jDik2H2IA==
2127+
dependencies:
2128+
debug "^4.3.4"
2129+
fs-extra "^10.1.0"
2130+
21232131
for-each@^0.3.3, for-each@^0.3.5:
21242132
version "0.3.5"
21252133
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47"
21262134
integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==
21272135
dependencies:
21282136
is-callable "^1.2.7"
21292137

2138+
fs-extra@^10.1.0:
2139+
version "10.1.0"
2140+
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf"
2141+
integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==
2142+
dependencies:
2143+
graceful-fs "^4.2.0"
2144+
jsonfile "^6.0.1"
2145+
universalify "^2.0.0"
2146+
21302147
fs.realpath@^1.0.0:
21312148
version "1.0.0"
21322149
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@@ -2159,6 +2176,15 @@ functions-have-names@^1.2.3:
21592176
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
21602177
integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
21612178

2179+
galactus@^1.0.0:
2180+
version "1.0.0"
2181+
resolved "https://registry.yarnpkg.com/galactus/-/galactus-1.0.0.tgz#c2615182afa0c6d0859b92e56ae36d052827db7e"
2182+
integrity sha512-R1fam6D4CyKQGNlvJne4dkNF+PvUUl7TAJInvTGa9fti9qAv95quQz29GXapA4d8Ec266mJJxFVh82M4GIIGDQ==
2183+
dependencies:
2184+
debug "^4.3.4"
2185+
flora-colossus "^2.0.0"
2186+
fs-extra "^10.1.0"
2187+
21622188
gensync@^1.0.0-beta.2:
21632189
version "1.0.0-beta.2"
21642190
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:
22822308
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
22832309
integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
22842310

2285-
graceful-fs@^4.2.9:
2311+
graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.9:
22862312
version "4.2.11"
22872313
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
22882314
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
@@ -3107,6 +3133,15 @@ json5@^2.2.3:
31073133
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
31083134
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
31093135

3136+
jsonfile@^6.0.1:
3137+
version "6.1.0"
3138+
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
3139+
integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
3140+
dependencies:
3141+
universalify "^2.0.0"
3142+
optionalDependencies:
3143+
graceful-fs "^4.1.6"
3144+
31103145
keyv@^4.5.3:
31113146
version "4.5.4"
31123147
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
@@ -3501,6 +3536,11 @@ prettier@^3.3.3:
35013536
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.6.1.tgz#cc3bce21c09a477b1e987b76ce9663925d86ae44"
35023537
integrity sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A==
35033538

3539+
pretty-bytes@^5.6.0:
3540+
version "5.6.0"
3541+
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
3542+
integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==
3543+
35043544
pretty-format@^29.0.0, pretty-format@^29.7.0:
35053545
version "29.7.0"
35063546
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812"
@@ -4090,6 +4130,11 @@ undici-types@~7.8.0:
40904130
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.8.0.tgz#de00b85b710c54122e44fbfd911f8d70174cd294"
40914131
integrity sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==
40924132

4133+
universalify@^2.0.0:
4134+
version "2.0.1"
4135+
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
4136+
integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==
4137+
40934138
unrs-resolver@^1.6.2:
40944139
version "1.9.2"
40954140
resolved "https://registry.yarnpkg.com/unrs-resolver/-/unrs-resolver-1.9.2.tgz#1a7c73335a5e510643664d7bb4bb6f5c28782e36"

0 commit comments

Comments
 (0)