diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 817b659..5ba121c 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -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 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 diff --git a/src/cli/pack.ts b/src/cli/pack.ts index e2411bf..1e35d47 100644 --- a/src/cli/pack.ts +++ b/src/cli/pack.ts @@ -22,6 +22,7 @@ interface PackOptions { extensionPath: string; outputPath?: string; silent?: boolean; + manifestPath?: string; } function formatFileSize(bytes: number): string { @@ -50,6 +51,7 @@ export async function packExtension({ extensionPath, outputPath, silent, + manifestPath: customManifestPath, }: PackOptions): Promise { const resolvedPath = resolve(extensionPath); const logger = getLogger({ silent }); @@ -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?", @@ -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}`); diff --git a/test/cli.test.ts b/test/cli.test.ts index e7757ac..2586d7c 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -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") {