/SKILL.md
+ join3(baseDir, "../SKILL.md"),
+ // Legacy Cursor rule: rules/slidesfly/scripts/slidesfly.mjs + rules/slidesfly.mdc
+ join3(baseDir, "../../slidesfly.mdc"),
+ // npm package: dist/skill/load.js + assets/SKILL.md
+ getBundledSkillPath(baseDir)
+ ];
+}
+async function fetchSkill(url) {
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(`Failed to fetch skill from ${url}: HTTP ${response.status}`);
+ }
+ return response.text();
+}
+async function loadCanonicalSkill(fromUrl) {
+ if (fromUrl) {
+ return fetchSkill(fromUrl);
+ }
+ for (const path of getLocalSkillPaths()) {
+ try {
+ return await readFile2(path, "utf8");
+ } catch {
+ }
+ }
+ return fetchSkill(DEFAULT_SKILL_URL);
+}
+
+// src/skill/paths.ts
+import { homedir as homedir3 } from "node:os";
+import { join as join4 } from "node:path";
+function resolveHome(home) {
+ return home ?? homedir3();
+}
+function resolveCwd(cwd) {
+ return cwd ?? process.cwd();
+}
+function getSkillInstallTargets(options = {}) {
+ const scope = options.scope ?? "user";
+ const home = resolveHome(options.home);
+ const cwd = resolveCwd(options.cwd);
+ const targets = [];
+ if (scope === "user") {
+ targets.push({
+ runtime: "claude-code",
+ path: join4(home, ".claude", "skills", "slidesfly", "SKILL.md"),
+ runnerPath: join4(home, ".claude", "skills", "slidesfly", "scripts", "slidesfly.mjs")
+ });
+ targets.push({
+ runtime: "cursor",
+ path: join4(home, ".cursor", "rules", "slidesfly.mdc"),
+ runnerPath: join4(home, ".cursor", "rules", "slidesfly", "scripts", "slidesfly.mjs")
+ });
+ targets.push({
+ runtime: "codex",
+ path: join4(home, ".codex", "skills", "slidesfly", "SKILL.md"),
+ runnerPath: join4(home, ".codex", "skills", "slidesfly", "scripts", "slidesfly.mjs")
+ });
+ return targets;
+ }
+ targets.push({
+ runtime: "cursor",
+ path: join4(cwd, ".cursor", "rules", "slidesfly.mdc"),
+ runnerPath: join4(cwd, ".cursor", "rules", "slidesfly", "scripts", "slidesfly.mjs")
+ });
+ targets.push({
+ runtime: "codex",
+ path: join4(cwd, ".agents", "skills", "slidesfly", "SKILL.md"),
+ runnerPath: join4(cwd, ".agents", "skills", "slidesfly", "scripts", "slidesfly.mjs")
+ });
+ return targets;
+}
+function filterTargetsByRuntime(targets, runtime) {
+ if (runtime === "all") {
+ return targets;
+ }
+ return targets.filter((target) => target.runtime === runtime);
+}
+
+// src/skill/runner.ts
+import { readFile as readFile3 } from "node:fs/promises";
+import { dirname as dirname3, join as join5, resolve } from "node:path";
+import { fileURLToPath as fileURLToPath2 } from "node:url";
+var moduleDir2 = dirname3(fileURLToPath2(import.meta.url));
+function getBundledRunnerPath() {
+ return join5(moduleDir2, "../../assets/scripts/slidesfly.mjs");
+}
+function looksLikeSlidesflyBundle(content) {
+ return content.startsWith("#!/usr/bin/env node") && content.includes('.name("slidesfly")') && content.includes("Publish HTML decks to Slidesfly");
+}
+async function loadBundledRunner() {
+ try {
+ return await readFile3(getBundledRunnerPath(), "utf8");
+ } catch {
+ const executable = process.argv[1];
+ if (executable) {
+ try {
+ const content = await readFile3(resolve(executable), "utf8");
+ if (looksLikeSlidesflyBundle(content)) {
+ return content;
+ }
+ } catch {
+ }
+ }
+ }
+ throw new Error(
+ "Bundled Slidesfly runner is missing. Reinstall the official CLI or use --skill-only."
+ );
+}
+
+// src/skill/transform.ts
+var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
+function parseSkillMarkdown(content) {
+ const match = content.match(FRONTMATTER_RE);
+ if (!match) {
+ return { frontmatter: {}, body: content };
+ }
+ const frontmatterBlock = match[1] ?? "";
+ const body = match[2] ?? content;
+ const frontmatter = {};
+ for (const line of frontmatterBlock.split("\n")) {
+ const trimmed = line.trim();
+ if (!trimmed || trimmed.startsWith("#")) {
+ continue;
+ }
+ const separator = trimmed.indexOf(":");
+ if (separator === -1) {
+ continue;
+ }
+ const key = trimmed.slice(0, separator).trim();
+ let value = trimmed.slice(separator + 1).trim();
+ if (value.startsWith("|")) {
+ continue;
+ }
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
+ value = value.slice(1, -1);
+ }
+ frontmatter[key] = value;
+ }
+ return {
+ frontmatter,
+ body
+ };
+}
+function extractBlockScalar(content, key) {
+ const lines = content.split("\n");
+ const keyPrefix = `${key}:`;
+ const startIndex = lines.findIndex((line) => line.trimStart().startsWith(keyPrefix));
+ if (startIndex === -1) {
+ return void 0;
+ }
+ const firstLine = lines[startIndex];
+ if (!firstLine) {
+ return void 0;
+ }
+ const inlineValue = firstLine.slice(firstLine.indexOf(":") + 1).trim();
+ if (inlineValue && !inlineValue.startsWith("|") && !inlineValue.startsWith(">")) {
+ return inlineValue.replace(/^['"]|['"]$/g, "");
+ }
+ const blockLines = [];
+ for (let i = startIndex + 1; i < lines.length; i += 1) {
+ const line = lines[i];
+ if (line === void 0) {
+ break;
+ }
+ if (line.length > 0 && !/^\s/.test(line)) {
+ break;
+ }
+ blockLines.push(line.replace(/^\s{2}/, ""));
+ }
+ return blockLines.join("\n").trimEnd();
+}
+function getSkillDescription(content) {
+ const block = extractBlockScalar(content, "description");
+ if (block) {
+ return block;
+ }
+ return parseSkillMarkdown(content).frontmatter.description ?? "";
+}
+function yamlQuote(value) {
+ if (value.includes("\n")) {
+ const indented = value.split("\n").map((line) => ` ${line}`).join("\n");
+ return `|
+${indented}`;
+ }
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
+}
+function transformSkillForRuntime(content, runtime) {
+ const description = getSkillDescription(content);
+ const { body } = parseSkillMarkdown(content);
+ if (runtime === "claude-code") {
+ const version = parseSkillMarkdown(content).frontmatter.version ?? "0.1.0";
+ const license = parseSkillMarkdown(content).frontmatter.license ?? "MIT";
+ return [
+ "---",
+ "name: slidesfly",
+ `description: ${yamlQuote(description)}`,
+ `version: ${version}`,
+ `license: ${license}`,
+ "---",
+ body.trimStart()
+ ].join("\n");
+ }
+ if (runtime === "codex") {
+ return [
+ "---",
+ "name: slidesfly",
+ `description: ${yamlQuote(description)}`,
+ "---",
+ body.trimStart()
+ ].join("\n");
+ }
+ return [
+ "---",
+ `description: ${yamlQuote(description)}`,
+ "alwaysApply: false",
+ "globs: ['**/*.html', '**/*.htm']",
+ "---",
+ body.trimStart()
+ ].join("\n");
+}
+
+// src/skill/install.ts
+async function resolveInstallTargets(options) {
+ const scope = options.scope ?? "user";
+ let targets = getSkillInstallTargets({
+ scope,
+ home: options.home,
+ cwd: options.cwd
+ });
+ const target = options.target ?? "auto";
+ if (target === "auto") {
+ const detected = await detectRuntimes({
+ home: options.home,
+ cwd: options.cwd,
+ scope
+ });
+ if (detected.length > 0) {
+ targets = targets.filter((entry) => detected.includes(entry.runtime));
+ }
+ } else if (target !== "all") {
+ targets = filterTargetsByRuntime(targets, target);
+ }
+ return targets;
+}
+async function assertOverwriteAllowed(path, content, options) {
+ try {
+ const existing = await readFile4(path, "utf8");
+ if (existing !== content && !options.force) {
+ exitError(
+ {
+ code: "SKILL_EXISTS",
+ message: `Skill file or bundled asset already exists and was modified: ${path}`,
+ hint: "Re-run with --force to overwrite"
+ },
+ { json: options.json }
+ );
+ }
+ } catch (err) {
+ if (!(err instanceof Error && "code" in err && err.code === "ENOENT")) {
+ throw err;
+ }
+ }
+}
+async function writeSkillTarget(target, canonicalContent, runnerContent, options) {
+ const transformed = transformSkillForRuntime(canonicalContent, target.runtime);
+ const version = parseSkillMarkdown(canonicalContent).frontmatter.version ?? "0.1.0";
+ const content = appendVersionStamp(transformed, version);
+ await assertOverwriteAllowed(target.path, content, options);
+ if (runnerContent !== void 0) {
+ await assertOverwriteAllowed(target.runnerPath, runnerContent, options);
+ }
+ await mkdir2(dirname4(target.path), { recursive: true });
+ await writeFile2(target.path, content, "utf8");
+ if (runnerContent === void 0) {
+ return { skill: target.path };
+ }
+ await mkdir2(dirname4(target.runnerPath), { recursive: true });
+ await writeFile2(target.runnerPath, runnerContent, { encoding: "utf8", mode: 493 });
+ await chmod2(target.runnerPath, 493);
+ return { skill: target.path, asset: target.runnerPath };
+}
+async function installSkill(options = {}) {
+ const canonicalContent = await loadCanonicalSkill(options.fromUrl);
+ const runnerContent = options.skillOnly ? void 0 : await loadBundledRunner();
+ const targets = await resolveInstallTargets(options);
+ if (targets.length === 0) {
+ exitError(
+ {
+ code: "NO_TARGETS",
+ message: "No install targets matched the requested runtime and scope",
+ hint: "Try --target all or create a Claude Code, Cursor, or Codex config directory first"
+ },
+ { json: options.json }
+ );
+ }
+ const installed = [];
+ const assets = [];
+ for (const target of targets) {
+ const result = await writeSkillTarget(target, canonicalContent, runnerContent, options);
+ installed.push(result.skill);
+ if (result.asset) {
+ assets.push(result.asset);
+ }
+ }
+ return { installed, assets };
+}
+async function runInstallSkill(options = {}) {
+ const result = await installSkill(options);
+ exitOk(result, { json: options.json });
+}
+
+// src/commands/install.ts
+async function runInstall(options) {
+ return runInstallSkill(options);
+}
+
+// src/commands/list.ts
+async function runList(options) {
+ const config = await loadConfig();
+ if (options.json || !process.stdout.isTTY) {
+ exitOk({ decks: config.anon_decks }, { json: options.json });
+ }
+ if (config.anon_decks.length === 0) {
+ exitOk({ decks: [] }, { json: false });
+ }
+ const idWidth = Math.max(2, ...config.anon_decks.map((d) => d.deck_id.length));
+ const titleWidth = Math.max(5, ...config.anon_decks.map((d) => d.title.length));
+ const header = `${"ID".padEnd(idWidth)} ${"TITLE".padEnd(titleWidth)} URL`;
+ const rows = config.anon_decks.map(
+ (deck) => `${deck.deck_id.padEnd(idWidth)} ${deck.title.padEnd(titleWidth)} ${deck.url}`
+ );
+ process.stdout.write(`${[header, ...rows].join("\n")}
+`);
+ process.exit(0);
+}
+
+// src/auth/login-device.ts
+import { randomBytes as randomBytes2 } from "node:crypto";
+import { createInterface } from "node:readline";
+
+// src/auth/pkce.ts
+import { createHash as createHash2, randomBytes } from "node:crypto";
+function generateCodeVerifier() {
+ return randomBytes(32).toString("base64url");
+}
+function codeChallengeS256(verifier) {
+ return createHash2("sha256").update(verifier).digest("base64url");
+}
+
+// src/auth/login-device.ts
+function getDefaultBaseUrl2() {
+ return process.env.SLIDESFLY_API_URL ?? "https://slidesfly.com";
+}
+function printVerificationUrl(baseUrl, state) {
+ const normalized = baseUrl.replace(/\/$/, "");
+ const url = `${normalized}/cli/code?state=${encodeURIComponent(state)}`;
+ process.stdout.write(`Open this URL in a browser to authorize:
+${url}
+`);
+}
+async function promptUserCode(options = {}) {
+ if (options.promptCode) {
+ const raw = await options.promptCode();
+ return normalizeUserCode(raw);
+ }
+ const rl = createInterface({
+ input: process.stdin,
+ output: process.stdout
+ });
+ try {
+ const raw = await new Promise((resolve2) => {
+ rl.question("Enter code: ", resolve2);
+ });
+ return normalizeUserCode(raw);
+ } finally {
+ rl.close();
+ }
+}
+function normalizeUserCode(raw) {
+ return raw.trim().replace(/\s+/g, "").toUpperCase();
+}
+async function runDeviceLogin(options = {}) {
+ const baseUrl = (options.baseUrl ?? getDefaultBaseUrl2()).replace(/\/$/, "");
+ const verifier = generateCodeVerifier();
+ const challenge = codeChallengeS256(verifier);
+ const state = randomBytes2(16).toString("base64url");
+ const register = options.registerDeviceAuth ?? (async (params) => {
+ const client = createApiClient({ baseUrl });
+ return client.registerDeviceAuth(params.state, params.challenge, params.challengeMethod);
+ });
+ await register({ state, challenge, challengeMethod: "S256" });
+ printVerificationUrl(baseUrl, state);
+ const userCode = await promptUserCode({ promptCode: options.promptCode });
+ const exchange = options.exchangeDeviceCode ?? (async (code, authState, codeVerifier) => {
+ const client = createApiClient({ baseUrl });
+ return client.exchangeDeviceCode(code, authState, codeVerifier);
+ });
+ return exchange(userCode, state, verifier);
+}
+
+// src/auth/login-pkce.ts
+import { randomBytes as randomBytes3 } from "node:crypto";
+
+// src/auth/loopback.ts
+import { createServer } from "node:http";
+import { URL } from "node:url";
+var DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
+async function beginLoopback(options) {
+ const { expectedState, timeoutMs = DEFAULT_TIMEOUT_MS } = options;
+ return new Promise((resolve2, reject) => {
+ const server = createServer();
+ server.on("error", reject);
+ server.listen(0, "127.0.0.1", () => {
+ const address = server.address();
+ if (!address || typeof address === "string") {
+ server.close();
+ reject(new Error("Failed to bind loopback server"));
+ return;
+ }
+ const redirectUri = `http://127.0.0.1:${address.port}/callback`;
+ resolve2({
+ redirectUri,
+ waitForCallback: () => waitForCallbackOnServer(server, redirectUri, expectedState, timeoutMs),
+ close: () => new Promise((closeResolve, closeReject) => {
+ server.close((err) => err ? closeReject(err) : closeResolve());
+ })
+ });
+ });
+ });
+}
+function waitForCallbackOnServer(server, redirectUri, expectedState, timeoutMs) {
+ return new Promise((resolve2, reject) => {
+ let settled = false;
+ const timeout = setTimeout(() => {
+ if (settled) return;
+ settled = true;
+ server.close();
+ reject(new Error("Callback timeout"));
+ }, timeoutMs);
+ const fail = (err) => {
+ if (settled) return;
+ settled = true;
+ clearTimeout(timeout);
+ server.close();
+ reject(err);
+ };
+ const succeed = (result) => {
+ if (settled) return;
+ settled = true;
+ clearTimeout(timeout);
+ server.close(() => resolve2(result));
+ };
+ server.on("request", (req, res) => {
+ if (settled) {
+ res.writeHead(410);
+ res.end("Gone");
+ return;
+ }
+ if (!req.url) {
+ res.writeHead(400);
+ res.end("Bad request");
+ return;
+ }
+ const url = new URL(req.url, redirectUri);
+ if (url.pathname !== "/callback") {
+ res.writeHead(404);
+ res.end("Not found");
+ return;
+ }
+ const code = url.searchParams.get("code");
+ const state = url.searchParams.get("state");
+ if (!code || !state) {
+ res.writeHead(400);
+ res.end("Missing code or state");
+ return;
+ }
+ if (state !== expectedState) {
+ res.writeHead(400);
+ res.end("State mismatch");
+ req.socket?.destroy();
+ fail(new Error("State mismatch"));
+ return;
+ }
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
+ res.end(
+ "Login successful. You can close this window.
"
+ );
+ req.socket?.destroy();
+ succeed({ code, state, redirectUri });
+ });
+ });
+}
+
+// src/auth/login-pkce.ts
+function getDefaultBaseUrl3() {
+ return process.env.SLIDESFLY_API_URL ?? "https://slidesfly.com";
+}
+async function defaultOpenBrowser(url) {
+ const { spawn } = await import("node:child_process");
+ const platform = process.platform;
+ let command;
+ let args;
+ if (platform === "darwin") {
+ command = "open";
+ args = [url];
+ } else if (platform === "win32") {
+ command = "cmd";
+ args = ["/c", "start", "", url];
+ } else {
+ command = "xdg-open";
+ args = [url];
+ }
+ await new Promise((resolve2, reject) => {
+ const child = spawn(command, args, { stdio: "ignore", detached: true });
+ child.on("error", reject);
+ child.on("close", (code) => {
+ if (code === 0) {
+ resolve2();
+ return;
+ }
+ reject(new Error(`Failed to open browser (exit ${code ?? "unknown"})`));
+ });
+ });
+}
+async function runPkceLogin(options = {}) {
+ const baseUrl = (options.baseUrl ?? getDefaultBaseUrl3()).replace(/\/$/, "");
+ const openBrowser = options.openBrowser ?? defaultOpenBrowser;
+ const beginLoopbackFn = options.beginLoopback ?? beginLoopback;
+ const verifier = generateCodeVerifier();
+ const challenge = codeChallengeS256(verifier);
+ const state = randomBytes3(16).toString("base64url");
+ const session = await beginLoopbackFn({ expectedState: state });
+ try {
+ const params = new URLSearchParams({
+ state,
+ code_challenge: challenge,
+ code_challenge_method: "S256",
+ redirect_uri: session.redirectUri
+ });
+ const authUrl = `${baseUrl}/cli/auth?${params.toString()}`;
+ await openBrowser(authUrl);
+ const { code } = await session.waitForCallback();
+ const exchange = options.exchangeCliAuth ?? (async (authCode, codeVerifier) => {
+ const client = createApiClient({ baseUrl });
+ return client.exchangeCliAuth(authCode, codeVerifier);
+ });
+ return await exchange(code, verifier);
+ } finally {
+ await session.close().catch(() => void 0);
+ }
+}
+
+// src/commands/login.ts
+async function runLogin(options, deps = {}) {
+ if (options.apiKey) {
+ await setApiKey(options.apiKey);
+ exitOk({ logged_in: true }, { json: options.json });
+ }
+ const loginFn = options.code ? deps.runDeviceLogin ?? runDeviceLogin : deps.runPkceLogin ?? runPkceLogin;
+ const claimAnonDecks = deps.claimAnonDecks ?? claimAnonDecksFromConfig;
+ try {
+ const { apiKey, prefix } = await loginFn();
+ await setApiKey(apiKey);
+ let claimResult = {
+ claimed: [],
+ failed: []
+ };
+ if (!options.noClaim) {
+ claimResult = await claimAnonDecks(apiKey);
+ }
+ exitOk(
+ {
+ logged_in: true,
+ prefix,
+ claimed: claimResult.claimed,
+ failed: claimResult.failed
+ },
+ { json: options.json }
+ );
+ } catch (err) {
+ if (err instanceof ApiError) {
+ exitError(
+ {
+ code: err.code,
+ message: err.message,
+ details: err.details,
+ hint: err.hint
+ },
+ { json: options.json }
+ );
+ }
+ if (err instanceof Error && /timeout/i.test(err.message)) {
+ exitError(
+ {
+ code: "LOGIN_TIMEOUT",
+ message: "Browser login timed out waiting for authorization",
+ hint: "Try again, or use `slidesfly login --api-key YOUR_KEY`"
+ },
+ { json: options.json }
+ );
+ }
+ if (err instanceof Error && /state mismatch/i.test(err.message)) {
+ exitError(
+ {
+ code: "LOGIN_FAILED",
+ message: "Browser login failed due to invalid callback state",
+ hint: "Try `slidesfly login` again"
+ },
+ { json: options.json }
+ );
+ }
+ throw err;
+ }
+}
+
+// src/commands/logout.ts
+async function runLogout(options) {
+ await clearApiKey();
+ exitOk({ logged_out: true }, { json: options.json });
+}
+
+// src/commands/open.ts
+import { execFile } from "node:child_process";
+import { promisify } from "node:util";
+var execFileAsync = promisify(execFile);
+async function defaultOpenUrl(url) {
+ switch (process.platform) {
+ case "darwin":
+ await execFileAsync("open", [url]);
+ return;
+ case "win32":
+ await execFileAsync("cmd", ["/c", "start", "", url]);
+ return;
+ default:
+ await execFileAsync("xdg-open", [url]);
+ }
+}
+async function runOpen(deckId, options, deps = {}) {
+ const config = await loadConfig();
+ const deck = findAnonDeck(config, deckId);
+ if (!deck) {
+ exitError(
+ {
+ code: "NOT_FOUND",
+ message: `Deck \`${deckId}\` not found in local config`
+ },
+ { json: options.json }
+ );
+ }
+ const openUrl = deps.openUrl ?? defaultOpenUrl;
+ try {
+ await openUrl(deck.url);
+ } catch (err) {
+ exitError(
+ {
+ code: "OPEN_FAILED",
+ message: err instanceof Error ? err.message : "Failed to open URL in browser"
+ },
+ { json: options.json }
+ );
+ }
+ exitOk({ url: deck.url, deck_id: deckId }, { json: options.json });
+}
+
+// src/commands/password.ts
+var CLEAR_VALUES3 = /* @__PURE__ */ new Set(["off", "none", "clear", "remove"]);
+function resolvePassword(value) {
+ if (CLEAR_VALUES3.has(value.toLowerCase())) {
+ return null;
+ }
+ return value;
+}
+async function runPassword(deckId, value, options, deps = {}) {
+ const password = resolvePassword(value);
+ const config = await loadConfig();
+ if (!config.api_key) {
+ exitError(
+ {
+ code: "AUTH_REQUIRED",
+ message: "API key required to set a deck password",
+ hint: "Run `slidesfly login` or `slidesfly login --api-key YOUR_KEY`"
+ },
+ { json: options.json }
+ );
+ }
+ const api = deps.api ?? createApiClient();
+ try {
+ const result = await api.setPasswordOwned(deckId, password, config.api_key);
+ exitOk(result, { json: options.json });
+ } catch (err) {
+ if (err instanceof ApiError) {
+ exitError(
+ {
+ code: err.code,
+ message: err.message,
+ details: err.details,
+ hint: err.hint
+ },
+ { json: options.json }
+ );
+ }
+ throw err;
+ }
+}
+
+// src/commands/publish.ts
+import { access as access2, readFile as readFile5 } from "node:fs/promises";
+import { basename, extname } from "node:path";
+var HTML_EXTENSIONS = /* @__PURE__ */ new Set([".html", ".htm"]);
+async function assertPublishFile(filePath, owned, json) {
+ const ext = extname(filePath).toLowerCase();
+ const isHtml = HTML_EXTENSIONS.has(ext);
+ const isZip = ext === ".zip";
+ if (!isHtml && !isZip) {
+ exitError(
+ {
+ code: "INVALID_FILE",
+ message: "Publish requires an .html, .htm, or .zip file"
+ },
+ { json }
+ );
+ }
+ if (isZip && !owned) {
+ exitError(
+ {
+ code: "INVALID_FILE",
+ message: "Multi-file (.zip) publish requires login (Pro plan)",
+ hint: "Run `slidesfly login` first, then publish the zip."
+ },
+ { json }
+ );
+ }
+ try {
+ await access2(filePath);
+ } catch {
+ exitError(
+ {
+ code: "FILE_NOT_FOUND",
+ message: `File not found: ${filePath}`
+ },
+ { json }
+ );
+ }
+}
+async function runPublish(filePath, options, deps = {}) {
+ const config = await loadConfig();
+ await assertPublishFile(filePath, Boolean(config.api_key), options.json);
+ const fileBuffer = await readFile5(filePath);
+ const filename = basename(filePath);
+ const api = deps.api ?? createApiClient();
+ try {
+ if (options.deckId) {
+ const anonymousDeck = findAnonDeck(config, options.deckId);
+ if (anonymousDeck) {
+ if (options.title) {
+ exitError(
+ {
+ code: "INVALID_ARGUMENT",
+ message: "`--title` is not supported when updating a local anonymous deck",
+ hint: "Claim the deck with `slidesfly login`, then retry the owned update"
+ },
+ { json: options.json }
+ );
+ }
+ try {
+ const result2 = await api.updateAnon(
+ anonymousDeck.deck_id,
+ anonymousDeck.claim_token,
+ fileBuffer,
+ filename
+ );
+ exitOk(result2, { json: options.json });
+ } catch (err) {
+ if (err instanceof ApiError) {
+ const claimedElsewhere = err.code === "DECK_NOT_OWNED" && Boolean(config.api_key);
+ if (!claimedElsewhere) {
+ throw err;
+ }
+ } else {
+ throw err;
+ }
+ }
+ }
+ if (!config.api_key) {
+ exitError(
+ {
+ code: "AUTH_REQUIRED",
+ message: "No local anonymous claim or API key found for this deck",
+ hint: "Publish from the original machine, or run `slidesfly login` to update an owned deck"
+ },
+ { json: options.json }
+ );
+ }
+ const result = await api.updateOwned(
+ options.deckId,
+ fileBuffer,
+ filename,
+ config.api_key,
+ options.title
+ );
+ await removeAnonDeck(options.deckId);
+ exitOk(result, { json: options.json });
+ } else if (config.api_key) {
+ const result = await api.publishOwned(
+ {
+ fileBuffer,
+ filename,
+ title: options.title,
+ visibility: options.visibility
+ },
+ config.api_key
+ );
+ const { warnings, ...data } = result;
+ exitOk(data, { json: options.json, warnings });
+ } else {
+ const result = await api.publishAnonymous({
+ filePath,
+ fileBuffer,
+ filename,
+ title: options.title
+ });
+ await addAnonDeck({
+ deck_id: result.deck_id,
+ claim_token: result.claim_token,
+ title: result.title,
+ url: result.url,
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
+ });
+ const { warnings, claim_token: _claimToken, ...data } = result;
+ exitOk(data, { json: options.json, warnings });
+ }
+ } catch (err) {
+ if (err instanceof ApiError) {
+ exitError(
+ {
+ code: err.code,
+ message: err.message,
+ details: err.details,
+ hint: err.hint
+ },
+ { json: options.json }
+ );
+ }
+ throw err;
+ }
+}
+
+// src/commands/restore.ts
+async function runRestore(deckId, options, deps = {}) {
+ const config = await loadConfig();
+ if (!config.api_key) {
+ exitError(
+ {
+ code: "AUTH_REQUIRED",
+ message: "API key required to restore a deck version",
+ hint: "Run `slidesfly login` or `slidesfly login --api-key YOUR_KEY`"
+ },
+ { json: options.json }
+ );
+ }
+ if (!Number.isInteger(options.version) || options.version < 1) {
+ exitError(
+ {
+ code: "INVALID_FILE",
+ message: "Version must be a positive integer",
+ hint: "Run `slidesfly versions ` to list versions"
+ },
+ { json: options.json }
+ );
+ }
+ const api = deps.api ?? createApiClient();
+ try {
+ const result = await api.restoreOwned(deckId, options.version, config.api_key);
+ exitOk(result, { json: options.json });
+ } catch (err) {
+ if (err instanceof ApiError) {
+ exitError(
+ { code: err.code, message: err.message, details: err.details, hint: err.hint },
+ { json: options.json }
+ );
+ }
+ throw err;
+ }
+}
+
+// src/skill/installed.ts
+import { access as access3, readFile as readFile6 } from "node:fs/promises";
+async function getSkillInstallStatus(options) {
+ const targets = getSkillInstallTargets(options);
+ const installed = [];
+ for (const target of targets) {
+ try {
+ await access3(target.path);
+ const content = await readFile6(target.path, "utf8");
+ const { frontmatter } = parseSkillMarkdown(content);
+ installed.push({
+ runtime: target.runtime,
+ path: target.path,
+ version: frontmatter.version
+ });
+ } catch {
+ }
+ }
+ const canonical = parseSkillMarkdown(await loadCanonicalSkill());
+ return {
+ installed,
+ latest_version: canonical.frontmatter.version ?? "0.0.0"
+ };
+}
+
+// src/commands/status.ts
+function formatApiKeyPrefix(apiKey) {
+ const match = apiKey.match(/^(sk_(?:live|test)_)/);
+ const prefix = match?.[1] ?? apiKey.slice(0, 8);
+ return `${prefix}...${apiKey.slice(-4)}`;
+}
+async function runStatus(options) {
+ const [config, skillStatus] = await Promise.all([loadConfig(), getSkillInstallStatus()]);
+ const data = {
+ config_path: getConfigPath(),
+ has_api_key: Boolean(config.api_key),
+ anon_count: config.anon_decks.length,
+ skill_latest_version: skillStatus.latest_version,
+ skill_installed: skillStatus.installed
+ };
+ if (config.api_key) {
+ data.api_key_prefix = formatApiKeyPrefix(config.api_key);
+ }
+ if (options.json || !process.stdout.isTTY) {
+ exitOk(data, { json: options.json });
+ }
+ const skillLine = data.skill_installed.length === 0 ? `Skill: not installed (latest ${data.skill_latest_version})` : `Skill: ${data.skill_installed.map((s) => `${s.runtime}@${s.version ?? "?"}`).join(", ")} (latest ${data.skill_latest_version})`;
+ const lines = [
+ `Config: ${data.config_path}`,
+ data.has_api_key ? `API key: ${data.api_key_prefix}` : "API key: not set",
+ `${data.anon_count} anonymous deck${data.anon_count === 1 ? "" : "s"}`,
+ skillLine
+ ];
+ process.stdout.write(`${lines.join("\n")}
+`);
+ process.exit(0);
+}
+
+// src/skill/uninstall.ts
+import { access as access4, rm, rmdir, stat } from "node:fs/promises";
+import { dirname as dirname5 } from "node:path";
+async function pathExists2(path) {
+ try {
+ await access4(path);
+ return true;
+ } catch {
+ return false;
+ }
+}
+async function removeFile(path) {
+ if (!await pathExists2(path)) {
+ return false;
+ }
+ await rm(path, { force: true });
+ return true;
+}
+async function removeEmptyDir(path) {
+ try {
+ const info = await stat(path);
+ if (!info.isDirectory()) {
+ return;
+ }
+ } catch {
+ return;
+ }
+ try {
+ await rmdir(path);
+ } catch {
+ }
+}
+async function uninstallSkill(options = {}) {
+ const removed = [];
+ const scopes = options.scope === void 0 ? ["user", "project"] : [options.scope];
+ for (const scope of scopes) {
+ const targets = getSkillInstallTargets({
+ scope,
+ home: options.home,
+ cwd: options.cwd
+ });
+ for (const target of targets) {
+ if (await removeFile(target.runnerPath)) {
+ removed.push(target.runnerPath);
+ }
+ if (await removeFile(target.path)) {
+ removed.push(target.path);
+ }
+ await removeEmptyDir(dirname5(target.runnerPath));
+ await removeEmptyDir(dirname5(dirname5(target.runnerPath)));
+ await removeEmptyDir(dirname5(target.path));
+ }
+ }
+ return { removed };
+}
+async function runUninstallSkill(options = {}) {
+ const result = await uninstallSkill(options);
+ exitOk(result, { json: options.json });
+}
+
+// src/commands/uninstall.ts
+async function runUninstall(options) {
+ return runUninstallSkill(options);
+}
+
+// src/commands/versions.ts
+async function runVersions(deckId, options, deps = {}) {
+ const config = await loadConfig();
+ if (!config.api_key) {
+ exitError(
+ {
+ code: "AUTH_REQUIRED",
+ message: "API key required to list deck versions",
+ hint: "Run `slidesfly login` or `slidesfly login --api-key YOUR_KEY`"
+ },
+ { json: options.json }
+ );
+ }
+ const api = deps.api ?? createApiClient();
+ try {
+ const result = await api.listVersions(deckId, config.api_key);
+ if (options.json || !process.stdout.isTTY) {
+ exitOk(result, { json: options.json });
+ }
+ if (result.versions.length === 0) {
+ exitOk(result, { json: false });
+ }
+ const header = "VERSION CURRENT CREATED SIZE";
+ const rows = result.versions.map((v) => {
+ const current = v.is_current ? "yes" : "";
+ const created = v.created_at.replace("T", " ").replace(/\.\d+Z$/, "Z");
+ return `${String(v.version).padEnd(8)} ${current.padEnd(8)} ${created.padEnd(22)} ${v.size_bytes}`;
+ });
+ process.stdout.write(`${[header, ...rows].join("\n")}
+`);
+ process.exit(0);
+ } catch (err) {
+ if (err instanceof ApiError) {
+ exitError(
+ { code: err.code, message: err.message, details: err.details, hint: err.hint },
+ { json: options.json }
+ );
+ }
+ throw err;
+ }
+}
+
+// src/commands/visibility.ts
+var VALID_VISIBILITY = /* @__PURE__ */ new Set(["public", "unlisted", "private"]);
+async function runVisibility(deckId, visibility, options, deps = {}) {
+ if (!VALID_VISIBILITY.has(visibility)) {
+ exitError(
+ {
+ code: "INVALID_ARGUMENT",
+ message: `Invalid visibility \`${visibility}\`. Expected public, unlisted, or private.`
+ },
+ { json: options.json }
+ );
+ }
+ const config = await loadConfig();
+ const deck = findAnonDeck(config, deckId);
+ const api = deps.api ?? createApiClient();
+ if (deck) {
+ try {
+ await api.visibilityAnon(deck.deck_id, visibility);
+ exitError(
+ {
+ code: "UNEXPECTED",
+ message: "Visibility change should not succeed for anonymous decks"
+ },
+ { json: options.json }
+ );
+ } catch (err) {
+ if (err instanceof ApiError) {
+ const claimedElsewhere = err.code === "DECK_NOT_OWNED" && Boolean(config.api_key);
+ if (!claimedElsewhere) {
+ exitError(
+ {
+ code: err.code,
+ message: err.message,
+ details: err.details,
+ hint: err.hint
+ },
+ { json: options.json }
+ );
+ }
+ } else {
+ throw err;
+ }
+ }
+ }
+ if (!config.api_key) {
+ exitError(
+ {
+ code: "AUTH_REQUIRED",
+ message: "API key required to change owned deck visibility",
+ hint: "Run `slidesfly login` or `slidesfly login --api-key YOUR_KEY`"
+ },
+ { json: options.json }
+ );
+ }
+ try {
+ const result = await api.setVisibilityOwned(deckId, visibility, config.api_key);
+ await removeAnonDeck(deckId);
+ exitOk(result, { json: options.json });
+ } catch (err) {
+ if (err instanceof ApiError) {
+ exitError(
+ {
+ code: err.code,
+ message: err.message,
+ details: err.details,
+ hint: err.hint
+ },
+ { json: options.json }
+ );
+ }
+ throw err;
+ }
+}
+
+// src/index.ts
+function readVersion() {
+ if (true) {
+ return "0.1.3";
+ }
+ try {
+ const pkgPath = join6(dirname6(fileURLToPath3(import.meta.url)), "../package.json");
+ return JSON.parse(readFileSync(pkgPath, "utf8")).version ?? "0.0.0";
+ } catch {
+ return "0.0.0";
+ }
+}
+var program2 = new Command();
+program2.name("slidesfly").description("Publish HTML decks to Slidesfly").version(readVersion()).option("--api-key ", "API key override").hook("preAction", async (thisCommand) => {
+ const opts = thisCommand.opts();
+ if (opts.apiKey) {
+ await setApiKey(opts.apiKey);
+ }
+});
+program2.command("install").description("Install Slidesfly agent skill for Claude Code, Cursor, and Codex").option("--target ", "auto, claude-code, cursor, codex, or all", "auto").option("--scope ", "user or project", "user").option("--force", "Overwrite modified skill files").option("--skill-only", "Install skill files only").option("--from-url ", "Fetch canonical SKILL.md from URL").option("--json", "Output JSON").action(
+ async (options) => {
+ await runInstall({
+ target: options.target,
+ scope: options.scope,
+ force: options.force,
+ skillOnly: options.skillOnly,
+ fromUrl: options.fromUrl,
+ json: options.json
+ });
+ }
+);
+program2.command("publish").description("Publish an HTML deck anonymously").argument("", "Path to .html file").option("--title ", "Deck title").option("--id ", "Update an existing local anonymous or owned deck").option("--visibility ", "public, unlisted, or private (owned publish only)").option("--json", "Output JSON").action(
+ async (file, options) => {
+ await runPublish(file, {
+ title: options.title,
+ deckId: options.id,
+ visibility: options.visibility,
+ json: options.json
+ });
+ }
+);
+program2.command("list").description("List anonymous decks from local config").option("--json", "Output JSON").action(async (options) => {
+ await runList(options);
+});
+program2.command("open").description("Open a deck URL in the default browser").argument("", "Deck ID").option("--json", "Output JSON").action(async (deckId, options) => {
+ await runOpen(deckId, options);
+});
+program2.command("delete").description("Delete an anonymous deck").argument("", "Deck ID").option("--json", "Output JSON").action(async (deckId, options) => {
+ await runDelete(deckId, options);
+});
+program2.command("visibility").description("Change deck visibility (anonymous decks are limited)").argument("", "Deck ID").argument("", "public, unlisted, or private").option("--json", "Output JSON").action(async (deckId, visibility, options) => {
+ await runVisibility(deckId, visibility, options);
+});
+program2.command("versions").description("List versions of an owned deck").argument("", "Deck ID").option("--json", "Output JSON").action(async (deckId, options) => {
+ await runVersions(deckId, options);
+});
+program2.command("restore").description("Restore an owned deck to a previous version (creates a new version)").argument("", "Deck ID").argument("", "Version number to restore", (v) => Number(v)).option("--json", "Output JSON").action(async (deckId, version, options) => {
+ await runRestore(deckId, { version, json: options.json });
+});
+program2.command("expire").description("Set or clear deck link expiry (Pro plan)").argument("", "Deck ID").argument("", "Duration (7d, 24h, 30m), ISO-8601 timestamp, or off").option("--json", "Output JSON").action(async (deckId, when, options) => {
+ await runExpire(deckId, when, options);
+});
+program2.command("password").description("Set or clear a deck password (Pro plan)").argument("", "Deck ID").argument("", "Password to set, or off to remove protection").option("--json", "Output JSON").action(async (deckId, password, options) => {
+ await runPassword(deckId, password, options);
+});
+program2.command("allowlist").description("Set or clear a deck email allowlist (Pro plan)").argument("", "Deck ID").argument("", "Emails to allow, or off to remove the allowlist").option("--json", "Output JSON").action(async (deckId, emails, options) => {
+ await runAllowlist(deckId, emails, options);
+});
+program2.command("login").description("Log in via browser PKCE flow (or store an API key)").option("--api-key ", "Store API key in local config (fallback)").option("--code", "Use device code flow (headless / SSH)").option("--no-claim", "Skip auto-claiming anonymous decks after login").option("--json", "Output JSON").action(
+ async (options) => {
+ await runLogin({
+ apiKey: options.apiKey,
+ code: options.code,
+ noClaim: options.noClaim,
+ json: options.json
+ });
+ }
+);
+program2.command("claim").description("Claim anonymous decks to your account").argument("[deck_id]", "Optional deck ID to claim from local config").option("--json", "Output JSON").action(async (deckId, options) => {
+ await runClaim(deckId, options);
+});
+program2.command("logout").description("Clear stored API key").option("--json", "Output JSON").action(async (options) => {
+ await runLogout(options);
+});
+program2.command("status").description("Show CLI config status").option("--json", "Output JSON").action(async (options) => {
+ await runStatus(options);
+});
+program2.command("uninstall").description("Remove installed Slidesfly agent skill files").option("--scope ", "user, project, or omit for both").option("--json", "Output JSON").action(async (options) => {
+ await runUninstall({
+ scope: options.scope,
+ json: options.json
+ });
+});
+program2.parse();