@hypersocial/decks-cli 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,71 @@
1
+ // The deck templates as text: the narrated outline and the layout library, filled
2
+ // for a slug and title. No file I/O beyond reading the packaged templates, so the
3
+ // hosted MCP route (new_deck) and the CLI (deck new) assemble the same deck.
4
+ import { packageText } from "./files.mjs";
5
+
6
+ export const NARRATED_FILES = { "index.html": "narrated-deck.html", "script.md": "script.md", "evidence.md": "evidence.md" };
7
+ // The library, in a sensible deck order; `--layouts` takes any order and repeats.
8
+ export const LAYOUTS = ["hero", "number", "product", "quote", "compare", "timeline", "features", "chart", "team", "closer"];
9
+ const LAYOUT_ALIASES = { "big-number": "number", "product-shot": "product", comparison: "compare", "three-up": "features" };
10
+
11
+ const escapeHtml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
12
+
13
+ // Template text with __SLUG__ / __TITLE__ filled; HTML gets an escaped title.
14
+ // Without a presenter style the player tag carries none (the player's default).
15
+ export function fillTemplate(file, { slug, title, presenterStyle }) {
16
+ const text = packageText(`templates/${file}`);
17
+ const t = /\.html$/.test(file) ? escapeHtml(title) : title;
18
+ const styled = presenterStyle ? text.split("__PRESENTER_STYLE__").join(presenterStyle) : text.replace(/\s*data-presenter-style="__PRESENTER_STYLE__"/g, "");
19
+ return styled.split("__SLUG__").join(slug).split("__TITLE__").join(t);
20
+ }
21
+
22
+ // One layout file: a leading comment of `key: value` lines (layout, summary,
23
+ // use, slot, presenter, cues, script), its <style> and its <section>.
24
+ export function readLayout(name) {
25
+ const text = packageText(`templates/layouts/${name}.html`);
26
+ const head = /^<!--([\s\S]*?)-->/.exec(text);
27
+ const style = /<style>[\s\S]*?<\/style>/.exec(text);
28
+ const section = /<section\b[\s\S]*<\/section>/.exec(text);
29
+ if (!head || !style || !section) throw new Error(`layout ${name}: needs a leading comment, a <style> and a <section>`);
30
+ const meta = {};
31
+ for (const line of head[1].split("\n")) {
32
+ const m = /^([a-z]+):\s*(.*)$/.exec(line.trim());
33
+ if (m) meta[m[1]] = m[2];
34
+ }
35
+ return { name, meta, style: style[0], section: section[0] };
36
+ }
37
+
38
+ export function parseLayouts(list) {
39
+ const names = String(list).split(",").map((n) => n.trim().toLowerCase()).filter(Boolean).map((n) => LAYOUT_ALIASES[n] ?? n);
40
+ const unknown = names.filter((n) => !LAYOUTS.includes(n));
41
+ if (unknown.length) throw new Error(`unknown layout${unknown.length > 1 ? "s" : ""} ${unknown.join(", ")}: pick from ${LAYOUTS.join(", ")}`);
42
+ if (!names.length) throw new Error(`--layouts needs at least one of ${LAYOUTS.join(", ")}`);
43
+ return names;
44
+ }
45
+
46
+ // index.html, script.md and evidence.md for a deck of `layouts`, one slide
47
+ // each, in order. A layout used twice gets ids hero, hero-2, …; ids inside a
48
+ // slide start with its id, so its cues stay unique.
49
+ export function assembleLayoutDeck({ slug, title, presenterStyle, layouts }) {
50
+ const names = parseLayouts(layouts);
51
+ const used = new Map();
52
+ const slides = names.map((name) => {
53
+ const n = (used.get(name) ?? 0) + 1;
54
+ used.set(name, n);
55
+ return { layout: readLayout(name), id: n === 1 ? name : `${name}-${n}` };
56
+ });
57
+ const fill = (text, id, t) => text.split("__ID__").join(id).split("__SLUG__").join(slug).split("__TITLE__").join(t);
58
+ const sections = slides.map(({ layout, id }, i) => {
59
+ let html = fill(layout.section, id, escapeHtml(title));
60
+ if (i === 0 && !/\sdata-intro=/.test(html.slice(0, html.indexOf(">")))) html = html.replace("<section ", '<section data-intro="Hi! Press play." ');
61
+ return `<!-- layout: ${layout.name}. ${layout.meta.summary ?? ""} -->\n${html}`;
62
+ });
63
+ const styles = [...new Set(names)].map((name) => `<!-- layout: ${name} -->\n${readLayout(name).style}`);
64
+ const shell = fillTemplate("layouts/_deck.html", { slug, title, presenterStyle });
65
+ // Function replacements: a "$" in the title or a layout is text, not a replacement pattern.
66
+ const html = shell.replace("<!-- LAYOUT STYLES -->", () => styles.join("\n")).replace("<!-- SLIDES -->", () => sections.join("\n\n"));
67
+ const intro = fillTemplate("script.md", { slug, title }).split(/\n## /)[0].trimEnd();
68
+ const script = `${intro}\n\n${slides.map(({ layout, id }) => `## ${id}\n${fill(layout.meta.script ?? "", id, title)}`).join("\n\n")}\n`;
69
+ return { "index.html": html, "script.md": script, "evidence.md": fillTemplate("evidence.md", { slug, title }) };
70
+ }
71
+
package/src/narrate.mjs CHANGED
@@ -8,18 +8,8 @@
8
8
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
9
  import { basename, join, resolve } from "node:path";
10
10
  import * as ops from "./ops.mjs";
11
-
12
- // Same rule as sectionIds() in lib/deckScript.ts (the published CLI cannot import it).
13
- export const sectionIds = (html) => [...html.matchAll(/<section\b[^>]*?\sid\s*=\s*["']([^"']+)["']/gi)].map((m) => m[1]);
14
-
15
- // Set data-manifest on the hosted player's <script> tag; html is unchanged when
16
- // there is no player tag or it already names this URL.
17
- export function setManifestUrl(html, url) {
18
- return html.replace(/<script\b[^>]*\/player\/v\d+\/player\.js"[^>]*>/, (tag) =>
19
- /\sdata-manifest\s*=\s*"[^"]*"/.test(tag)
20
- ? tag.replace(/(\sdata-manifest\s*=\s*)"[^"]*"/, (_, lead) => `${lead}"${url}"`)
21
- : tag.replace(/\s*>$/, () => ` data-manifest="${url}">`));
22
- }
11
+ export { sectionIds, setManifestUrl } from "./checkRules.mjs";
12
+ import { sectionIds, setManifestUrl } from "./checkRules.mjs";
23
13
 
