@markdstage/markdstage 0.1.3 → 2.4.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.
@@ -1,6 +1,6 @@
1
1
  // markdstage present — serve the deck on loopback and open it in a browser.
2
2
 
3
- import { mkdtemp, rm } from "node:fs/promises";
3
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
4
4
  import { join, resolve } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
  import { spawn } from "node:child_process";
@@ -19,73 +19,162 @@ async function createWatcher(session, server, { onStatus }) {
19
19
  const { createMarkdownWatcher } = await import(
20
20
  pathToFileURL(sharedPath("scripts", "markdown-watcher.mjs")).href
21
21
  );
22
- return createMarkdownWatcher({
22
+ const setWatchState = (status, error = "") => {
23
+ const changed = session.watchStatus !== status || session.watchError !== error;
24
+ session.watchStatus = status;
25
+ session.watchError = error;
26
+ if (changed) server.broadcast();
27
+ };
28
+ const watcher = createMarkdownWatcher({
23
29
  path: session.file,
24
30
  onChange: async () => {
25
31
  try {
32
+ if ((await readFile(session.file, "utf8")) === session.sourceMarkdown) {
33
+ setWatchState("watching");
34
+ return;
35
+ }
26
36
  // Keep the current slide, and keep the last valid deck when the file is
27
37
  // saved in a broken intermediate state.
28
38
  await session.load({ preserveIndex: true });
39
+ setWatchState("watching");
29
40
  server.broadcast();
30
41
  onStatus(`reloaded ${session.sourceName} (${session.slides.length} slides)`);
31
42
  } catch (error) {
43
+ setWatchState("error", error?.code || "source_reload_failed");
32
44
  onStatus(`reload failed, keeping the last valid deck: ${error?.message || error}`, true);
33
45
  }
34
46
  },
35
- onError: (error) => onStatus(`watch error: ${error?.message || error}`, true),
47
+ onError: (error) => {
48
+ setWatchState("error", "watch_failed");
49
+ onStatus(`watch error: ${error?.message || error}`, true);
50
+ },
36
51
  });
52
+ setWatchState("watching");
53
+ return watcher;
37
54
  }
38
55
 
39
56
  export async function presentCommand(options, io) {
40
- return withDeckServer(options, async (session, server) => {
41
- const watcher = options.watch ? await createWatcher(session, server, {
42
- onStatus: (message, isError) => io.status(message, isError),
43
- }) : null;
57
+ let audienceProcess = null;
58
+ let audienceProfileDir = "";
59
+ let audienceUrl = "";
44
60
 
45
- let browserProcess = null;
46
- let profileDir = "";
47
- if (options.open) {
48
- const browser = findChromiumBrowser();
49
- if (!browser) {
50
- throw new MarkdStageError(
51
- "presenter_browser_not_found",
52
- "Presenting requires Microsoft Edge, Google Chrome, or Chromium. Re-run with --no-open to serve the deck only.",
53
- );
54
- }
55
- profileDir = await mkdtemp(join(tmpdir(), "markdstage-presenter-window-"));
56
- browserProcess = spawn(
57
- browser,
58
- buildPresenterBrowserArgs({ profileDir, presenterUrl: server.url }),
59
- { windowsHide: false, stdio: "ignore" },
60
- );
61
- browserProcess.once("error", (error) => {
62
- io.status(`browser failed to start: ${error?.message || error}`, true);
63
- });
61
+ const closeAudience = async () => {
62
+ const process = audienceProcess;
63
+ audienceProcess = null;
64
+ if (isProcessRunning(process)) await terminateProcessTree(process);
65
+ if (audienceProfileDir) {
66
+ await rm(audienceProfileDir, { recursive: true, force: true }).catch(() => {});
67
+ audienceProfileDir = "";
64
68
  }
69
+ return { stopped: Boolean(process) };
70
+ };
65
71
 
66
- io.print(`MarkdStage is presenting ${session.sourceName || session.file}`);
67
- io.print(` slides: ${session.slides.length}`);
68
- io.print(` theme: ${session.theme}`);
69
- io.print(` workspace: ${resolve(session.workspaceRoot)}`);
70
- io.print(` url: ${server.url}`);
71
- if (options.watch) io.print(" watching: on (the deck reloads when the file is saved)");
72
- io.print("Press Ctrl+C to stop.");
72
+ const audience = options.presenterView
73
+ ? {
74
+ isRunning: () => isProcessRunning(audienceProcess),
75
+ open: async () => {
76
+ if (isProcessRunning(audienceProcess)) return { alreadyRunning: true };
77
+ await closeAudience();
78
+ const browser = findChromiumBrowser();
79
+ if (!browser) {
80
+ throw new MarkdStageError(
81
+ "presenter_browser_not_found",
82
+ "Opening the audience view requires Microsoft Edge, Google Chrome, or Chromium.",
83
+ );
84
+ }
85
+ audienceProfileDir = await mkdtemp(join(tmpdir(), "markdstage-audience-window-"));
86
+ audienceProcess = spawn(
87
+ browser,
88
+ buildPresenterBrowserArgs({
89
+ profileDir: audienceProfileDir,
90
+ presenterUrl: audienceUrl,
91
+ }),
92
+ { windowsHide: false, stdio: "ignore" },
93
+ );
94
+ await new Promise((ready, reject) => {
95
+ audienceProcess.once("spawn", ready);
96
+ audienceProcess.once("error", reject);
97
+ });
98
+ return { alreadyRunning: false };
99
+ },
100
+ close: closeAudience,
101
+ }
102
+ : null;
73
103
 
74
- await new Promise((done) => {
75
- const stop = () => {
76
- process.off("SIGINT", stop);
77
- process.off("SIGTERM", stop);
78
- done();
79
- };
80
- process.once("SIGINT", stop);
81
- process.once("SIGTERM", stop);
82
- if (browserProcess) browserProcess.once("close", stop);
83
- if (options.until) options.until.then(stop, stop);
84
- });
104
+ try {
105
+ return await withDeckServer({ ...options, presenter: audience }, async (session, server) => {
106
+ if (audience) {
107
+ const url = new URL(server.url);
108
+ url.searchParams.set("present", "1");
109
+ audienceUrl = url.href;
110
+ }
111
+ const watcher = options.watch
112
+ ? await createWatcher(session, server, {
113
+ onStatus: (message, isError) => io.status(message, isError),
114
+ })
115
+ : null;
85
116
 
86
- watcher?.close();
87
- if (isProcessRunning(browserProcess)) await terminateProcessTree(browserProcess);
88
- if (profileDir) await rm(profileDir, { recursive: true, force: true }).catch(() => {});
89
- return { ok: true, url: server.url, total: session.slides.length, theme: session.theme };
90
- });
117
+ let browserProcess = null;
118
+ let profileDir = "";
119
+ const browserUrl = new URL(server.url);
120
+ if (options.presenterView) browserUrl.searchParams.set("presenter", "1");
121
+ try {
122
+ if (options.open) {
123
+ const browser = findChromiumBrowser();
124
+ if (!browser) {
125
+ throw new MarkdStageError(
126
+ "presenter_browser_not_found",
127
+ "Presenting requires Microsoft Edge, Google Chrome, or Chromium. Re-run with --no-open to serve the deck only.",
128
+ );
129
+ }
130
+ profileDir = await mkdtemp(join(tmpdir(), "markdstage-presenter-window-"));
131
+ browserProcess = spawn(
132
+ browser,
133
+ buildPresenterBrowserArgs({ profileDir, presenterUrl: browserUrl.href }),
134
+ { windowsHide: false, stdio: "ignore" },
135
+ );
136
+ await new Promise((ready, reject) => {
137
+ browserProcess.once("spawn", ready);
138
+ browserProcess.once("error", reject);
139
+ });
140
+ }
141
+
142
+ io.print(`MarkdStage is presenting ${session.sourceName || session.file}`);
143
+ io.print(` slides: ${session.slides.length}`);
144
+ io.print(` theme: ${session.theme}`);
145
+ io.print(` workspace: ${resolve(session.workspaceRoot)}`);
146
+ io.print(` url: ${browserUrl.href}`);
147
+ if (options.watch) {
148
+ io.print(" watching: on (live reload and Architecture editing are enabled)");
149
+ }
150
+ io.print("Press Ctrl+C to stop.");
151
+
152
+ await new Promise((done) => {
153
+ const stop = () => {
154
+ process.off("SIGINT", stop);
155
+ process.off("SIGTERM", stop);
156
+ done();
157
+ };
158
+ process.once("SIGINT", stop);
159
+ process.once("SIGTERM", stop);
160
+ if (browserProcess) browserProcess.once("close", stop);
161
+ if (options.until) options.until.then(stop, stop);
162
+ });
163
+
164
+ return {
165
+ ok: true,
166
+ url: browserUrl.href,
167
+ total: session.slides.length,
168
+ theme: session.theme,
169
+ };
170
+ } finally {
171
+ watcher?.close();
172
+ await closeAudience();
173
+ if (isProcessRunning(browserProcess)) await terminateProcessTree(browserProcess);
174
+ if (profileDir) await rm(profileDir, { recursive: true, force: true }).catch(() => {});
175
+ }
176
+ });
177
+ } finally {
178
+ await closeAudience();
179
+ }
91
180
  }
package/src/deck.mjs CHANGED
@@ -23,6 +23,8 @@ export async function withDeckServer(options, run) {
23
23
  const server = await startPresentationServer(session, {
24
24
  token,
25
25
  onLog: options.log,
26
+ editable: options.watch === true,
27
+ presenter: options.presenter,
26
28
  });
27
29
  try {
28
30
  return await run(session, server);
package/src/runtime.mjs CHANGED
@@ -64,9 +64,15 @@ export const { MarkdStageError } = errors;
64
64
  export const { createDeckSession, readDeckSlides, resolveDeckFile, resolveDeckTheme } =
65
65
  deckSession;
66
66
  export const { createUrlToken, startPresentationServer } = presentationServer;
67
- export const { captureSlides, exportPdf, inspectLayout, MAX_CAPTURE_SLIDES } = output;
67
+ export const {
68
+ captureSlides,
69
+ exportPdf,
70
+ exportPptx,
71
+ inspectLayout,
72
+ MAX_CAPTURE_SLIDES,
73
+ } = output;
68
74
  export const { findChromiumBrowser, terminateProcessTree, isProcessRunning } = browser;
69
- export const { captureDirectoryName, pdfNameForSource } = outputPaths;
75
+ export const { captureDirectoryName, pdfNameForSource, pptxNameForSource } = outputPaths;
70
76
  export const {
71
77
  architectureValidationErrors,
72
78
  deckValidationFeedback,
package/src/skills.mjs CHANGED
@@ -24,11 +24,12 @@ export const SKILL_TARGETS = {
24
24
  };
25
25
 
26
26
  const DESCRIPTION =
27
- "Turn Markdown into 16:9 slides with the MarkdStage CLI. Use when the user " +
28
- 'asks to present, preview, validate, screenshot, or export a Markdown deck ("present slides.md", ' +
29
- '"turn this file into slides", "export the deck to PDF", "check whether my slides fit"). ' +
30
- "Provides deterministic commands for presenting in a browser, validating Architecture DSL and " +
31
- "themes, inspecting 1280x720 clipping, capturing PNGs, and exporting PDF.";
27
+ "Turn Markdown into 16:9 slides with the MarkdStage CLI. Use when the user asks to create, " +
28
+ "refine, present, preview, validate, inspect, screenshot, or export a Markdown deck " +
29
+ '("present slides.md", "turn this file into slides", "export the deck to PDF or PowerPoint", ' +
30
+ '"check whether my slides fit"). Provides a deterministic create-review-deliver workflow, ' +
31
+ "browser-based Architecture DSL editing, theme validation, 1280x720 clipping diagnostics, " +
32
+ "targeted PNG capture, and PDF/PowerPoint export.";
32
33
 
33
34
  function frontMatter(fields) {
34
35
  const lines = ["---"];
@@ -66,26 +67,51 @@ what the MarkdStage canvas and MarkdStage Desktop render.
66
67
  - An installed Microsoft Edge, Google Chrome, or Chromium (never downloaded automatically).
67
68
  - The CLI: \`npx @markdstage/markdstage <command>\` or \`npm install --global @markdstage/markdstage\`.
68
69
 
69
- ## Workflow
70
-
71
- 1. Write or edit the deck as one Markdown file (see \`references/slide-format.md\`).
72
- 2. Validate it: \`markdstage validate slides.md --json\`.
73
- 3. Check the fixed 16:9 layout: \`markdstage inspect slides.md --json\`.
74
- 4. Capture clipped slides for review: \`markdstage capture slides.md\`.
75
- 5. Present or export: \`markdstage present slides.md --watch\` / \`markdstage export slides.md\`.
70
+ ## Recommended authoring workflow
71
+
72
+ 1. Establish the source material, audience, objective, approximate length, theme,
73
+ required diagrams, and output format.
74
+ 2. Read only the relevant guidance. Start with
75
+ \`markdstage guide slide-format\`, then retrieve \`themes\`,
76
+ \`custom-themes\`, or \`architecture-dsl\` when needed.
77
+ 3. Create the complete deck as one Markdown source file (see
78
+ \`references/slide-format.md\`).
79
+ 4. Validate structure, themes, and Architecture DSL before visual review:
80
+ \`markdstage validate slides.md --json\`.
81
+ 5. Use \`markdstage present slides.md --watch\` for live source-backed authoring.
82
+ It reloads on save without losing the current slide and keeps the last valid
83
+ deck while a save is incomplete.
84
+ 6. Check fixed 16:9 output with \`markdstage inspect slides.md --json\`. Use
85
+ \`--slide <n>\` after localized changes and \`--fail-on-issues\` in CI.
86
+ 7. Run \`markdstage capture slides.md\` only after inspection. Without
87
+ \`--pages\`, it captures only clipped slides; use \`--pages 2,4\` for pages
88
+ whose balance, spacing, or diagrams need visual judgment.
89
+ 8. Revise Markdown and repeat validation plus targeted inspection until the deck
90
+ is valid, unclipped, concise, and visually balanced.
91
+ 9. Deliver from the same source with \`markdstage presentation slides.md\`,
92
+ \`markdstage export slides.md --output slides.pdf\`, or
93
+ \`markdstage export slides.md --output slides.pptx\`.
94
+
95
+ The browser in \`present --watch\` starts in viewing mode. The user can activate
96
+ the pencil control to move Architecture elements, then choose **Advanced edit**
97
+ for the detailed designer. Placement changes save immediately, while the
98
+ detailed designer saves only when the user selects **Save**. \`present\` without
99
+ \`--watch\` is read-only.
76
100
 
77
101
  Never hand-write HTML or CSS for a slide. Fix layout problems by shortening the
78
- content or by changing the layout in front matter.
102
+ content or by changing the layout in front matter. Prefer structured validation
103
+ and layout diagnostics over capturing every slide.
79
104
 
80
105
  ## Commands
81
106
 
82
107
  | Command | Purpose |
83
108
  | --- | --- |
84
- | \`markdstage present <file> [--watch]\` | Serve the deck on loopback and open it in a browser window. \`--watch\` reloads on save and keeps the current slide. |
109
+ | \`markdstage presentation <file> [--watch]\` | Open presenter view with the current slide, next-slide preview, speaker notes, and controls for a synchronized audience window. |
110
+ | \`markdstage present <file> [--watch]\` | Serve the deck on loopback and open it in a browser window. \`--watch\` reloads on save, keeps the current slide, and enables Architecture placement and detailed editing. Without it, the source is read-only. |
85
111
  | \`markdstage validate <file> [--json]\` | Check deck structure, Architecture DSL blocks, and themes. |
86
- | \`markdstage inspect <file> [--json]\` | Report 1280x720 clipping diagnostics for the deck or one slide. |
112
+ | \`markdstage inspect <file> [--json]\` | Report 1280x720 clipping diagnostics for the deck or one slide; use \`--fail-on-issues\` for quality gates. |
87
113
  | \`markdstage capture <file> [--pages 2,4]\` | Write 1280x720 PNG files; without \`--pages\` only clipped slides are captured. |
88
- | \`markdstage export <file> [--output slides.pdf]\` | Produce the 16:9 PDF. |
114
+ | \`markdstage export <file> [--output slides.pdf|slides.pptx]\` | Produce a 16:9 PDF or hybrid editable PowerPoint. |
89
115
  | \`markdstage guide <topic>\` | Print the canonical MarkdStage authoring guide. |
90
116
 
91
117
  Exit codes: \`0\` success, \`1\` usage error, \`2\` deck or input error, \`3\` no