Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
43 changes: 27 additions & 16 deletions src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,22 +101,33 @@ program
program
.command("pack [directory] [output]")
.description("Pack a directory into an MCPB extension")
.action((directory: string = process.cwd(), output?: string) => {
void (async () => {
try {
const success = await packExtension({
extensionPath: directory,
outputPath: output,
});
process.exit(success ? 0 : 1);
} catch (error) {
console.error(
`ERROR: ${error instanceof Error ? error.message : "Unknown error"}`,
);
process.exit(1);
}
})();
});
.option(
"-m, --manifest <path>",
"Path to manifest file (defaults to manifest.json in directory)",
)
.action(
(
directory: string = process.cwd(),
output: string | undefined,
options: { manifest?: string },
) => {
void (async () => {
try {
const success = await packExtension({
extensionPath: directory,
outputPath: output,
manifestPath: options.manifest,
});
process.exit(success ? 0 : 1);
} catch (error) {
console.error(
`ERROR: ${error instanceof Error ? error.message : "Unknown error"}`,
);
process.exit(1);
}
})();
},
);

// Unpack command
program
Expand Down
24 changes: 22 additions & 2 deletions src/cli/pack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ interface PackOptions {
extensionPath: string;
outputPath?: string;
silent?: boolean;
manifestPath?: string;
}

function formatFileSize(bytes: number): string {
Expand Down Expand Up @@ -50,6 +51,7 @@ export async function packExtension({
extensionPath,
outputPath,
silent,
manifestPath: customManifestPath,
}: PackOptions): Promise<boolean> {
const resolvedPath = resolve(extensionPath);
const logger = getLogger({ silent });
Expand All @@ -60,9 +62,18 @@ export async function packExtension({
return false;
}

// Check if manifest exists
const manifestPath = join(resolvedPath, "manifest.json");
// Resolve manifest path
const manifestPath = customManifestPath
? resolve(customManifestPath)
: join(resolvedPath, "manifest.json");

if (!existsSync(manifestPath)) {
if (customManifestPath) {
// When --manifest is explicitly provided, error immediately
logger.error(`ERROR: Manifest file not found: ${customManifestPath}`);
return false;
}

logger.log(`No manifest.json found in ${extensionPath}`);
const shouldInit = await confirm({
message: "Would you like to create a manifest.json file?",
Expand Down Expand Up @@ -135,6 +146,15 @@ export async function packExtension({
mcpbIgnorePatterns,
);

// When using a custom manifest path, inject the manifest into the bundle
if (customManifestPath) {
const manifestStat = statSync(manifestPath);
files["manifest.json"] = {
data: readFileSync(manifestPath),
mode: manifestStat.mode,
};
}

// Print package header
logger.log(`\n📦 ${manifest.name}@${manifest.version}`);

Expand Down
78 changes: 78 additions & 0 deletions test/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,84 @@ describe("DXT CLI", () => {
expect(originalFile2).toEqual(unpackedFile2);
});

it("should pack with --manifest pointing to a separate directory", () => {
const projectDir = join(__dirname, "temp-manifest-project");
const manifestDir = join(__dirname, "temp-manifest-separate");
const manifestPackedPath = join(__dirname, "test-manifest-flag.mcpb");

try {
// Create project directory with source files (no manifest)
fs.mkdirSync(join(projectDir, "server"), { recursive: true });
fs.writeFileSync(
join(projectDir, "server", "index.js"),
"console.log('hello');",
);

// Create separate manifest directory
fs.mkdirSync(manifestDir, { recursive: true });
fs.writeFileSync(
join(manifestDir, "manifest.json"),
JSON.stringify({
manifest_version: DEFAULT_MANIFEST_VERSION,
name: "Test Separate Manifest",
version: "1.0.0",
description: "Test with separate manifest",
author: { name: "MCPB" },
server: {
type: "node",
entry_point: "server/index.js",
mcp_config: { command: "node" },
},
}),
);

const result = execSync(
`node ${cliPath} pack ${projectDir} ${manifestPackedPath} --manifest ${join(manifestDir, "manifest.json")}`,
{ encoding: "utf-8" },
);

expect(fs.existsSync(manifestPackedPath)).toBe(true);
expect(result).toContain("Validating manifest");
} finally {
fs.rmSync(projectDir, { recursive: true, force: true });
fs.rmSync(manifestDir, { recursive: true, force: true });
if (fs.existsSync(manifestPackedPath)) {
fs.unlinkSync(manifestPackedPath);
}
}
});

it("should fail with --manifest pointing to nonexistent file", () => {
const projectDir = join(__dirname, "temp-manifest-missing-project");

try {
fs.mkdirSync(projectDir, { recursive: true });
fs.writeFileSync(join(projectDir, "index.js"), "console.log('hello');");

expect(() => {
execSync(
`node ${cliPath} pack ${projectDir} /tmp/out.mcpb --manifest /nonexistent/manifest.json`,
{ encoding: "utf-8", stdio: "pipe" },
);
}).toThrow();

try {
execSync(
`node ${cliPath} pack ${projectDir} /tmp/out.mcpb --manifest /nonexistent/manifest.json`,
{ encoding: "utf-8", stdio: "pipe" },
);
} catch (error: unknown) {
const execError = error as { stdout?: Buffer; stderr?: Buffer };
const output =
(execError.stdout?.toString() || "") +
(execError.stderr?.toString() || "");
expect(output).toContain("Manifest file not found");
}
} finally {
fs.rmSync(projectDir, { recursive: true, force: true });
}
});

it("should preserve executable file permissions after packing and unpacking", () => {
// Skip this test on Windows since it doesn't support Unix permissions
if (process.platform === "win32") {
Expand Down
Loading