@markdstage/markdstage 3.3.0 → 3.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.
package/src/cli.mjs CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  exitCodeFor,
22
22
  } from "./exit.mjs";
23
23
  import { parsePageList } from "./deck.mjs";
24
- import { presentCommand } from "./commands/present.mjs";
24
+ import { applicationCommand } from "./commands/present.mjs";
25
25
  import { validateCommand, formatValidateReport } from "./commands/validate.mjs";
26
26
  import { inspectCommand, formatInspectReport } from "./commands/inspect.mjs";
27
27
  import { captureCommand, formatCaptureReport } from "./commands/capture.mjs";
@@ -36,8 +36,8 @@ import {
36
36
  const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
37
37
 
38
38
  const COMMANDS = [
39
- ["present", "Open presenter view and launch the audience view from it."],
40
- ["preview", "Serve a deck on loopback and open it in a browser window."],
39
+ ["present", "Open the MarkdStage UI in presenter view."],
40
+ ["preview", "Open the MarkdStage UI in slide view."],
41
41
  ["validate", "Check deck structure, Architecture DSL blocks, and themes."],
42
42
  ["inspect", "Report 1280x720 clipping diagnostics for a deck."],
43
43
  ["capture", "Write 1280x720 PNG files for selected or clipped slides."],
@@ -78,7 +78,14 @@ function usage(command) {
78
78
  const lines = [
79
79
  "MarkdStage — turn Markdown into 16:9 slides.",
80
80
  "",
81
- "Usage: markdstage <command> [options]",
81
+ "Usage:",
82
+ " markdstage [options]",
83
+ " markdstage <file.md> [options]",
84
+ " markdstage <command> [options]",
85
+ "",
86
+ "Application:",
87
+ " With no file, open an empty UI and choose Markdown from the workspace.",
88
+ " With a Markdown file, open it in live slide view and refresh it on save.",
82
89
  "",
83
90
  "Commands:",
84
91
  ];
@@ -91,6 +98,7 @@ function usage(command) {
91
98
  " --workspace <dir> Confine every read and write to this directory.",
92
99
  " --theme <name> Override the deck theme.",
93
100
  " --theme-file <path> Use a custom theme metadata file.",
101
+ " --no-open Serve the UI without launching a browser.",
94
102
  " --json Print machine-readable JSON.",
95
103
  " -h, --help Show help for a command.",
96
104
  " -v, --version Print the CLI version.",
@@ -106,10 +114,10 @@ function usage(command) {
106
114
  present: [
107
115
  "Usage: markdstage present <file.md> [options]",
108
116
  "",
109
- "Opens presenter view with the current slide, next-slide preview, and speaker notes.",
110
- "Use Start presentation in that view to open the synchronized audience window.",
117
+ "Opens the full MarkdStage UI in presenter view.",
118
+ "Open Markdown, automatic refresh, editing, export, and audience controls remain available.",
111
119
  "",
112
- " --watch Reload on save.",
120
+ " --watch Start with automatic refresh enabled.",
113
121
  " --no-open Serve the presenter view without launching a browser.",
114
122
  "",
115
123
  "Presentation requires an installed Microsoft Edge, Google Chrome, or Chromium.",
@@ -117,11 +125,13 @@ function usage(command) {
117
125
  preview: [
118
126
  "Usage: markdstage preview <file.md> [options]",
119
127
  "",
120
- " --watch Reload on save and enable Architecture editing.",
121
- " --no-open Serve the deck without launching a browser.",
128
+ "Opens the full MarkdStage UI in slide view.",
129
+ "",
130
+ " --watch Start with automatic refresh enabled.",
131
+ " --no-open Serve the UI without launching a browser.",
122
132
  "",
123
- "Without --watch, preview is read-only. Watch mode starts in normal viewing mode;",
124
- "use the pencil control to edit Architecture diagrams and open the detailed designer.",
133
+ "Preview starts on the fixed 16:9 output surface. Use Output preview to switch",
134
+ "to the responsive layout. Architecture editing and export remain available.",
125
135
  "",
126
136
  "Preview requires an installed Microsoft Edge, Google Chrome, or Chromium.",
127
137
  ],
@@ -201,6 +211,10 @@ function deckOptions(file, values) {
201
211
  };
202
212
  }
203
213
 
214
+ function isMarkdownArgument(value) {
215
+ return typeof value === "string" && /\.(?:md|markdown)$/i.test(value);
216
+ }
217
+
204
218
  export async function run(argv, io = {}) {
205
219
  const out = io.out ?? ((text) => console.log(text));
206
220
  const err = io.err ?? ((text) => console.error(text));
@@ -208,7 +222,7 @@ export async function run(argv, io = {}) {
208
222
 
209
223
  const command = argv[0];
210
224
  const rest = argv.slice(1);
211
- if (command === "--help" || command === "-h" || command === undefined) {
225
+ if (command === "--help" || command === "-h") {
212
226
  out(usage());
213
227
  return EXIT_OK;
214
228
  }
@@ -229,9 +243,63 @@ export async function run(argv, io = {}) {
229
243
  out(await packageVersion());
230
244
  return EXIT_OK;
231
245
  }
232
- if (command.startsWith("-")) {
233
- err(`Unknown option: ${command}\n\n${usage()}`);
234
- return EXIT_USAGE;
246
+ if (command === undefined || isMarkdownArgument(command) || command.startsWith("-")) {
247
+ let values;
248
+ let positionals;
249
+ try {
250
+ ({ values, positionals } = parseArgs({
251
+ args: argv,
252
+ options: {
253
+ ...GLOBAL_OPTIONS,
254
+ "no-open": { type: "boolean" },
255
+ },
256
+ allowPositionals: true,
257
+ }));
258
+ } catch (error) {
259
+ err(`${error.message}\n\n${usage()}`);
260
+ return EXIT_USAGE;
261
+ }
262
+ if (values.help) {
263
+ out(usage());
264
+ return EXIT_OK;
265
+ }
266
+ if (values.version) {
267
+ out(await packageVersion());
268
+ return EXIT_OK;
269
+ }
270
+ if (positionals.length > 1) {
271
+ err(`Application mode accepts at most one Markdown file.\n\n${usage()}`);
272
+ return EXIT_USAGE;
273
+ }
274
+ const file = positionals[0];
275
+ if (file && !isMarkdownArgument(file)) {
276
+ err(`Unknown command: ${file}\n\n${usage()}`);
277
+ return EXIT_USAGE;
278
+ }
279
+ try {
280
+ const report = await applicationCommand(
281
+ {
282
+ ...deckOptions(file, values),
283
+ live: Boolean(file),
284
+ open: io.open ?? !values["no-open"],
285
+ until: io.until,
286
+ },
287
+ {
288
+ print: values.json ? () => {} : (message) => out(message),
289
+ status: (message, isError) => {
290
+ if (isError) err(message);
291
+ else if (!values.json) out(message);
292
+ },
293
+ },
294
+ );
295
+ if (values.json) json(report);
296
+ return EXIT_OK;
297
+ } catch (error) {
298
+ const code = exitCodeFor(error);
299
+ if (values.json) json(errorPayload(error));
300
+ else err(error?.message || String(error));
301
+ return code || EXIT_FAILURE;
302
+ }
235
303
  }
236
304
  if (!COMMAND_OPTIONS[command]) {
237
305
  err(`Unknown command: ${command}\n\n${usage()}`);
@@ -264,8 +332,13 @@ export async function run(argv, io = {}) {
264
332
  switch (command) {
265
333
  case "preview": {
266
334
  const file = requireFile(positionals, "preview");
267
- const report = await presentCommand(
268
- { ...deckOptions(file, values), watch: values.watch, open: !values["no-open"], until: io.until },
335
+ const report = await applicationCommand(
336
+ {
337
+ ...deckOptions(file, values),
338
+ watch: values.watch,
339
+ open: io.open ?? !values["no-open"],
340
+ until: io.until,
341
+ },
269
342
  {
270
343
  print: values.json ? () => {} : (message) => out(message),
271
344
  status: (message, isError) => {
@@ -279,11 +352,11 @@ export async function run(argv, io = {}) {
279
352
  }
280
353
  case "present": {
281
354
  const file = requireFile(positionals, "present");
282
- const report = await presentCommand(
355
+ const report = await applicationCommand(
283
356
  {
284
357
  ...deckOptions(file, values),
285
358
  watch: values.watch,
286
- open: !values["no-open"],
359
+ open: io.open ?? !values["no-open"],
287
360
  presenterView: true,
288
361
  until: io.until,
289
362
  },
@@ -1,181 +1,171 @@
1
- // Shared server for the markdstage preview and present commands.
1
+ // Shared browser application for the top-level, preview, and present commands.
2
2
 
3
- import { mkdtemp, readFile, rm } from "node:fs/promises";
3
+ import { mkdtemp, 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";
7
- import { pathToFileURL } from "node:url";
8
7
  import {
9
8
  MarkdStageError,
10
9
  buildPresenterBrowserArgs,
11
10
  findChromiumBrowser,
12
11
  isProcessRunning,
13
- sharedPath,
14
12
  terminateProcessTree,
15
13
  } from "../runtime.mjs";
16
14
  import { withDeckServer } from "../deck.mjs";
17
15
 
18
- async function createWatcher(session, server, { onStatus }) {
19
- const { createMarkdownWatcher } = await import(
20
- pathToFileURL(sharedPath("scripts", "markdown-watcher.mjs")).href
21
- );
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({
29
- path: session.file,
30
- onChange: async () => {
31
- try {
32
- if ((await readFile(session.file, "utf8")) === session.sourceMarkdown) {
33
- setWatchState("watching");
34
- return;
35
- }
36
- // Keep the current slide, and keep the last valid deck when the file is
37
- // saved in a broken intermediate state.
38
- await session.load({ preserveIndex: true });
39
- setWatchState("watching");
40
- server.broadcast();
41
- onStatus(`reloaded ${session.sourceName} (${session.slides.length} slides)`);
42
- } catch (error) {
43
- setWatchState("error", error?.code || "source_reload_failed");
44
- onStatus(`reload failed, keeping the last valid deck: ${error?.message || error}`, true);
45
- }
46
- },
47
- onError: (error) => {
48
- setWatchState("error", "watch_failed");
49
- onStatus(`watch error: ${error?.message || error}`, true);
50
- },
51
- });
52
- setWatchState("watching");
53
- return watcher;
54
- }
55
-
56
- export async function presentCommand(options, io) {
16
+ export async function applicationCommand(options, io, dependencies = {}) {
17
+ const createTempDirectory = dependencies.mkdtemp ?? mkdtemp;
18
+ const findBrowser = dependencies.findChromiumBrowser ?? findChromiumBrowser;
19
+ const processIsRunning = dependencies.isProcessRunning ?? isProcessRunning;
20
+ const remove = dependencies.rm ?? rm;
21
+ const spawnBrowser = dependencies.spawn ?? spawn;
22
+ const terminate = dependencies.terminateProcessTree ?? terminateProcessTree;
57
23
  let audienceProcess = null;
58
24
  let audienceProfileDir = "";
59
25
  let audienceUrl = "";
26
+ let audienceOperation = Promise.resolve();
27
+
28
+ const runAudienceOperation = (operation) => {
29
+ const result = audienceOperation.then(operation, operation);
30
+ audienceOperation = result.catch(() => {});
31
+ return result;
32
+ };
60
33
 
61
- const closeAudience = async () => {
34
+ const closeAudienceNow = async () => {
62
35
  const process = audienceProcess;
36
+ const profileDir = audienceProfileDir;
63
37
  audienceProcess = null;
64
- if (isProcessRunning(process)) await terminateProcessTree(process);
65
- if (audienceProfileDir) {
66
- await rm(audienceProfileDir, { recursive: true, force: true }).catch(() => {});
67
- audienceProfileDir = "";
38
+ audienceProfileDir = "";
39
+ if (processIsRunning(process)) await terminate(process);
40
+ if (profileDir) {
41
+ await remove(profileDir, { recursive: true, force: true }).catch(() => {});
68
42
  }
69
43
  return { stopped: Boolean(process) };
70
44
  };
71
45
 
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,
46
+ const closeAudience = () => runAudienceOperation(closeAudienceNow);
47
+
48
+ const audience = {
49
+ isRunning: () => processIsRunning(audienceProcess),
50
+ open: () => runAudienceOperation(async () => {
51
+ if (processIsRunning(audienceProcess)) return { alreadyRunning: true };
52
+ await closeAudienceNow();
53
+ const browser = findBrowser();
54
+ if (!browser) {
55
+ throw new MarkdStageError(
56
+ "presenter_browser_not_found",
57
+ "Opening the audience view requires Microsoft Edge, Google Chrome, or Chromium.",
58
+ );
101
59
  }
102
- : null;
60
+ const profileDir = await createTempDirectory(
61
+ join(tmpdir(), "markdstage-audience-window-"),
62
+ );
63
+ const process = spawnBrowser(
64
+ browser,
65
+ buildPresenterBrowserArgs({
66
+ profileDir,
67
+ presenterUrl: audienceUrl,
68
+ }),
69
+ { windowsHide: false, stdio: "ignore" },
70
+ );
71
+ try {
72
+ await new Promise((ready, reject) => {
73
+ process.once("spawn", ready);
74
+ process.once("error", reject);
75
+ });
76
+ } catch (error) {
77
+ if (processIsRunning(process)) await terminate(process);
78
+ await remove(profileDir, { recursive: true, force: true }).catch(() => {});
79
+ throw error;
80
+ }
81
+ audienceProcess = process;
82
+ audienceProfileDir = profileDir;
83
+ return { alreadyRunning: false };
84
+ }),
85
+ close: closeAudience,
86
+ };
103
87
 
104
88
  try {
105
- return await withDeckServer({ ...options, presenter: audience }, async (session, server) => {
106
- if (audience) {
89
+ return await withDeckServer(
90
+ {
91
+ ...options,
92
+ application: true,
93
+ initialSourceMode: options.live || options.watch ? "live" : "snapshot",
94
+ presenter: audience,
95
+ },
96
+ async (session, server) => {
107
97
  const url = new URL(server.url);
108
98
  url.searchParams.set("present", "1");
109
99
  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;
116
100
 
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.",
101
+ let browserProcess = null;
102
+ let profileDir = "";
103
+ const browserUrl = new URL(server.url);
104
+ if (options.presenterView) browserUrl.searchParams.set("presenter", "1");
105
+ try {
106
+ if (options.open) {
107
+ const browser = findBrowser();
108
+ if (!browser) {
109
+ throw new MarkdStageError(
110
+ "presenter_browser_not_found",
111
+ "Opening MarkdStage requires Microsoft Edge, Google Chrome, or Chromium. Re-run with --no-open to serve the UI only.",
112
+ );
113
+ }
114
+ profileDir = await createTempDirectory(join(tmpdir(), "markdstage-app-window-"));
115
+ browserProcess = spawnBrowser(
116
+ browser,
117
+ buildPresenterBrowserArgs({ profileDir, presenterUrl: browserUrl.href }),
118
+ { windowsHide: false, stdio: "ignore" },
128
119
  );
120
+ await new Promise((ready, reject) => {
121
+ browserProcess.once("spawn", ready);
122
+ browserProcess.once("error", reject);
123
+ });
129
124
  }
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
125
 
142
- io.print(
143
- `MarkdStage is ${options.presenterView ? "presenting" : "previewing"} ${session.sourceName || session.file}`,
144
- );
145
- io.print(` slides: ${session.slides.length}`);
146
- io.print(` theme: ${session.theme}`);
147
- io.print(` workspace: ${resolve(session.workspaceRoot)}`);
148
- io.print(` url: ${browserUrl.href}`);
149
- if (options.watch) {
150
- io.print(" watching: on (live reload and Architecture editing are enabled)");
151
- }
152
- io.print("Press Ctrl+C to stop.");
126
+ const operation = options.presenterView
127
+ ? "presenting"
128
+ : session.file
129
+ ? "previewing"
130
+ : "ready in";
131
+ io.print(`MarkdStage is ${operation} ${session.sourceName || "the workspace"}`);
132
+ io.print(` slides: ${session.slides.length}`);
133
+ io.print(` theme: ${session.theme}`);
134
+ io.print(` workspace: ${resolve(session.workspaceRoot)}`);
135
+ io.print(` url: ${browserUrl.href}`);
136
+ if ((options.live || options.watch) && session.file) {
137
+ io.print(" watching: on (live reload is enabled)");
138
+ }
139
+ io.print("Press Ctrl+C to stop.");
153
140
 
154
- await new Promise((done) => {
155
- const stop = () => {
156
- process.off("SIGINT", stop);
157
- process.off("SIGTERM", stop);
158
- done();
159
- };
160
- process.once("SIGINT", stop);
161
- process.once("SIGTERM", stop);
162
- if (browserProcess) browserProcess.once("close", stop);
163
- if (options.until) options.until.then(stop, stop);
164
- });
141
+ await new Promise((done) => {
142
+ const stop = () => {
143
+ process.off("SIGINT", stop);
144
+ process.off("SIGTERM", stop);
145
+ done();
146
+ };
147
+ process.once("SIGINT", stop);
148
+ process.once("SIGTERM", stop);
149
+ if (browserProcess) browserProcess.once("close", stop);
150
+ if (options.until) options.until.then(stop, stop);
151
+ });
165
152
 
166
- return {
167
- ok: true,
168
- url: browserUrl.href,
169
- total: session.slides.length,
170
- theme: session.theme,
171
- };
172
- } finally {
173
- watcher?.close();
174
- await closeAudience();
175
- if (isProcessRunning(browserProcess)) await terminateProcessTree(browserProcess);
176
- if (profileDir) await rm(profileDir, { recursive: true, force: true }).catch(() => {});
177
- }
178
- });
153
+ return {
154
+ ok: true,
155
+ url: browserUrl.href,
156
+ total: session.slides.length,
157
+ theme: session.theme,
158
+ sourceMode: server.sourceMode,
159
+ };
160
+ } finally {
161
+ await closeAudience();
162
+ if (processIsRunning(browserProcess)) await terminate(browserProcess);
163
+ if (profileDir) {
164
+ await remove(profileDir, { recursive: true, force: true }).catch(() => {});
165
+ }
166
+ }
167
+ },
168
+ );
179
169
  } finally {
180
170
  await closeAudience();
181
171
  }
package/src/deck.mjs CHANGED
@@ -21,10 +21,14 @@ export async function withDeckServer(options, run) {
21
21
  log: options.log,
22
22
  });
23
23
  const server = await startPresentationServer(session, {
24
+ application: options.application,
25
+ exporters: options.exporters,
26
+ initialSourceMode: options.initialSourceMode,
24
27
  token,
25
28
  onLog: options.log,
26
29
  editable: options.watch === true,
27
30
  presenter: options.presenter,
31
+ watcherFactory: options.watcherFactory,
28
32
  });
29
33
  try {
30
34
  return await run(session, server);
package/src/skills.mjs CHANGED
@@ -82,7 +82,7 @@ what the MarkdStage canvas and MarkdStage Desktop render.
82
82
  \`markdstage validate slides.md --json\`. Review diagnostic codes, JSON Pointers,
83
83
  and completeness, fix independent issues together, and preserve the same
84
84
  validated content when presenting. Suggestions are never automatic repairs.
85
- 5. Use \`markdstage preview slides.md --watch\` for live source-backed authoring.
85
+ 5. Use \`markdstage slides.md\` for live source-backed authoring.
86
86
  It reloads on save without losing the current slide and keeps the last valid
87
87
  deck while a save is incomplete.
88
88
  6. Check fixed 16:9 output with \`markdstage inspect slides.md --json\`. Use
@@ -96,11 +96,13 @@ what the MarkdStage canvas and MarkdStage Desktop render.
96
96
  \`markdstage export slides.md --output slides.pdf\`, or
97
97
  \`markdstage export slides.md --output slides.pptx\`.
98
98
 
99
- The browser in \`preview --watch\` starts in viewing mode. The user can activate
100
- the pencil control to move Architecture elements, then choose **Advanced edit**
101
- for the detailed designer. Placement changes save immediately, while the
102
- detailed designer saves only when the user selects **Save**. \`preview\` without
103
- \`--watch\` is read-only.
99
+ The browser in \`markdstage slides.md\` starts in viewing mode on the fixed 16:9
100
+ output surface. **Output preview** switches to the retained responsive layout.
101
+ The user can activate the pencil control to switch to the responsive layout and
102
+ move Architecture elements, then choose **Advanced edit** for the detailed
103
+ designer. Placement changes save immediately, while the detailed designer saves
104
+ only when the user selects **Save**. Automatic refresh can be toggled from the
105
+ same UI without disabling Architecture editing.
104
106
 
105
107
  Never hand-write HTML or CSS for a slide. Fix layout problems by shortening the
106
108
  content or by changing the layout in front matter. Prefer structured validation
@@ -110,8 +112,10 @@ and layout diagnostics over capturing every slide.
110
112
 
111
113
  | Command | Purpose |
112
114
  | --- | --- |
113
- | \`markdstage present <file> [--watch]\` | Open presenter view with the current slide, next-slide preview, speaker notes, and controls for a synchronized audience window. |
114
- | \`markdstage preview <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. |
115
+ | \`markdstage\` | Open an empty Canvas-equivalent UI and choose Markdown from the workspace. |
116
+ | \`markdstage <file>\` | Open the full UI in live slide view with automatic refresh, editing, presenting, and UI export. |
117
+ | \`markdstage present <file> [--watch]\` | Open the same full UI in presenter view; \`--watch\` enables automatic refresh initially. |
118
+ | \`markdstage preview <file> [--watch]\` | Open the same full UI in slide view; \`--watch\` enables automatic refresh initially. |
115
119
  | \`markdstage validate <file> [--json]\` | Check deck structure, Architecture DSL blocks, and themes. |
116
120
  | \`markdstage inspect <file> [--json]\` | Report 1280x720 clipping diagnostics for the deck or one slide; use \`--fail-on-issues\` for quality gates. |
117
121
  | \`markdstage capture <file> [--pages 2,4]\` | Write 1280x720 PNG files; without \`--pages\` only clipped slides are captured. |