@markdstage/markdstage 0.1.1
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/README.md +90 -0
- package/bin/markdstage.mjs +12 -0
- package/package.json +45 -0
- package/shared/README.md +1014 -0
- package/shared/THIRD-PARTY-NOTICES.md +19 -0
- package/shared/deck-state.mjs +105 -0
- package/shared/docs/custom-theme-authoring.md +208 -0
- package/shared/markdown-deck.mjs +220 -0
- package/shared/markdstage-guide.mjs +276 -0
- package/shared/presenter-window.mjs +17 -0
- package/shared/renderer/architecture-document.mjs +596 -0
- package/shared/renderer/architecture-edit.mjs +298 -0
- package/shared/renderer/architecture-editor.mjs +449 -0
- package/shared/renderer/architecture.mjs +4033 -0
- package/shared/renderer/import-path.mjs +11 -0
- package/shared/renderer/index.html +106 -0
- package/shared/renderer/renderer.js +2082 -0
- package/shared/renderer/slides.css +614 -0
- package/shared/renderer/speaker-notes.mjs +106 -0
- package/shared/renderer/theme.mjs +205 -0
- package/shared/runtime/browser.mjs +539 -0
- package/shared/runtime/custom-theme.mjs +135 -0
- package/shared/runtime/deck-session.mjs +188 -0
- package/shared/runtime/errors.mjs +17 -0
- package/shared/runtime/output-paths.mjs +159 -0
- package/shared/runtime/output.mjs +385 -0
- package/shared/runtime/presentation-server.mjs +505 -0
- package/shared/runtime/static-files.mjs +70 -0
- package/shared/schema/README.md +228 -0
- package/shared/schema/architecture-v1.schema.json +664 -0
- package/shared/schema/examples/web-app.architecture.json +119 -0
- package/shared/schema/theme-metadata-v1.schema.json +75 -0
- package/shared/schema/theme-v1.json +84 -0
- package/shared/scripts/architecture-assets.mjs +226 -0
- package/shared/scripts/asset-paths.mjs +92 -0
- package/shared/scripts/atomic-markdown-replace.mjs +46 -0
- package/shared/scripts/markdown-blocks.mjs +182 -0
- package/shared/scripts/markdown-files.mjs +63 -0
- package/shared/scripts/markdown-save-coordinator.mjs +18 -0
- package/shared/scripts/markdown-watcher.mjs +80 -0
- package/shared/scripts/theme-paths.mjs +108 -0
- package/shared/scripts/vendor-assets.mjs +132 -0
- package/shared/scripts/workspace-root.mjs +32 -0
- package/shared/vendor/highlight.LICENSE +29 -0
- package/shared/vendor/highlight.min.js +1244 -0
- package/shared/vendor/marked.min.js +6 -0
- package/shared/vendor/mermaid.min.js.part-0001 +268 -0
- package/shared/vendor/mermaid.min.js.part-0002 +304 -0
- package/shared/vendor/mermaid.min.js.part-0003 +324 -0
- package/shared/vendor/mermaid.min.js.part-0004 +374 -0
- package/shared/vendor/mermaid.min.js.part-0005 +564 -0
- package/shared/vendor/mermaid.min.js.part-0006 +1308 -0
- package/shared/vendor/mermaid.min.js.part-0007 +269 -0
- package/shared/vendor/purify.min.js +3 -0
- package/shared/vendor/vendor-assets.lock.json +60 -0
- package/src/cli.mjs +347 -0
- package/src/commands/capture.mjs +23 -0
- package/src/commands/export.mjs +18 -0
- package/src/commands/guide.mjs +23 -0
- package/src/commands/inspect.mjs +35 -0
- package/src/commands/present.mjs +91 -0
- package/src/commands/skill.mjs +114 -0
- package/src/commands/validate.mjs +79 -0
- package/src/deck.mjs +63 -0
- package/src/exit.mjs +58 -0
- package/src/runtime.mjs +77 -0
- package/src/skills.mjs +155 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// markdstage skill — install or check portable Agent Skills.
|
|
2
|
+
|
|
3
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { UsageError } from "../exit.mjs";
|
|
7
|
+
import { SKILL_TARGETS, buildSkillFiles } from "../skills.mjs";
|
|
8
|
+
|
|
9
|
+
export const SKILL_TARGET_NAMES = Object.keys(SKILL_TARGETS);
|
|
10
|
+
|
|
11
|
+
function resolveTargets(target) {
|
|
12
|
+
if (!target || target === "all") return SKILL_TARGET_NAMES;
|
|
13
|
+
const names = target
|
|
14
|
+
.split(",")
|
|
15
|
+
.map((name) => name.trim())
|
|
16
|
+
.filter(Boolean);
|
|
17
|
+
for (const name of names) {
|
|
18
|
+
if (!SKILL_TARGETS[name]) {
|
|
19
|
+
throw new UsageError(
|
|
20
|
+
`Unknown skill target: ${name}. Available targets: ${SKILL_TARGET_NAMES.join(", ")}, all.`,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return names;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function skillDirectory(root, target) {
|
|
28
|
+
return path.join(root, ...SKILL_TARGETS[target].directory);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function readIfExists(file) {
|
|
32
|
+
try {
|
|
33
|
+
return await readFile(file, "utf8");
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error?.code === "ENOENT") return null;
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Install or verify generated skill files.
|
|
42
|
+
*
|
|
43
|
+
* `check` reports drift without writing. Without `force`, files that already
|
|
44
|
+
* exist with different contents are reported as `conflict` and left untouched.
|
|
45
|
+
*/
|
|
46
|
+
export async function skillCommand({
|
|
47
|
+
action = "install",
|
|
48
|
+
target,
|
|
49
|
+
root = process.cwd(),
|
|
50
|
+
force = false,
|
|
51
|
+
} = {}) {
|
|
52
|
+
if (action !== "install" && action !== "check") {
|
|
53
|
+
throw new UsageError(
|
|
54
|
+
`Unknown skill action: ${action}. Available actions: install, check.`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
const targets = resolveTargets(target);
|
|
58
|
+
const results = [];
|
|
59
|
+
let conflicts = 0;
|
|
60
|
+
let changed = 0;
|
|
61
|
+
|
|
62
|
+
for (const name of targets) {
|
|
63
|
+
const directory = skillDirectory(root, name);
|
|
64
|
+
const files = await buildSkillFiles(name);
|
|
65
|
+
for (const [relative, contents] of files) {
|
|
66
|
+
const file = path.join(directory, ...relative.split("/"));
|
|
67
|
+
const existing = await readIfExists(file);
|
|
68
|
+
let status;
|
|
69
|
+
if (existing === contents) {
|
|
70
|
+
status = "unchanged";
|
|
71
|
+
} else if (existing === null) {
|
|
72
|
+
status = "created";
|
|
73
|
+
} else if (force || action === "check") {
|
|
74
|
+
status = "updated";
|
|
75
|
+
} else {
|
|
76
|
+
status = "conflict";
|
|
77
|
+
}
|
|
78
|
+
if (status === "conflict") conflicts += 1;
|
|
79
|
+
if (status === "created" || status === "updated") changed += 1;
|
|
80
|
+
if (action === "install" && (status === "created" || status === "updated")) {
|
|
81
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
82
|
+
await writeFile(file, contents, "utf8");
|
|
83
|
+
}
|
|
84
|
+
results.push({ target: name, path: file, status });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { action, targets, files: results, changed, conflicts };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function formatSkillReport(report) {
|
|
92
|
+
const lines = [];
|
|
93
|
+
for (const file of report.files) {
|
|
94
|
+
if (file.status === "unchanged") continue;
|
|
95
|
+
lines.push(` ${file.status.padEnd(9)} ${file.path}`);
|
|
96
|
+
}
|
|
97
|
+
if (report.action === "check") {
|
|
98
|
+
lines.unshift(
|
|
99
|
+
report.changed || report.conflicts
|
|
100
|
+
? `Generated skills are out of date (${report.changed} file(s) differ).`
|
|
101
|
+
: "Generated skills are up to date.",
|
|
102
|
+
);
|
|
103
|
+
return lines.join("\n");
|
|
104
|
+
}
|
|
105
|
+
lines.unshift(
|
|
106
|
+
`Installed skills for ${report.targets.join(", ")} (${report.changed} file(s) written).`,
|
|
107
|
+
);
|
|
108
|
+
if (report.conflicts) {
|
|
109
|
+
lines.push(
|
|
110
|
+
`${report.conflicts} file(s) were modified locally and were left untouched. Re-run with --force to overwrite.`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return lines.join("\n");
|
|
114
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// markdstage validate — check deck structure, Architecture DSL, and themes.
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
MarkdStageError,
|
|
5
|
+
architectureValidationErrors,
|
|
6
|
+
createDeckSession,
|
|
7
|
+
createUrlToken,
|
|
8
|
+
hasFrontMatter,
|
|
9
|
+
} from "../runtime.mjs";
|
|
10
|
+
|
|
11
|
+
export async function validateCommand(options) {
|
|
12
|
+
const errors = [];
|
|
13
|
+
const warnings = [];
|
|
14
|
+
let session = null;
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
session = await createDeckSession({
|
|
18
|
+
file: options.file,
|
|
19
|
+
workspaceRoot: options.workspace,
|
|
20
|
+
theme: options.theme,
|
|
21
|
+
themeFile: options.themeFile,
|
|
22
|
+
assetUrlPrefix: `/${createUrlToken()}/theme-assets/`,
|
|
23
|
+
});
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if (!(error instanceof MarkdStageError)) throw error;
|
|
26
|
+
errors.push({
|
|
27
|
+
code: error.code,
|
|
28
|
+
message: error.message,
|
|
29
|
+
});
|
|
30
|
+
return { ok: false, file: options.file, total: 0, errors, warnings };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const issue of architectureValidationErrors(session.slides)) {
|
|
34
|
+
errors.push({
|
|
35
|
+
code: issue.code,
|
|
36
|
+
page: issue.page,
|
|
37
|
+
architecture: issue.architecture,
|
|
38
|
+
message: issue.message,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
session.slides.forEach((slide, index) => {
|
|
43
|
+
if (!hasFrontMatter(slide)) {
|
|
44
|
+
warnings.push({
|
|
45
|
+
code: "missing_front_matter",
|
|
46
|
+
page: index + 1,
|
|
47
|
+
message:
|
|
48
|
+
"Front matter is missing. Add the deck/layout/page/total/size fields to the leading --- block.",
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
ok: errors.length === 0,
|
|
55
|
+
file: session.file,
|
|
56
|
+
workspace: session.workspaceRoot,
|
|
57
|
+
total: session.slides.length,
|
|
58
|
+
theme: session.theme,
|
|
59
|
+
themeFile: session.customThemeFile || undefined,
|
|
60
|
+
errors,
|
|
61
|
+
warnings,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function formatValidateReport(report) {
|
|
66
|
+
const lines = [];
|
|
67
|
+
lines.push(`${report.file}`);
|
|
68
|
+
if (report.total) lines.push(` slides: ${report.total}${report.theme ? `, theme: ${report.theme}` : ""}`);
|
|
69
|
+
for (const error of report.errors) {
|
|
70
|
+
lines.push(
|
|
71
|
+
` error ${error.page ? `slide ${error.page}: ` : ""}${error.message} (${error.code})`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
for (const warning of report.warnings) {
|
|
75
|
+
lines.push(` warn slide ${warning.page}: ${warning.message}`);
|
|
76
|
+
}
|
|
77
|
+
lines.push(report.ok ? " OK: the deck is valid." : ` ${report.errors.length} error(s) found.`);
|
|
78
|
+
return lines.join("\n");
|
|
79
|
+
}
|
package/src/deck.mjs
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Deck lifecycle helpers shared by the CLI commands.
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createDeckSession,
|
|
5
|
+
createUrlToken,
|
|
6
|
+
startPresentationServer,
|
|
7
|
+
} from "./runtime.mjs";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Open a deck, start its loopback presentation server, and hand both to `run`.
|
|
11
|
+
* The server is always closed again, including on failure.
|
|
12
|
+
*/
|
|
13
|
+
export async function withDeckServer(options, run) {
|
|
14
|
+
const token = createUrlToken();
|
|
15
|
+
const session = await createDeckSession({
|
|
16
|
+
file: options.file,
|
|
17
|
+
workspaceRoot: options.workspace,
|
|
18
|
+
theme: options.theme,
|
|
19
|
+
themeFile: options.themeFile,
|
|
20
|
+
assetUrlPrefix: `/${token}/theme-assets/`,
|
|
21
|
+
log: options.log,
|
|
22
|
+
});
|
|
23
|
+
const server = await startPresentationServer(session, {
|
|
24
|
+
token,
|
|
25
|
+
onLog: options.log,
|
|
26
|
+
});
|
|
27
|
+
try {
|
|
28
|
+
return await run(session, server);
|
|
29
|
+
} finally {
|
|
30
|
+
await server.close();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function parsePageList(value, { total } = {}) {
|
|
35
|
+
const entries = String(value)
|
|
36
|
+
.split(",")
|
|
37
|
+
.map((entry) => entry.trim())
|
|
38
|
+
.filter(Boolean);
|
|
39
|
+
const indexes = [];
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
const range = entry.match(/^(\d+)\s*-\s*(\d+)$/);
|
|
42
|
+
if (range) {
|
|
43
|
+
const from = Number.parseInt(range[1], 10);
|
|
44
|
+
const to = Number.parseInt(range[2], 10);
|
|
45
|
+
if (!Number.isInteger(from) || !Number.isInteger(to) || from < 1 || to < from) {
|
|
46
|
+
throw new RangeError(`Invalid page range: ${entry}`);
|
|
47
|
+
}
|
|
48
|
+
for (let page = from; page <= to; page += 1) indexes.push(page - 1);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const page = Number.parseInt(entry, 10);
|
|
52
|
+
if (!Number.isInteger(page) || page < 1 || String(page) !== entry) {
|
|
53
|
+
throw new RangeError(`Invalid page number: ${entry}`);
|
|
54
|
+
}
|
|
55
|
+
indexes.push(page - 1);
|
|
56
|
+
}
|
|
57
|
+
const unique = [...new Set(indexes)].sort((a, b) => a - b);
|
|
58
|
+
if (!unique.length) throw new RangeError("At least one page is required.");
|
|
59
|
+
if (total !== undefined && unique.some((index) => index >= total)) {
|
|
60
|
+
throw new RangeError(`Pages must be between 1 and ${total}.`);
|
|
61
|
+
}
|
|
62
|
+
return unique;
|
|
63
|
+
}
|
package/src/exit.mjs
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Stable exit codes and error formatting for the MarkdStage CLI.
|
|
2
|
+
|
|
3
|
+
export const EXIT_OK = 0;
|
|
4
|
+
export const EXIT_USAGE = 1;
|
|
5
|
+
export const EXIT_DECK = 2;
|
|
6
|
+
export const EXIT_ENVIRONMENT = 3;
|
|
7
|
+
export const EXIT_FAILURE = 4;
|
|
8
|
+
export const EXIT_ISSUES = 5;
|
|
9
|
+
|
|
10
|
+
export const EXIT_CODES = {
|
|
11
|
+
0: "success",
|
|
12
|
+
1: "usage error",
|
|
13
|
+
2: "deck or input error",
|
|
14
|
+
3: "environment error (no Chromium-based browser)",
|
|
15
|
+
4: "rendering or output failure",
|
|
16
|
+
5: "layout or validation issues were found",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const DECK_ERROR_CODES = new Set([
|
|
20
|
+
"empty_markdown",
|
|
21
|
+
"file_not_found",
|
|
22
|
+
"file_too_large",
|
|
23
|
+
"invalid_input",
|
|
24
|
+
"invalid_markdown_path",
|
|
25
|
+
"invalid_output_path",
|
|
26
|
+
"invalid_theme_file",
|
|
27
|
+
"no_deck",
|
|
28
|
+
"path_outside_workspace",
|
|
29
|
+
"slide_out_of_range",
|
|
30
|
+
"theme_file_not_found",
|
|
31
|
+
"too_many_slides",
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
export class UsageError extends Error {
|
|
35
|
+
constructor(message) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = "UsageError";
|
|
38
|
+
this.code = "usage_error";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function exitCodeFor(error) {
|
|
43
|
+
if (error instanceof UsageError) return EXIT_USAGE;
|
|
44
|
+
const code = error?.code;
|
|
45
|
+
if (typeof code === "string") {
|
|
46
|
+
if (code.endsWith("browser_not_found")) return EXIT_ENVIRONMENT;
|
|
47
|
+
if (DECK_ERROR_CODES.has(code)) return EXIT_DECK;
|
|
48
|
+
}
|
|
49
|
+
return EXIT_FAILURE;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function errorPayload(error) {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
error: typeof error?.code === "string" ? error.code : "unexpected_error",
|
|
56
|
+
message: error?.message || String(error),
|
|
57
|
+
};
|
|
58
|
+
}
|
package/src/runtime.mjs
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Resolve and load the shared MarkdStage runtime.
|
|
2
|
+
//
|
|
3
|
+
// Published packages carry the runtime in `shared/` (populated by
|
|
4
|
+
// scripts/sync-shared.mjs at pack time). In a repository checkout the CLI falls
|
|
5
|
+
// back to the canonical Extension folder so both paths run the very same
|
|
6
|
+
// parser, renderer, validation, and output implementations.
|
|
7
|
+
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { dirname, join, resolve } from "node:path";
|
|
10
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
11
|
+
|
|
12
|
+
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
13
|
+
const BUNDLED_ROOT = join(PACKAGE_ROOT, "shared");
|
|
14
|
+
const CHECKOUT_ROOT = resolve(
|
|
15
|
+
PACKAGE_ROOT,
|
|
16
|
+
"..",
|
|
17
|
+
"..",
|
|
18
|
+
".github",
|
|
19
|
+
"extensions",
|
|
20
|
+
"markdstage",
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
export const SHARED_ROOT = existsSync(join(BUNDLED_ROOT, "markdown-deck.mjs"))
|
|
24
|
+
? BUNDLED_ROOT
|
|
25
|
+
: CHECKOUT_ROOT;
|
|
26
|
+
|
|
27
|
+
if (!existsSync(join(SHARED_ROOT, "markdown-deck.mjs"))) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
"The MarkdStage shared runtime is missing. Reinstall the package or run `npm run sync`.",
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function sharedPath(...parts) {
|
|
34
|
+
return join(SHARED_ROOT, ...parts);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function load(relativePath) {
|
|
38
|
+
return import(pathToFileURL(sharedPath(relativePath)).href);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const [
|
|
42
|
+
errors,
|
|
43
|
+
deckSession,
|
|
44
|
+
presentationServer,
|
|
45
|
+
output,
|
|
46
|
+
browser,
|
|
47
|
+
outputPaths,
|
|
48
|
+
guide,
|
|
49
|
+
presenterWindow,
|
|
50
|
+
markdownFiles,
|
|
51
|
+
] = await Promise.all([
|
|
52
|
+
load("runtime/errors.mjs"),
|
|
53
|
+
load("runtime/deck-session.mjs"),
|
|
54
|
+
load("runtime/presentation-server.mjs"),
|
|
55
|
+
load("runtime/output.mjs"),
|
|
56
|
+
load("runtime/browser.mjs"),
|
|
57
|
+
load("runtime/output-paths.mjs"),
|
|
58
|
+
load("markdstage-guide.mjs"),
|
|
59
|
+
load("presenter-window.mjs"),
|
|
60
|
+
load("scripts/markdown-files.mjs"),
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
export const { MarkdStageError } = errors;
|
|
64
|
+
export const { createDeckSession, readDeckSlides, resolveDeckFile, resolveDeckTheme } =
|
|
65
|
+
deckSession;
|
|
66
|
+
export const { createUrlToken, startPresentationServer } = presentationServer;
|
|
67
|
+
export const { captureSlides, exportPdf, inspectLayout, MAX_CAPTURE_SLIDES } = output;
|
|
68
|
+
export const { findChromiumBrowser, terminateProcessTree, isProcessRunning } = browser;
|
|
69
|
+
export const { captureDirectoryName, pdfNameForSource } = outputPaths;
|
|
70
|
+
export const {
|
|
71
|
+
architectureValidationErrors,
|
|
72
|
+
deckValidationFeedback,
|
|
73
|
+
hasFrontMatter,
|
|
74
|
+
readGuide,
|
|
75
|
+
} = guide;
|
|
76
|
+
export const { buildPresenterBrowserArgs } = presenterWindow;
|
|
77
|
+
export const { isMarkdownPath, MARKDOWN_MAX_BYTES } = markdownFiles;
|
package/src/skills.mjs
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Portable Agent Skill generation.
|
|
2
|
+
//
|
|
3
|
+
// Every reference file is generated from readGuide(topic) so the skills cannot
|
|
4
|
+
// drift from the canonical MarkdStage documentation. The generated tree follows
|
|
5
|
+
// the Agent Skills specification: a SKILL.md with YAML front matter (name +
|
|
6
|
+
// description) plus progressive-disclosure reference files.
|
|
7
|
+
|
|
8
|
+
import { GUIDE_TOPICS } from "./commands/guide.mjs";
|
|
9
|
+
import { readGuide } from "./runtime.mjs";
|
|
10
|
+
|
|
11
|
+
export const SKILL_TARGETS = {
|
|
12
|
+
codex: {
|
|
13
|
+
label: "Codex",
|
|
14
|
+
directory: [".agents", "skills", "markdstage"],
|
|
15
|
+
},
|
|
16
|
+
claude: {
|
|
17
|
+
label: "Claude Code",
|
|
18
|
+
directory: [".claude", "skills", "markdstage"],
|
|
19
|
+
},
|
|
20
|
+
copilot: {
|
|
21
|
+
label: "GitHub Copilot",
|
|
22
|
+
directory: [".github", "skills", "markdstage"],
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
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.";
|
|
32
|
+
|
|
33
|
+
function frontMatter(fields) {
|
|
34
|
+
const lines = ["---"];
|
|
35
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
36
|
+
lines.push(`${key}: ${JSON.stringify(value)}`);
|
|
37
|
+
}
|
|
38
|
+
lines.push("---");
|
|
39
|
+
return lines.join("\n");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function canvasNote(target) {
|
|
43
|
+
if (target !== "copilot") return "";
|
|
44
|
+
return [
|
|
45
|
+
"",
|
|
46
|
+
"## Canvas adapter",
|
|
47
|
+
"",
|
|
48
|
+
"Inside GitHub Copilot with the MarkdStage canvas Extension installed, prefer the",
|
|
49
|
+
"canvas: pass every slide to the `open` input of canvas ID `MarkdStage` and let the",
|
|
50
|
+
"canvas controls handle navigation. Use the CLI commands below when the canvas is not",
|
|
51
|
+
"available, or for validation, PNG capture, and PDF export in a terminal or CI job.",
|
|
52
|
+
].join("\n");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function skillBody(target) {
|
|
56
|
+
const label = SKILL_TARGETS[target].label;
|
|
57
|
+
return `# MarkdStage
|
|
58
|
+
|
|
59
|
+
Markdown is the single source of truth. MarkdStage renders each Markdown fragment
|
|
60
|
+
between \`---\` separators as one 1280x720 (16:9) slide, and the CLI renders exactly
|
|
61
|
+
what the MarkdStage canvas and MarkdStage Desktop render.
|
|
62
|
+
|
|
63
|
+
## Requirements
|
|
64
|
+
|
|
65
|
+
- Node.js 24 or later.
|
|
66
|
+
- An installed Microsoft Edge, Google Chrome, or Chromium (never downloaded automatically).
|
|
67
|
+
- The CLI: \`npx @markdstage/markdstage <command>\` or \`npm install --global @markdstage/markdstage\`.
|
|
68
|
+
|
|
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\`.
|
|
76
|
+
|
|
77
|
+
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.
|
|
79
|
+
|
|
80
|
+
## Commands
|
|
81
|
+
|
|
82
|
+
| Command | Purpose |
|
|
83
|
+
| --- | --- |
|
|
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. |
|
|
85
|
+
| \`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. |
|
|
87
|
+
| \`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. |
|
|
89
|
+
| \`markdstage guide <topic>\` | Print the canonical MarkdStage authoring guide. |
|
|
90
|
+
|
|
91
|
+
Exit codes: \`0\` success, \`1\` usage error, \`2\` deck or input error, \`3\` no
|
|
92
|
+
Chromium-based browser, \`4\` rendering failure, \`5\` issues found with \`--fail-on-issues\`.
|
|
93
|
+
|
|
94
|
+
## References
|
|
95
|
+
|
|
96
|
+
Read the reference that matches the task before writing Markdown:
|
|
97
|
+
|
|
98
|
+
- \`references/slide-format.md\` — slide fragments, front matter, layouts.
|
|
99
|
+
- \`references/themes.md\` — built-in themes.
|
|
100
|
+
- \`references/custom-themes.md\` — custom theme authoring and \`theme-file\`.
|
|
101
|
+
- \`references/theme-schema.md\` — custom theme properties.
|
|
102
|
+
- \`references/architecture-dsl.md\` — Architecture DSL v1 diagrams.
|
|
103
|
+
- \`references/architecture-schema.md\` — Architecture DSL schema summary.
|
|
104
|
+
- \`references/overview.md\` — how MarkdStage works.
|
|
105
|
+
|
|
106
|
+
## Notes for ${label}
|
|
107
|
+
|
|
108
|
+
Run the CLI through the shell. Presentation servers bind to loopback with an
|
|
109
|
+
unguessable per-process URL token, and every generated file stays inside the
|
|
110
|
+
workspace.${canvasNote(target)}
|
|
111
|
+
`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const REFERENCE_HEADERS = {
|
|
115
|
+
overview: "MarkdStage overview",
|
|
116
|
+
"slide-format": "Slide fragment format",
|
|
117
|
+
themes: "Built-in themes",
|
|
118
|
+
"custom-themes": "Custom theme authoring",
|
|
119
|
+
"theme-schema": "Custom theme schema",
|
|
120
|
+
"architecture-dsl": "Architecture DSL v1",
|
|
121
|
+
"architecture-schema": "Architecture DSL schema summary",
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Build every file of the generated skill as a `path -> contents` map.
|
|
126
|
+
* Paths use POSIX separators and are relative to the skill directory.
|
|
127
|
+
*/
|
|
128
|
+
export async function buildSkillFiles(target) {
|
|
129
|
+
if (!SKILL_TARGETS[target]) {
|
|
130
|
+
throw new Error(`Unknown skill target: ${target}`);
|
|
131
|
+
}
|
|
132
|
+
const files = new Map();
|
|
133
|
+
files.set(
|
|
134
|
+
"SKILL.md",
|
|
135
|
+
`${frontMatter({
|
|
136
|
+
name: "markdstage",
|
|
137
|
+
description: DESCRIPTION,
|
|
138
|
+
license: "MIT",
|
|
139
|
+
})}\n\n${skillBody(target)}`,
|
|
140
|
+
);
|
|
141
|
+
for (const topic of GUIDE_TOPICS) {
|
|
142
|
+
const content = (await readGuide(topic)).trim();
|
|
143
|
+
const parts = [
|
|
144
|
+
"<!-- Generated by `markdstage skill install`. Do not edit by hand. -->",
|
|
145
|
+
`<!-- Source: markdstage_guide topic "${topic}" -->`,
|
|
146
|
+
"",
|
|
147
|
+
];
|
|
148
|
+
if (!content.startsWith("#")) {
|
|
149
|
+
parts.push(`# ${REFERENCE_HEADERS[topic]}`, "");
|
|
150
|
+
}
|
|
151
|
+
parts.push(content, "");
|
|
152
|
+
files.set(`references/${topic}.md`, parts.join("\n"));
|
|
153
|
+
}
|
|
154
|
+
return files;
|
|
155
|
+
}
|