#!/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 packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const sourceDirectory = join(packageRoot, "skills", SKILL_NAME); 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) --force Replace an existing 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, force: false, target: undefined }; while (args.length > 0) { const argument = args.shift(); if (command === "install" && argument === "--force") { options.force = true; 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({ force, 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); if (existsSync(destination) && !force) { throw new Error( `${destination} already exists; rerun with --force to replace it`, ); } 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)), }); if (force) { rmSync(destination, { recursive: true, force: true }); } renameSync(stagedSkill, destination); } finally { rmSync(stagingRoot, { recursive: true, force: true }); } console.log(`Installed ${SKILL_NAME} to ${destination}`); } async function main() { 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; });