#!/usr/bin/env node import { cpSync, existsSync, mkdtempSync, mkdirSync, renameSync, rmSync } from "node:fs"; import { homedir } from "node:os"; import { basename, dirname, join, resolve } from "node:path"; import { spawn } from "node:child_process"; import { fileURLToPath } from "node:url"; import { startEditorServer } from "../lib/editor-server.js"; const SKILL_NAME = "open-kimi-ppt"; const MIN_NODE_MAJOR = 18; const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const sourceDirectory = join(packageRoot, "skills", SKILL_NAME); function assertNodeVersion() { const major = Number.parseInt(process.versions.node.split(".")[0], 10); if (!Number.isInteger(major) || major < MIN_NODE_MAJOR) { throw new Error( `Node.js ${MIN_NODE_MAJOR}+ is required; found ${process.version}. Install from https://nodejs.org`, ); } } function printHelp(command) { if (command === "serve") { console.log(`Start the local PPTD editor and exporter. Usage: open-kimi-ppt-skills serve [options] Options: --port HTTP port (default: 55173) --open Open the editor in the default browser -h, --help Show this help `); return; } console.log(`Install ${SKILL_NAME} for your AI coding agent or start its local editor. Usage: open-kimi-ppt-skills [install] [options] open-kimi-ppt-skills serve [options] Install options: --target Skills directory (default: ~/.agents/skills) Re-running install replaces an existing open-kimi-ppt installation. Run "open-kimi-ppt-skills serve --help" for server options. `); } function parseArguments(arguments_) { const args = [...arguments_]; const command = args[0] === "install" || args[0] === "serve" ? args.shift() : "install"; const options = command === "serve" ? { command, open: false, port: 55173 } : { command, target: undefined }; while (args.length > 0) { const argument = args.shift(); // Accepted for backward compatibility; install always overwrites. if (command === "install" && argument === "--force") { continue; } if (command === "install" && argument === "--target") { const target = args.shift(); if (!target || target.startsWith("-")) { throw new Error("--target requires a directory"); } options.target = resolve(target); continue; } if (command === "serve" && argument === "--port") { const port = Number(args.shift()); if (!Number.isInteger(port) || port < 1 || port > 65_535) { throw new Error("--port must be an integer between 1 and 65535"); } options.port = port; continue; } if (command === "serve" && argument === "--open") { options.open = true; continue; } if (argument === "--help" || argument === "-h") { options.help = true; continue; } throw new Error(`unknown argument: ${argument}`); } return options; } function openBrowser(url) { const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open"; const args = process.platform === "win32" ? ["/c", "start", "", url] : [url]; const child = spawn(command, args, { detached: true, stdio: "ignore" }); child.on("error", (error) => console.warn(`Could not open the browser: ${error.message}`)); child.unref(); } function defaultSkillsDirectory() { return join(homedir(), ".agents", "skills"); } function installSkill({ target }) { if (!existsSync(join(sourceDirectory, "SKILL.md"))) { throw new Error(`packaged skill is incomplete: ${sourceDirectory}`); } const skillsDirectory = target ?? defaultSkillsDirectory(); const destination = join(skillsDirectory, SKILL_NAME); const replaced = existsSync(destination); mkdirSync(skillsDirectory, { recursive: true }); const stagingRoot = mkdtempSync(join(skillsDirectory, `.${SKILL_NAME}-`)); const stagedSkill = join(stagingRoot, SKILL_NAME); try { cpSync(sourceDirectory, stagedSkill, { recursive: true, filter: (source) => ![".DS_Store", "_user_meta.json"].includes(basename(source)), }); rmSync(destination, { recursive: true, force: true }); renameSync(stagedSkill, destination); } finally { rmSync(stagingRoot, { recursive: true, force: true }); } console.log( replaced ? `Updated ${SKILL_NAME} at ${destination}` : `Installed ${SKILL_NAME} to ${destination}`, ); } async function main() { assertNodeVersion(); const options = parseArguments(process.argv.slice(2)); if (options.help) { printHelp(options.command); return; } if (options.command === "install") { installSkill(options); return; } const { server, url } = await startEditorServer({ port: options.port }); console.log(`Open Kimi PPT editor is running at ${url}`); console.log("Press Ctrl+C to stop the server."); if (options.open) openBrowser(url); const shutdown = () => server.close(() => process.exit(0)); process.once("SIGINT", shutdown); process.once("SIGTERM", shutdown); } main().catch((error) => { console.error(`Error: ${error.message}`); process.exitCode = 1; });