24
14
  function slideList(value) {
25
15
  const ids = value.split(",").map((s) => s.trim()).filter(Boolean);
package/src/ops.mjs CHANGED
@@ -181,6 +181,7 @@ export async function reviewDeck(opts, fields = {}) {
181
181
  if (!fields.slug) throw new Error("slug is required");
182
182
  const args = { slug: String(fields.slug) };
183
183
  if (fields.script) args.script = String(fields.script);
184
+ if (fields.draft) args.draft = true;
184
185
  return (await client(opts)).action(api.review.reviewDeck, args);
185
186
  }
186
187
 
@@ -245,8 +246,9 @@ export async function pollVideoJob(opts, jobId, { intervalMs = 5000, timeoutMs =
245
246
  // thin client calls shared by the CLI `narrate` command and the MCP tools.
246
247
  function narrationArgs(fields = {}) {
247
248
  if (!fields.slug) throw new Error("slug is required");
248
- if (!fields.script) throw new Error("script is required (the deck's script.md)");
249
- const args = { slug: String(fields.slug), script: String(fields.script) };
249
+ // No script: narrate the deck's hosted script.md (deckSources), and the
250
+ // finished job writes the manifest URL into the hosted draft.
251
+ const args = fields.script ? { slug: String(fields.slug), script: String(fields.script) } : { slug: String(fields.slug), fromSource: true };
250
252
  if (fields.voice) args.voice = String(fields.voice);
251
253
  if (fields.slides) {
252
254
  if (!fields.slides.length) throw new Error("slides is empty; omit it to narrate every slide");
package/src/runtime.mjs CHANGED
@@ -8,7 +8,7 @@ import { fileURLToPath } from "node:url";
8
8
  import { ConvexHttpClient } from "convex/browser";
9
9
  import { makeFunctionReference } from "convex/server";
10
10
 
11
- export const VERSION = "0.2.2";
11
+ export const VERSION = "0.3.0";
12
12
  const UPLOAD_TIMEOUT_MS = 125_000;
13
13
 
14
14
  export const PROD = {
@@ -24,6 +24,16 @@ export const api = {
24
24
  generateUploadUrl: makeFunctionReference("assets:generateUploadUrl"),
25
25
  saveAsset: makeFunctionReference("assets:saveAsset"),
26
26
  },
27
+ assetsNode: {
28
+ importAssetFromUrl: makeFunctionReference("assetsNode:importAssetFromUrl"),
29
+ },
30
+ deckSources: {
31
+ create: makeFunctionReference("deckSources:create"),
32
+ get: makeFunctionReference("deckSources:get"),
33
+ write: makeFunctionReference("deckSources:write"),
34
+ check: makeFunctionReference("deckSources:check"),
35
+ release: makeFunctionReference("deckSources:release"),
36
+ },
27
37
  activity: {
28
38
  overview: makeFunctionReference("activity:overview"),
29
39
  setArchived: makeFunctionReference("activity:setArchived"),
@@ -89,7 +99,7 @@ export const api = {
89
99
  },
90
100
  };
91
101
 
92
- const BOOL = new Set(["prod", "dev", "admin", "json", "require-email", "qa", "no-notify", "help", "version", "all-agents", "global", "project", "transparent", "upload", "dry-run", "narrated", "layout", "full-page", "force"]);
102
+ const BOOL = new Set(["prod", "dev", "admin", "json", "require-email", "qa", "no-notify", "help", "version", "all-agents", "global", "project", "transparent", "upload", "dry-run", "narrated", "layout", "full-page", "force", "hosted", "yes", "no-voice", "no-skill", "no-release", "no-open"]);
93
103
  // release's recipients, in order: each --to starts one, and an --email right
94
104
  // after it belongs to it (`--to A --email a --to B`). Collected as opts.recipients;
95
105
  // opts.email stays the last --email for the other commands (share new).
@@ -274,7 +284,42 @@ function splitAgents(value) {
274
284
  return String(value || "").split(/[,\s]+/).filter(Boolean);
275
285
  }
276
286
 
277
- export function installHyperdecksSkill(opts = {}, extraArgs = []) {
287
+ export const DEFAULT_AGENTS = ["codex", "claude-code"];
288
+
289
+ // Agents `npx skills` installs into, keyed by its agent id: the directory whose
290
+ // presence means the agent is installed (the same test `npx skills` uses) and
291
+ // where its global skills live. From skills@1.7 (dist/cli.mjs, agent table).
292
+ export function agentTable({ home = homedir(), env = process.env } = {}) {
293
+ const config = env.XDG_CONFIG_HOME || join(home, ".config");
294
+ const claude = env.CLAUDE_CONFIG_DIR || join(home, ".claude");
295
+ return [
296
+ { id: "claude-code", label: "Claude Code", home: claude, skills: join(claude, "skills") },
297
+ { id: "codex", label: "Codex", home: env.CODEX_HOME || join(home, ".codex"), skills: join(home, ".agents", "skills") },
298
+ { id: "cursor", label: "Cursor", home: join(home, ".cursor"), skills: join(home, ".cursor", "skills") },
299
+ { id: "gemini-cli", label: "Gemini CLI", home: join(home, ".gemini"), skills: join(home, ".gemini", "skills") },
300
+ { id: "opencode", label: "OpenCode", home: join(config, "opencode"), skills: join(config, "opencode", "skills") },
301
+ { id: "windsurf", label: "Windsurf", home: join(home, ".codeium", "windsurf"), skills: join(home, ".codeium", "windsurf", "skills") },
302
+ { id: "github-copilot", label: "GitHub Copilot", home: join(home, ".copilot"), skills: join(home, ".copilot", "skills") },
303
+ ];
304
+ }
305
+
306
+ // The agents whose home directory exists on this machine.
307
+ export function detectAgents({ home, env, exists = existsSync } = {}) {
308
+ return agentTable({ home, env }).filter((a) => exists(a.home)).map((a) => a.id);
309
+ }
310
+
311
+ // Which agents an install targets: --all-agents, --agent, or the detected ones
312
+ // (today's codex,claude-code when none is found).
313
+ export function chooseAgents(opts = {}, extraArgs = [], detect = {}) {
314
+ if (opts["all-agents"]) return { agents: ["*"], source: "all" };
315
+ if (opts.agent) return { agents: [...splitAgents(opts.agent), ...extraArgs.flatMap(splitAgents)], source: "flag" };
316
+ const found = detectAgents(detect);
317
+ return found.length ? { agents: found, source: "detected" } : { agents: DEFAULT_AGENTS, source: "default" };
318
+ }
319
+
320
+ // `quiet` pipes `npx skills` output instead of inheriting the terminal (init
321
+ // --json keeps stdout for its one JSON object); a failure then carries its tail.
322
+ export function installHyperdecksSkill(opts = {}, extraArgs = [], { spawn = spawnSync, quiet = false, log = (line) => console.log(line), home, env } = {}) {
278
323
  const skillDir = join(PACKAGED_SKILLS_DIR, "hyperdecks");
279
324
  if (!existsSync(skillDir)) {
280
325
  throw new Error(`packaged hyperdecks skill not found at ${skillDir}`);
@@ -292,19 +337,19 @@ export function installHyperdecksSkill(opts = {}, extraArgs = []) {
292
337
  throw new Error(`unexpected argument(s): ${extraArgs.join(" ")}; pass agent ids with --agent`);
293
338
  }
294
339
 
295
- const agents = opts["all-agents"]
296
- ? ["*"]
297
- : [...splitAgents(opts.agent || "codex,claude-code"), ...extraArgs.flatMap(splitAgents)];
340
+ const { agents, source } = chooseAgents(opts, extraArgs, { home, env });
341
+ if (source === "default" && !quiet) log(`no agent found in your home directory; installing for ${agents.join(", ")}`);
298
342
  const args = ["-y", "skills@latest", "add", PACKAGED_SKILLS_DIR, "-g"];
299
343
  args.push("-a", ...agents, "-s", "hyperdecks", "--copy", "-y", "--full-depth");
300
344
 
301
345
  const command = process.platform === "win32" ? "npx.cmd" : "npx";
302
- const result = spawnSync(command, args, { stdio: "inherit" });
346
+ const result = spawn(command, args, quiet ? { stdio: ["ignore", "pipe", "pipe"], encoding: "utf8" } : { stdio: "inherit" });
303
347
  if (result.error) throw result.error;
304
348
  if (result.status !== 0) {
305
- throw new Error(`skills add failed with exit code ${result.status}`);
349
+ const tail = quiet ? `${result.stdout || ""}\n${result.stderr || ""}`.trim().split("\n").filter((l) => l.trim() && !/^npm warn/i.test(l)).slice(-3).join(" ") : "";
350
+ throw new Error(`skills add failed with exit code ${result.status}${tail ? `: ${tail}` : ""}`);
306
351
  }
307
- return { installed: true };
352
+ return { installed: true, agents, source };
308
353
  }
309
354
 
310
355
  async function verifyAndStampEmail(t, opts) {
@@ -316,14 +361,14 @@ async function verifyAndStampEmail(t, opts) {
316
361
  return who;
317
362
  }
318
363
 
319
- export async function login(opts) {
364
+ export async function login(opts, { log = (line) => console.log(line), open = openBrowser } = {}) {
320
365
  const t = target(opts);
321
366
 
322
367
  if (opts.token) {
323
368
  saveCred(t.name, { refreshToken: opts.token, convexUrl: t.convexUrl, brokerBase: t.brokerBase });
324
369
  try {
325
370
  const who = await verifyAndStampEmail(t, opts);
326
- console.log(`OK logged in to ${t.name}${who.email ? ` as ${who.email}` : ""}`);
371
+ log(`OK logged in to ${t.name}${who.email ? ` as ${who.email}` : ""}`);
327
372
  } catch (e) {
328
373
  clearCred(t.name);
329
374
  throw e;
@@ -381,14 +426,14 @@ export async function login(opts) {
381
426
  server.listen(0, "127.0.0.1", () => {
382
427
  const { port } = server.address();
383
428
  const url = `${t.brokerBase}/cli/authorize?port=${port}&state=${state}`;
384
- console.log(`Opening your browser to sign in (${t.name})...\n ${url}\nIf it does not open, paste that URL into your browser.`);
385
- openBrowser(url);
429
+ log(`Opening your browser to sign in (${t.name})...\n ${url}\nIf it does not open, paste that URL into your browser.`);
430
+ open(url);
386
431
  });
387
432
  timer = setTimeout(() => finish(new Error("login timed out after 3 min")), 180000);
388
433
  });
389
434
 
390
435
  const who = await verifyAndStampEmail(t, opts);
391
- console.log(`OK logged in to ${t.name}${who.email ? ` as ${who.email}` : ""}`);
436
+ log(`OK logged in to ${t.name}${who.email ? ` as ${who.email}` : ""}`);
392
437
  }
393
438
 
394
439
  export async function logout(opts) {
package/src/scaffold.mjs CHANGED
@@ -5,82 +5,17 @@
5
5
  // templates/layouts/), script.md (the narration) and evidence.md (where each
6
6
  // fact comes from), then registers the deck as a draft you own so narrate can
7
7
  // render into it.
8
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
9
- import { dirname, join, resolve } from "node:path";
10
- import { fileURLToPath } from "node:url";
8
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
9
+ import { join, resolve } from "node:path";
11
10
  import * as ops from "./ops.mjs";
12
- import { PRESENTER_STYLES } from "./check.mjs";
11
+ import { PRESENTER_STYLES } from "./checkRules.mjs";
12
+ import { LAYOUTS, NARRATED_FILES, assembleLayoutDeck, fillTemplate } from "./layouts.mjs";
13
+
14
+ export { LAYOUTS, NARRATED_FILES, assembleLayoutDeck, fillTemplate, parseLayouts, readLayout } from "./layouts.mjs";
13
15
 
14
- export const TEMPLATE_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "templates");
15
- export const NARRATED_FILES = { "index.html": "narrated-deck.html", "script.md": "script.md", "evidence.md": "evidence.md" };
16
16
  const SLUG = /^[a-z0-9][a-z0-9-]{0,63}$/;
17
- export const LAYOUT_DIR = join(TEMPLATE_DIR, "layouts");
18
- // The library, in a sensible deck order; `--layouts` takes any order and repeats.
19
- export const LAYOUTS = ["hero", "number", "product", "quote", "compare", "timeline", "features", "chart", "team", "closer"];
20
- const LAYOUT_ALIASES = { "big-number": "number", "product-shot": "product", comparison: "compare", "three-up": "features" };
21
17
  const USAGE = 'usage: deck new <slug> "<title>" --narrated [--layouts hero,product,number,closer] [--presenter-style <stage|bubble|glass|corner>] [--dir <path>]';
22
18
 
23
- const escapeHtml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
24
-
25
- // Template text with __SLUG__ / __TITLE__ filled; HTML gets an escaped title.
26
- // Without a presenter style the player tag carries none (the player's default).
27
- export function fillTemplate(file, { slug, title, presenterStyle }) {
28
- const text = readFileSync(join(TEMPLATE_DIR, file), "utf8");
29
- const t = /\.html$/.test(file) ? escapeHtml(title) : title;
30
- const styled = presenterStyle ? text.split("__PRESENTER_STYLE__").join(presenterStyle) : text.replace(/\s*data-presenter-style="__PRESENTER_STYLE__"/g, "");
31
- return styled.split("__SLUG__").join(slug).split("__TITLE__").join(t);
32
- }
33
-
34
- // One layout file: a leading comment of `key: value` lines (layout, summary,
35
- // use, slot, presenter, cues, script), its <style> and its <section>.
36
- export function readLayout(name) {
37
- const text = readFileSync(join(LAYOUT_DIR, `${name}.html`), "utf8");
38
- const head = /^<!--([\s\S]*?)-->/.exec(text);
39
- const style = /<style>[\s\S]*?<\/style>/.exec(text);
40
- const section = /<section\b[\s\S]*<\/section>/.exec(text);
41
- if (!head || !style || !section) throw new Error(`layout ${name}: needs a leading comment, a <style> and a <section>`);
42
- const meta = {};
43
- for (const line of head[1].split("\n")) {
44
- const m = /^([a-z]+):\s*(.*)$/.exec(line.trim());
45
- if (m) meta[m[1]] = m[2];
46
- }
47
- return { name, meta, style: style[0], section: section[0] };
48
- }
49
-
50
- export function parseLayouts(list) {
51
- const names = String(list).split(",").map((n) => n.trim().toLowerCase()).filter(Boolean).map((n) => LAYOUT_ALIASES[n] ?? n);
52
- const unknown = names.filter((n) => !LAYOUTS.includes(n));
53
- if (unknown.length) throw new Error(`unknown layout${unknown.length > 1 ? "s" : ""} ${unknown.join(", ")}: pick from ${LAYOUTS.join(", ")}`);
54
- if (!names.length) throw new Error(`--layouts needs at least one of ${LAYOUTS.join(", ")}`);
55
- return names;
56
- }
57
-
58
- // index.html, script.md and evidence.md for a deck of `layouts`, one slide
59
- // each, in order. A layout used twice gets ids hero, hero-2, …; ids inside a
60
- // slide start with its id, so its cues stay unique.
61
- export function assembleLayoutDeck({ slug, title, presenterStyle, layouts }) {
62
- const names = parseLayouts(layouts);
63
- const used = new Map();
64
- const slides = names.map((name) => {
65
- const n = (used.get(name) ?? 0) + 1;
66
- used.set(name, n);
67
- return { layout: readLayout(name), id: n === 1 ? name : `${name}-${n}` };
68
- });
69
- const fill = (text, id, t) => text.split("__ID__").join(id).split("__SLUG__").join(slug).split("__TITLE__").join(t);
70
- const sections = slides.map(({ layout, id }, i) => {
71
- let html = fill(layout.section, id, escapeHtml(title));
72
- if (i === 0 && !/\sdata-intro=/.test(html.slice(0, html.indexOf(">")))) html = html.replace("<section ", '<section data-intro="Hi! Press play." ');
73
- return `<!-- layout: ${layout.name}. ${layout.meta.summary ?? ""} -->\n${html}`;
74
- });
75
- const styles = [...new Set(names)].map((name) => `<!-- layout: ${name} -->\n${readLayout(name).style}`);
76
- const shell = fillTemplate("layouts/_deck.html", { slug, title, presenterStyle });
77
- // Function replacements: a "$" in the title or a layout is text, not a replacement pattern.
78
- const html = shell.replace("<!-- LAYOUT STYLES -->", () => styles.join("\n")).replace("<!-- SLIDES -->", () => sections.join("\n\n"));
79
- const intro = fillTemplate("script.md", { slug, title }).split(/\n## /)[0].trimEnd();
80
- const script = `${intro}\n\n${slides.map(({ layout, id }) => `## ${id}\n${fill(layout.meta.script ?? "", id, title)}`).join("\n\n")}\n`;
81
- return { "index.html": html, "script.md": script, "evidence.md": fillTemplate("evidence.md", { slug, title }) };
82
- }
83
-
84
19
  export async function deckNew(pos, opts = {}, { log = (line) => console.log(line), cwd = process.cwd() } = {}) {
85
20
  const [slug, title] = pos;
86
21
  if (!slug || !title) throw new Error(USAGE);
@@ -90,7 +25,7 @@ export async function deckNew(pos, opts = {}, { log = (line) => console.log(line
90
25
  throw new Error(`--presenter-style must be one of ${PRESENTER_STYLES.join(", ")}`);
91
26
  }
92
27
  if (!SLUG.test(slug)) throw new Error("slug must be lowercase letters, digits and dashes (max 64)");
93
- const dir = resolve(cwd, typeof opts.dir === "string" ? opts.dir : join("decks", slug));
28
+ const dir = resolve(/*turbopackIgnore: true*/ cwd, typeof opts.dir === "string" ? opts.dir : join("decks", slug));
94
29
  if (existsSync(dir)) throw new Error(`${dir} already exists; pick another slug or --dir`);
95
30
  if (opts.layouts === true) throw new Error(`--layouts needs a list, e.g. --layouts ${LAYOUTS.slice(0, 3).join(",")},closer`);
96
31
  const files = opts.layouts !== undefined
@@ -99,7 +34,7 @@ export async function deckNew(pos, opts = {}, { log = (line) => console.log(line
99
34
  await ops.createDeck(opts, { slug, title });
100
35
  try {
101
36
  mkdirSync(dir, { recursive: true });
102
- for (const [out, text] of files) writeFileSync(join(dir, out), text);
37
+ for (const [out, text] of files) writeFileSync(join(/*turbopackIgnore: true*/ dir, out), text);
103
38
  } catch (e) {
104
39
  rmSync(dir, { recursive: true, force: true });
105
40
  throw new Error(`deck ${slug} is registered but its files could not be written (${e.message}); remove the draft with \`hyperdecks deck rm ${slug}\` and run deck new again`);
package/src/source.mjs ADDED
@@ -0,0 +1,69 @@
1
+ // The hosted deck source (convex/deckSources.ts): a deck's files kept on
2
+ // Hyperdecks as a draft, for chat agents on the remote MCP and for
3
+ // hyperdecks deck new <slug> "<title>" --narrated --hosted [--layouts …]
4
+ // hyperdecks source pull <slug> [dir] the draft into a folder
5
+ // hyperdecks source push <slug> [dir] [--base-rev N] a folder's files to the draft
6
+ // hyperdecks check <slug> --hosted
7
+ // hyperdecks release <slug> --hosted [--to "<name>" [--email e]]... [--qa]
8
+ // hyperdecks asset add <slug> <file|url> [--name n]
9
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
10
+ import { basename, extname, join, resolve } from "node:path";
11
+ import { api, client, uploadBytes } from "./runtime.mjs";
12
+ import { SOURCE_FILES, getDeckSource, uploadDeckAsset, writeDeckSource } from "./hosted.mjs";
13
+
14
+ export * from "./hosted.mjs";
15
+
16
+ // ---- CLI ------------------------------------------------------------------
17
+
18
+ export async function sourcePull(pos, opts = {}, { log = (line) => console.log(line), cwd = process.cwd() } = {}) {
19
+ const [slug, dirArg] = pos;
20
+ if (!slug) throw new Error("usage: source pull <slug> [dir]");
21
+ const dir = resolve(/*turbopackIgnore: true*/ cwd, dirArg ?? join("decks", slug));
22
+ const got = await getDeckSource(opts, { slug });
23
+ mkdirSync(dir, { recursive: true });
24
+ const written = [];
25
+ for (const [file, text] of Object.entries(got.files)) {
26
+ if (typeof text !== "string") continue;
27
+ writeFileSync(join(/*turbopackIgnore: true*/ dir, file), text);
28
+ written.push(file);
29
+ }
30
+ log(`✓ pulled ${slug} rev ${got.rev} → ${dir} (${written.join(", ")})\n push it back with: hyperdecks source push ${slug} ${dirArg ?? dir} --base-rev ${got.rev}`);
31
+ return { slug, rev: got.rev, dir, files: written };
32
+ }
33
+
34
+ export async function sourcePush(pos, opts = {}, { log = (line) => console.log(line), cwd = process.cwd() } = {}) {
35
+ const [slug, dirArg] = pos;
36
+ if (!slug) throw new Error("usage: source push <slug> [dir] [--base-rev N]");
37
+ const dir = resolve(/*turbopackIgnore: true*/ cwd, dirArg ?? join("decks", slug));
38
+ const files = SOURCE_FILES.filter((f) => existsSync(join(/*turbopackIgnore: true*/ dir, f)));
39
+ if (!files.includes("index.html")) throw new Error(`no index.html in ${dir}`);
40
+ let baseRev = opts["base-rev"] !== undefined ? Number(opts["base-rev"]) : undefined;
41
+ let rev;
42
+ for (const file of files) {
43
+ ({ rev } = await writeDeckSource(opts, { slug, file, content: readFileSync(join(/*turbopackIgnore: true*/ dir, file), "utf8"), baseRev }));
44
+ baseRev = baseRev === undefined ? undefined : rev; // each write moves the rev
45
+ }
46
+ log(`✓ pushed ${files.join(", ")} to ${slug}: rev ${rev}`);
47
+ return { slug, rev, files };
48
+ }
49
+
50
+ const TYPES = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp", ".avif": "image/avif", ".svg": "image/svg+xml", ".mp4": "video/mp4", ".webm": "video/webm", ".woff2": "font/woff2", ".woff": "font/woff" };
51
+
52
+ export async function assetAdd(pos, opts = {}, { log = (line) => console.log(line), cwd = process.cwd() } = {}) {
53
+ const [slug, from] = pos;
54
+ if (!slug || !from) throw new Error("usage: asset add <slug> <file|url> [--name <name>]");
55
+ let out;
56
+ if (/^https?:\/\//i.test(from)) out = await uploadDeckAsset(opts, { slug, name: opts.name, url: from });
57
+ else {
58
+ const path = resolve(/*turbopackIgnore: true*/ cwd, from);
59
+ const contentType = TYPES[extname(path).toLowerCase()];
60
+ if (!contentType) throw new Error(`${from}: asset add takes images, video and fonts (${Object.keys(TYPES).join(" ")})`);
61
+ const c = await client(opts);
62
+ const bytes = readFileSync(path);
63
+ const storageId = await uploadBytes(c, bytes, contentType);
64
+ const saved = await c.mutation(api.assets.saveAsset, { name: String(opts.name || basename(path)), storageId, contentType, deckSlug: slug });
65
+ out = { url: saved.url, name: saved.name, contentType, bytes: bytes.byteLength };
66
+ }
67
+ log(out.url);
68
+ return out;
69
+ }
package/src/voice.mjs CHANGED
@@ -11,6 +11,17 @@ import * as ops from "./ops.mjs";
11
11
  // Same text as convex/lib/voice/consent.ts (the published CLI cannot import it).
12
12
  export const CONSENT_STATEMENT = "I confirm this is my own voice and I consent to it being cloned for narrating my decks";
13
13
  export const CONSENT_PHRASE = "I consent";
14
+ // About 45 s read aloud at an easy pace (init prints it to record). Plain
15
+ // sentences with varied sounds, questions and numbers give the clone range.
16
+ export const READING_PASSAGE = [
17
+ "Hi, this is a recording of my own voice, for my Hyperdecks presenter.",
18
+ "I'm reading at my normal pace, the way I would talk to a friend across the table.",
19
+ "Some days start slowly, with coffee and a long list. Others start with a phone call at seven thirty.",
20
+ "Either way, I like to get to the point: what's the problem, who has it, and what would make it better?",
21
+ "When I show people my work, I want them to hear me, not a robot.",
22
+ "So here are a few numbers, said clearly: twelve, forty-five, three hundred, and two thousand and six.",
23
+ "Thanks for listening. That's the end of my sample.",
24
+ ].join(" ");
14
25
  export const MAX_SAMPLES = 3;
15
26
  export const MAX_SAMPLE_BYTES = 10 * 1024 * 1024;
16
27
  const TYPES = { ".mp3": "audio/mpeg", ".m4a": "audio/mp4", ".wav": "audio/wav" };
@@ -0,0 +1,26 @@
1
+ {
2
+ "about": "The first deck `hyperdecks init` makes when D.A.S.H. presents: the same three slides, introduced in the third person. D.A.S.H. quotes the answers rather than rephrasing them, so no pronoun is guessed from a name.",
3
+ "hero": {
4
+ "kicker": "Meet",
5
+ "title": "Meet __NAME__.",
6
+ "lead": "__WHAT__",
7
+ "script": "Hi, [wave] I'm D.A.S.H. [look: h1] I'm here to introduce __NAME__."
8
+ },
9
+ "features": {
10
+ "kicker": "In short",
11
+ "title": "Three things to know.",
12
+ "cards": [
13
+ { "name": "Who", "line": "__NAME__" },
14
+ { "name": "What", "line": "__WHAT__" },
15
+ { "name": "The ask", "line": "__ASK__" }
16
+ ],
17
+ "script": "Three quick things. [go: #features-f1] Who this is. [go: #features-f2] What __NAME__ does, in their own words. __WHAT__ [go: #features-f3] And what __NAME__ would love from you. [go: slot]"
18
+ },
19
+ "closer": {
20
+ "title": "Thanks.",
21
+ "lead": "What __NAME__ would love from you:",
22
+ "cta": "__ASK__",
23
+ "contact": "Presented by D.A.S.H. on hyperdecks.ai",
24
+ "script": "Here's the ask. [go: #closer-cta] __ASK__ Thanks [go: slot] for watching! [wave]"
25
+ }
26
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "about": "The first deck `hyperdecks init` makes, in your own voice: three slides from your three answers. __NAME__, __WHAT__ and __ASK__ are the answers; text is escaped before it goes into index.html.",
3
+ "hero": {
4
+ "kicker": "Hello",
5
+ "title": "Hi, I'm __NAME__.",
6
+ "lead": "__WHAT__",
7
+ "script": "Hi, [wave] I'm __NAME__. [look: h1] Thanks for taking a minute to watch this."
8
+ },
9
+ "features": {
10
+ "kicker": "In short",
11
+ "title": "Me, in three cards.",
12
+ "cards": [
13
+ { "name": "Who", "line": "__NAME__" },
14
+ { "name": "What I do", "line": "__WHAT__" },
15
+ { "name": "My ask", "line": "__ASK__" }
16
+ ],
17
+ "script": "Here's me, in three cards. [go: #features-f1] Who I am. [go: #features-f2] What I do. __WHAT__ [go: #features-f3] And what I'd love from you. [go: slot]"
18
+ },
19
+ "closer": {
20
+ "title": "Thanks.",
21
+ "lead": "One thing I'd love from you:",
22
+ "cta": "__ASK__",
23
+ "contact": "Made with hyperdecks.ai",
24
+ "script": "So here's my ask. [go: #closer-cta] __ASK__ Thanks [go: slot] for watching! [wave]"
25
+ }
26
+ }
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  maxTotalSeconds: 180
3
+ transition: drift
3
4
  ---
4
5
 
5
6
  # __TITLE__ — narration script
@@ -7,12 +8,16 @@ maxTotalSeconds: 180
7
8
  One `## <slide-id>` block per slide, in slide order; the id is the slide's
8
9
  `<section id>` in index.html. Each sentence is one caption: keep it a short
9
10
  phrase (4 to 85 characters, two lines in the bubble). Aim for 150 to 180 s in
10
- all. Holds: 1.1 s by default, `hold: 2` on interactive slides. Stage tags become
11
+ all. Holds: 0.4 s by default, `hold: 1.5` for a number, chart or reveal,
12
+ `hold: 2` on interactive slides. Stage tags become
11
13
  cues at the word before them: [wave], [mood: excited], [reveal: #detail],
12
14
  [go: #detail] (the presenter flies beside it; [go: slot] brings it home),
13
15
  [look: h2] (the head looks at it), [custom: confetti]. `presenterStyle: stage`
14
16
  above sets the deck's presenter; `presenter-style: corner` under a heading sets
15
- one slide's. With no `voice:` above, your first ready voice narrates.
17
+ one slide's. `transition: drift` above is how every slide enters;
18
+ `transition: push` (cut, wipe, zoom #id, morph, fly) under a heading changes
19
+ one slide's: see "Transitions" in the skill. With no `voice:` above, your first
20
+ ready voice narrates.
16
21
 
17
22
  hyperdecks narrate decks/__SLUG__ --dry-run
18
23
  hyperdecks narrate decks/__SLUG__