mdx-artifacts 0.1.2 → 0.2.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/README.md +211 -59
- package/README.zh-CN.md +299 -42
- package/agents/AGENTS.snippet.md +21 -10
- package/artifact-docs/examples/commentable-feedback.mdx +76 -70
- package/artifact-docs/examples/decision-matrix.mdx +122 -50
- package/artifact-docs/examples/layout-composition.mdx +106 -128
- package/artifact-docs/examples/streamlit-style-mixed.mdx +100 -85
- package/dist/lib/cli/{build.js → commands/build.js} +2 -2
- package/dist/lib/cli/{components.js → commands/components.js} +19 -3
- package/dist/lib/cli/{dev.js → commands/dev.js} +2 -2
- package/dist/lib/cli/commands/interactions.d.ts +2 -0
- package/dist/lib/cli/commands/interactions.js +280 -0
- package/dist/lib/cli/commands/review.d.ts +1 -0
- package/dist/lib/cli/commands/review.js +171 -0
- package/dist/lib/cli/commands/scaffold.d.ts +16 -0
- package/dist/lib/cli/commands/scaffold.js +440 -0
- package/dist/lib/cli/commands/validate.d.ts +18 -0
- package/dist/lib/cli/commands/validate.js +311 -0
- package/dist/lib/cli/config/config.d.ts +2 -0
- package/dist/lib/cli/{config.js → config/config.js} +3 -2
- package/dist/lib/cli/{types.d.ts → config/types.d.ts} +2 -1
- package/dist/lib/cli/{vite-artifact.d.ts → dev-server/vite-artifact.d.ts} +3 -3
- package/dist/lib/cli/{vite-artifact.js → dev-server/vite-artifact.js} +170 -10
- package/dist/lib/cli/diagnostics/diagnostics.d.ts +11 -0
- package/dist/lib/cli/diagnostics/diagnostics.js +1 -0
- package/dist/lib/cli/index.js +39 -18
- package/dist/lib/cli/mdx/sortable-list.d.ts +14 -0
- package/dist/lib/cli/mdx/sortable-list.js +520 -0
- package/dist/lib/cli/resources/resource-policy.d.ts +15 -0
- package/dist/lib/cli/resources/resource-policy.js +46 -0
- package/dist/lib/cli/resources/safe-path.d.ts +13 -0
- package/dist/lib/cli/resources/safe-path.js +55 -0
- package/dist/lib/cli/services/interaction-service.d.ts +40 -0
- package/dist/lib/cli/services/interaction-service.js +226 -0
- package/dist/lib/cli/services/review.d.ts +43 -0
- package/dist/lib/cli/{review.js → services/review.js} +34 -172
- package/dist/lib/react/{components → composites/comparison-set}/ComparisonSet.d.ts +1 -1
- package/dist/lib/react/{components → composites/comparison-set}/ComparisonSet.js +3 -3
- package/dist/lib/react/composites/comparison-set/index.d.ts +2 -0
- package/dist/lib/react/composites/comparison-set/index.js +1 -0
- package/dist/lib/react/composites/content-set/ContentItem.d.ts +37 -0
- package/dist/lib/react/composites/content-set/ContentItem.js +49 -0
- package/dist/lib/react/composites/content-set/index.d.ts +2 -0
- package/dist/lib/react/composites/content-set/index.js +1 -0
- package/dist/lib/react/{components → composites/export-panel}/ExportPanel.js +2 -2
- package/dist/lib/react/composites/export-panel/index.d.ts +2 -0
- package/dist/lib/react/composites/export-panel/index.js +1 -0
- package/dist/lib/react/{components → composites/section}/Section.js +1 -1
- package/dist/lib/react/composites/section/index.d.ts +2 -0
- package/dist/lib/react/composites/section/index.js +1 -0
- package/dist/lib/react/index.d.ts +36 -31
- package/dist/lib/react/index.js +18 -15
- package/dist/lib/react/interactions/artifact-state/index.d.ts +1 -0
- package/dist/lib/react/interactions/artifact-state/index.js +1 -0
- package/dist/lib/react/{components → interactions/comments}/Comments.d.ts +2 -2
- package/dist/lib/react/{components → interactions/comments}/Comments.js +3 -3
- package/dist/lib/react/interactions/comments/index.d.ts +1 -0
- package/dist/lib/react/interactions/comments/index.js +1 -0
- package/dist/lib/react/interactions/sortable-list/SortableList.d.ts +29 -0
- package/dist/lib/react/interactions/sortable-list/SortableList.js +282 -0
- package/dist/lib/react/interactions/sortable-list/index.d.ts +1 -0
- package/dist/lib/react/interactions/sortable-list/index.js +1 -0
- package/dist/lib/react/layout/layout-primitives/index.d.ts +2 -0
- package/dist/lib/react/layout/layout-primitives/index.js +1 -0
- package/dist/lib/react/legacy/LegacyContentComponents.d.ts +65 -0
- package/dist/lib/react/legacy/LegacyContentComponents.js +26 -0
- package/dist/lib/react/mdx-components.d.ts +5 -0
- package/dist/lib/react/mdx-components.js +38 -0
- package/dist/lib/react/{components → primitives/annotated-code}/AnnotatedCode.d.ts +1 -1
- package/dist/lib/react/{components → primitives/annotated-code}/AnnotatedCode.js +5 -5
- package/dist/lib/react/primitives/annotated-code/index.d.ts +2 -0
- package/dist/lib/react/primitives/annotated-code/index.js +1 -0
- package/dist/lib/react/primitives/callout/Callout.d.ts +11 -0
- package/dist/lib/react/{components → primitives/callout}/Callout.js +9 -6
- package/dist/lib/react/primitives/callout/index.d.ts +2 -0
- package/dist/lib/react/primitives/callout/index.js +1 -0
- package/dist/lib/react/primitives/code-block/CodeBlock.d.ts +20 -0
- package/dist/lib/react/primitives/code-block/CodeBlock.js +32 -0
- package/dist/lib/react/primitives/code-block/index.d.ts +2 -0
- package/dist/lib/react/primitives/code-block/index.js +1 -0
- package/dist/lib/react/primitives/code-surface/CodeSurface.d.ts +11 -0
- package/dist/lib/react/primitives/code-surface/CodeSurface.js +34 -0
- package/dist/lib/react/primitives/code-surface/index.d.ts +2 -0
- package/dist/lib/react/primitives/code-surface/index.js +1 -0
- package/dist/lib/react/primitives/diff-block/DiffBlock.js +25 -0
- package/dist/lib/react/primitives/diff-block/index.d.ts +2 -0
- package/dist/lib/react/primitives/diff-block/index.js +1 -0
- package/dist/lib/react/{components → primitives/inline-text}/InlineText.d.ts +4 -2
- package/dist/lib/react/primitives/inline-text/InlineText.js +28 -0
- package/dist/lib/react/primitives/inline-text/index.d.ts +2 -0
- package/dist/lib/react/primitives/inline-text/index.js +1 -0
- package/dist/lib/react/primitives/markdown-body/MarkdownBody.d.ts +9 -0
- package/dist/lib/react/primitives/markdown-body/MarkdownBody.js +49 -0
- package/dist/lib/react/primitives/markdown-body/index.d.ts +2 -0
- package/dist/lib/react/primitives/markdown-body/index.js +1 -0
- package/dist/lib/react/primitives/severity-badge/index.d.ts +2 -0
- package/dist/lib/react/primitives/severity-badge/index.js +1 -0
- package/dist/lib/react/registry.d.ts +10 -0
- package/dist/lib/react/registry.js +505 -210
- package/dist/lib/react/styles.css +490 -38
- package/docs/cli-structure.md +141 -0
- package/docs/component-protocol.md +199 -33
- package/docs/component-taxonomy.md +40 -4
- package/docs/design.md +42 -21
- package/docs/design.zh-CN.md +41 -21
- package/docs/naming.md +17 -7
- package/docs/releasing.md +132 -0
- package/docs/testing.md +35 -10
- package/package.json +9 -7
- package/dist/lib/cli/config.d.ts +0 -2
- package/dist/lib/cli/review.d.ts +0 -33
- package/dist/lib/cli/scaffold.d.ts +0 -1
- package/dist/lib/cli/scaffold.js +0 -56
- package/dist/lib/cli/validate.d.ts +0 -6
- package/dist/lib/cli/validate.js +0 -79
- package/dist/lib/react/components/Callout.d.ts +0 -9
- package/dist/lib/react/components/CodeBlock.d.ts +0 -10
- package/dist/lib/react/components/CodeBlock.js +0 -28
- package/dist/lib/react/components/DecisionMatrix.d.ts +0 -16
- package/dist/lib/react/components/DecisionMatrix.js +0 -27
- package/dist/lib/react/components/DiffBlock.js +0 -24
- package/dist/lib/react/components/InlineText.js +0 -18
- package/dist/lib/react/components/MarkdownBody.d.ts +0 -7
- package/dist/lib/react/components/MarkdownBody.js +0 -36
- package/dist/lib/react/components/OptionGrid.d.ts +0 -13
- package/dist/lib/react/components/OptionGrid.js +0 -21
- /package/dist/lib/cli/{build.d.ts → commands/build.d.ts} +0 -0
- /package/dist/lib/cli/{components.d.ts → commands/components.d.ts} +0 -0
- /package/dist/lib/cli/{dev.d.ts → commands/dev.d.ts} +0 -0
- /package/dist/lib/cli/{types.js → config/types.js} +0 -0
- /package/dist/lib/cli/{artifact-state.d.ts → state/artifact-state.d.ts} +0 -0
- /package/dist/lib/cli/{artifact-state.js → state/artifact-state.js} +0 -0
- /package/dist/lib/react/{components → composites/export-panel}/ExportPanel.d.ts +0 -0
- /package/dist/lib/react/{components → composites/section}/Section.d.ts +0 -0
- /package/dist/lib/react/{components → interactions/artifact-state}/ArtifactState.d.ts +0 -0
- /package/dist/lib/react/{components → interactions/artifact-state}/ArtifactState.js +0 -0
- /package/dist/lib/react/{components → layout/layout-primitives}/Layout.d.ts +0 -0
- /package/dist/lib/react/{components → layout/layout-primitives}/Layout.js +0 -0
- /package/dist/lib/react/{components → primitives/diff-block}/DiffBlock.d.ts +0 -0
- /package/dist/lib/react/{components → primitives/severity-badge}/SeverityBadge.d.ts +0 -0
- /package/dist/lib/react/{components → primitives/severity-badge}/SeverityBadge.js +0 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { addReviewThreadService, replyToReviewThreadsService, validateReviewStateService } from "../services/review.js";
|
|
2
|
+
export async function reviewCommand(projectRoot, args) {
|
|
3
|
+
const [subcommand] = args;
|
|
4
|
+
if (subcommand === "add") {
|
|
5
|
+
const options = parseReviewAddArgs(args.slice(1));
|
|
6
|
+
const result = await addReviewThreadService(projectRoot, options.input, options);
|
|
7
|
+
console.log(formatReviewAddResult(result));
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (subcommand === "reply") {
|
|
11
|
+
const options = parseReviewReplyArgs(args.slice(1));
|
|
12
|
+
const result = await replyToReviewThreadsService(projectRoot, options.input, options);
|
|
13
|
+
console.log(formatReviewReplyResult(result));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (subcommand === "validate") {
|
|
17
|
+
const input = parseReviewValidateArgs(args.slice(1));
|
|
18
|
+
const result = await validateReviewStateService(projectRoot, input);
|
|
19
|
+
console.log(formatReviewValidationResult(result));
|
|
20
|
+
if (result.missingThreads.length > 0) {
|
|
21
|
+
process.exitCode = 1;
|
|
22
|
+
}
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
throw new Error("review requires a subcommand. Use `mdx-artifacts review add <file.mdx>`, `mdx-artifacts review reply <file.mdx>`, or `mdx-artifacts review validate <file.mdx>`.");
|
|
26
|
+
}
|
|
27
|
+
function parseReviewReplyArgs(args) {
|
|
28
|
+
const [input, ...rest] = args;
|
|
29
|
+
if (!input) {
|
|
30
|
+
throw new Error("review reply requires a .mdx file path.");
|
|
31
|
+
}
|
|
32
|
+
const { replies, status } = parseReviewReplyOptions(rest);
|
|
33
|
+
return {
|
|
34
|
+
input,
|
|
35
|
+
replies,
|
|
36
|
+
...(status ? { status } : {})
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function parseReviewValidateArgs(args) {
|
|
40
|
+
const [input, ...rest] = args;
|
|
41
|
+
if (!input) {
|
|
42
|
+
throw new Error("review validate requires a .mdx file path.");
|
|
43
|
+
}
|
|
44
|
+
if (rest.length > 0) {
|
|
45
|
+
throw new Error(`Unexpected review validate argument: ${rest[0]}`);
|
|
46
|
+
}
|
|
47
|
+
return input;
|
|
48
|
+
}
|
|
49
|
+
function parseReviewAddArgs(args) {
|
|
50
|
+
const [input, ...rest] = args;
|
|
51
|
+
if (!input) {
|
|
52
|
+
throw new Error("review add requires a .mdx file path.");
|
|
53
|
+
}
|
|
54
|
+
const options = parseKeyValueOptions(rest, ["anchor", "body", "title"]);
|
|
55
|
+
const anchorId = options.get("anchor");
|
|
56
|
+
const body = options.get("body");
|
|
57
|
+
const title = options.get("title");
|
|
58
|
+
if (!anchorId) {
|
|
59
|
+
throw new Error("review add requires --anchor <anchorId>.");
|
|
60
|
+
}
|
|
61
|
+
if (!body || !body.trim()) {
|
|
62
|
+
throw new Error("review add requires --body <message>.");
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
input,
|
|
66
|
+
anchorId,
|
|
67
|
+
body: body.trim(),
|
|
68
|
+
...(title ? { title } : {})
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function parseKeyValueOptions(args, allowedKeys) {
|
|
72
|
+
const options = new Map();
|
|
73
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
74
|
+
const name = args[index];
|
|
75
|
+
if (!name?.startsWith("--")) {
|
|
76
|
+
throw new Error(`Unexpected review argument: ${name}`);
|
|
77
|
+
}
|
|
78
|
+
const key = name.slice(2);
|
|
79
|
+
const value = args[index + 1];
|
|
80
|
+
if (!allowedKeys.includes(key)) {
|
|
81
|
+
throw new Error(`Unknown review option: --${key}.`);
|
|
82
|
+
}
|
|
83
|
+
if (!value || value.startsWith("--")) {
|
|
84
|
+
throw new Error(`Missing value for --${key}.`);
|
|
85
|
+
}
|
|
86
|
+
options.set(key, value);
|
|
87
|
+
index += 1;
|
|
88
|
+
}
|
|
89
|
+
return options;
|
|
90
|
+
}
|
|
91
|
+
function parseReviewReplyOptions(args) {
|
|
92
|
+
const replies = [];
|
|
93
|
+
let status;
|
|
94
|
+
let threadId;
|
|
95
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
96
|
+
const name = args[index];
|
|
97
|
+
if (!name?.startsWith("--")) {
|
|
98
|
+
throw new Error(`Unexpected review reply argument: ${name}`);
|
|
99
|
+
}
|
|
100
|
+
const key = name.slice(2);
|
|
101
|
+
const value = args[index + 1];
|
|
102
|
+
if (!value || value.startsWith("--")) {
|
|
103
|
+
throw new Error(`Missing value for --${key}.`);
|
|
104
|
+
}
|
|
105
|
+
if (key === "thread") {
|
|
106
|
+
if (threadId) {
|
|
107
|
+
throw new Error(`Missing --body for --thread ${threadId}.`);
|
|
108
|
+
}
|
|
109
|
+
threadId = value;
|
|
110
|
+
}
|
|
111
|
+
else if (key === "body") {
|
|
112
|
+
if (!threadId) {
|
|
113
|
+
throw new Error("review reply requires --thread before --body.");
|
|
114
|
+
}
|
|
115
|
+
if (!value.trim()) {
|
|
116
|
+
throw new Error("review reply requires --body <message>.");
|
|
117
|
+
}
|
|
118
|
+
replies.push({ threadId, body: value.trim() });
|
|
119
|
+
threadId = undefined;
|
|
120
|
+
}
|
|
121
|
+
else if (key === "status") {
|
|
122
|
+
status = value;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
throw new Error(`Unknown review reply option: --${key}.`);
|
|
126
|
+
}
|
|
127
|
+
index += 1;
|
|
128
|
+
}
|
|
129
|
+
if (threadId) {
|
|
130
|
+
throw new Error(`Missing --body for --thread ${threadId}.`);
|
|
131
|
+
}
|
|
132
|
+
if (replies.length === 0) {
|
|
133
|
+
throw new Error("review reply requires at least one --thread <threadId> --body <message> pair.");
|
|
134
|
+
}
|
|
135
|
+
return { replies, status };
|
|
136
|
+
}
|
|
137
|
+
function formatReviewAddResult(result) {
|
|
138
|
+
return [`review add ok`, `state: ${result.statePath}`, `thread: ${result.threadId}`, `anchorId: ${result.anchorId}`].join("\n");
|
|
139
|
+
}
|
|
140
|
+
function formatReviewReplyResult(result) {
|
|
141
|
+
return [
|
|
142
|
+
`review reply ok`,
|
|
143
|
+
`state: ${result.statePath}`,
|
|
144
|
+
`messages: ${result.messageRecords.length}`,
|
|
145
|
+
...result.messageRecords.map((record) => `- thread: ${record.threadId} message: ${record.messageId}`)
|
|
146
|
+
].join("\n");
|
|
147
|
+
}
|
|
148
|
+
function formatReviewValidationResult(result) {
|
|
149
|
+
return result.missingThreads.length === 0
|
|
150
|
+
? [
|
|
151
|
+
`review validate ok`,
|
|
152
|
+
`state: ${result.statePath}`,
|
|
153
|
+
`anchors: ${result.anchorCount}`,
|
|
154
|
+
`threads: ${result.threadCount}`
|
|
155
|
+
].join("\n")
|
|
156
|
+
: [
|
|
157
|
+
`review validate failed`,
|
|
158
|
+
`state: ${result.statePath}`,
|
|
159
|
+
`anchors: ${result.anchorCount}`,
|
|
160
|
+
`threads: ${result.threadCount}`,
|
|
161
|
+
`missing: ${result.missingThreads.length}`,
|
|
162
|
+
...result.missingThreads.map((thread) => [
|
|
163
|
+
`- thread: ${thread.threadId}`,
|
|
164
|
+
`anchorId: ${thread.anchorId}`,
|
|
165
|
+
thread.status ? `status: ${thread.status}` : undefined,
|
|
166
|
+
thread.title ? `title: ${thread.title}` : undefined
|
|
167
|
+
]
|
|
168
|
+
.filter(Boolean)
|
|
169
|
+
.join(" "))
|
|
170
|
+
].join("\n");
|
|
171
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type InitAgent = "generic" | "codex" | "claude-code" | "cursor" | "all";
|
|
2
|
+
export type InitProjectOptions = {
|
|
3
|
+
agent?: InitAgent;
|
|
4
|
+
docsDir?: string;
|
|
5
|
+
componentsDir?: string;
|
|
6
|
+
yes?: boolean;
|
|
7
|
+
};
|
|
8
|
+
type InitPromptIo = {
|
|
9
|
+
input: NodeJS.ReadStream;
|
|
10
|
+
output: NodeJS.WriteStream;
|
|
11
|
+
question?: (query: string) => Promise<string>;
|
|
12
|
+
};
|
|
13
|
+
export declare function initProject(projectRoot: string, options?: InitProjectOptions): Promise<void>;
|
|
14
|
+
export declare function parseInitOptions(args: string[], io?: InitPromptIo): Promise<InitProjectOptions>;
|
|
15
|
+
export declare function parseInitAgent(value: string | undefined): InitAgent;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { stdin as defaultInput, stdout as defaultOutput } from "node:process";
|
|
4
|
+
import { createInterface } from "node:readline/promises";
|
|
5
|
+
const defaultDocsDir = "artifact-docs";
|
|
6
|
+
const defaultComponentsDir = "artifact-components";
|
|
7
|
+
function agentSnippetContent(options) {
|
|
8
|
+
return `# MDX Artifacts Agent Instructions
|
|
9
|
+
|
|
10
|
+
Use MDX Artifacts when the user asks for an interactive report, option comparison, review handoff, temporary tool, or visual explanation page that should be authored as Markdown-native MDX.
|
|
11
|
+
|
|
12
|
+
Do not use MDX Artifacts for ordinary README files, simple notes, raw HTML pages, full web apps, or cases where the user explicitly asks for plain Markdown.
|
|
13
|
+
|
|
14
|
+
Authoring rules:
|
|
15
|
+
|
|
16
|
+
1. Create .mdx files under ${options.docsDir}/.
|
|
17
|
+
2. Keep prose in Markdown and use semantic React components as islands.
|
|
18
|
+
3. Prefer high-level components from mdx-artifacts/react.
|
|
19
|
+
4. Prefer MDX children for human-readable body content when a component supports it.
|
|
20
|
+
5. Use props for stable ids, short labels, variants, layout controls, export values, and structured data.
|
|
21
|
+
6. Do not inline bulky data in JSX props. Prefer adjacent local files when the project supports them.
|
|
22
|
+
7. Local resource paths must stay inside the project root. Do not use remote URLs, ~ paths, or project-root escapes.
|
|
23
|
+
8. Interactive artifacts must include ExportPanel or an equivalent export path.
|
|
24
|
+
9. Run mdx-artifacts components <ComponentName> when component props are unclear.
|
|
25
|
+
10. Run mdx-artifacts components --json when machine-readable component metadata is needed.
|
|
26
|
+
11. Run mdx-artifacts validate <file.mdx> before build.
|
|
27
|
+
12. Run mdx-artifacts validate <file.mdx> --json when structured diagnostics are useful for repairs.
|
|
28
|
+
13. If validation reports diagnostics, fix errors first, review warnings, and validate again.
|
|
29
|
+
14. Run mdx-artifacts build <file.mdx> to produce standalone HTML.
|
|
30
|
+
|
|
31
|
+
${projectLocalComponentGuidance(options)}
|
|
32
|
+
`;
|
|
33
|
+
}
|
|
34
|
+
function skillMarkdownContent(options) {
|
|
35
|
+
return `---
|
|
36
|
+
name: mdx-artifacts
|
|
37
|
+
description: Create, validate, and build Markdown-native MDX artifacts with MDX Artifacts semantic React components, structured diagnostics, and standalone HTML output.
|
|
38
|
+
license: MIT
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
# MDX Artifacts Skill
|
|
42
|
+
|
|
43
|
+
Use MDX Artifacts when the user asks for an interactive report, option comparison, review handoff, temporary tool, or visual explanation page that should remain readable as Markdown-native MDX.
|
|
44
|
+
|
|
45
|
+
Do not use MDX Artifacts for ordinary README files, simple notes, raw HTML pages, full web apps, or cases where the user explicitly asks for plain Markdown.
|
|
46
|
+
|
|
47
|
+
Authoring workflow:
|
|
48
|
+
|
|
49
|
+
1. Create .mdx files under ${options.docsDir}/.
|
|
50
|
+
2. Keep narrative prose in Markdown.
|
|
51
|
+
3. Use semantic components from mdx-artifacts/react for workflow UI.
|
|
52
|
+
4. Import only the components the artifact uses.
|
|
53
|
+
5. Prefer MDX children for readable body content.
|
|
54
|
+
6. Use stable id props on reviewable or stateful components.
|
|
55
|
+
7. Include ExportPanel or an equivalent export path for interactive artifacts.
|
|
56
|
+
8. Keep local resource paths inside the project root.
|
|
57
|
+
9. Do not use remote URLs, ~ paths, or project-root escapes for local resources.
|
|
58
|
+
|
|
59
|
+
Component discovery:
|
|
60
|
+
|
|
61
|
+
- Run mdx-artifacts components to list available components.
|
|
62
|
+
- Run mdx-artifacts components <ComponentName> when props are unclear.
|
|
63
|
+
- Run mdx-artifacts components --json when machine-readable metadata is needed.
|
|
64
|
+
- Do not copy component metadata into this skill. The CLI registry is the source of truth.
|
|
65
|
+
|
|
66
|
+
Validation and repair loop:
|
|
67
|
+
|
|
68
|
+
- Run mdx-artifacts validate <file.mdx> before build.
|
|
69
|
+
- Run mdx-artifacts validate <file.mdx> --json when structured diagnostics are useful.
|
|
70
|
+
- Use diagnostic code, suggestion, componentName, propName, and example fields to repair the MDX.
|
|
71
|
+
- Re-run validation after each repair pass.
|
|
72
|
+
- Treat errors as blockers. Review warnings before calling the artifact complete.
|
|
73
|
+
|
|
74
|
+
Build:
|
|
75
|
+
|
|
76
|
+
- Run mdx-artifacts build <file.mdx> to produce standalone HTML.
|
|
77
|
+
- Do not call build until validation has been reviewed.
|
|
78
|
+
|
|
79
|
+
Project-local components:
|
|
80
|
+
|
|
81
|
+
${projectLocalComponentGuidance(options)}
|
|
82
|
+
`;
|
|
83
|
+
}
|
|
84
|
+
function cursorRuleContent(options) {
|
|
85
|
+
return `---
|
|
86
|
+
description: Use MDX Artifacts to create Markdown-native MDX artifacts with semantic React components and validation.
|
|
87
|
+
alwaysApply: false
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
# MDX Artifacts Rule
|
|
91
|
+
|
|
92
|
+
Use MDX Artifacts when the user asks for an interactive report, option comparison, review handoff, temporary tool, or visual explanation page that should be authored as Markdown-native MDX.
|
|
93
|
+
|
|
94
|
+
Do not use MDX Artifacts for ordinary README files, simple notes, raw HTML pages, full web apps, or cases where the user explicitly asks for plain Markdown.
|
|
95
|
+
|
|
96
|
+
Workflow:
|
|
97
|
+
|
|
98
|
+
1. Create .mdx files under ${options.docsDir}/.
|
|
99
|
+
2. Keep prose in Markdown and use semantic React components as islands.
|
|
100
|
+
3. Query component metadata with mdx-artifacts components <ComponentName> instead of guessing props.
|
|
101
|
+
4. Use mdx-artifacts components --json when machine-readable metadata is needed.
|
|
102
|
+
5. Run mdx-artifacts validate <file.mdx> before build.
|
|
103
|
+
6. Use mdx-artifacts validate <file.mdx> --json for structured repair diagnostics.
|
|
104
|
+
7. Fix validation errors, review warnings, and validate again.
|
|
105
|
+
8. Run mdx-artifacts build <file.mdx> only after validation has been reviewed.
|
|
106
|
+
|
|
107
|
+
Local resource paths must stay inside the project root. Do not use remote URLs, ~ paths, or project-root escapes for local resources.
|
|
108
|
+
|
|
109
|
+
${projectLocalComponentGuidance(options)}
|
|
110
|
+
`;
|
|
111
|
+
}
|
|
112
|
+
export async function initProject(projectRoot, options = {}) {
|
|
113
|
+
const agent = options.agent ?? "generic";
|
|
114
|
+
const docsDir = validateProjectRelativePath(projectRoot, options.docsDir ?? defaultDocsDir, "docsDir");
|
|
115
|
+
const componentsDir = options.componentsDir
|
|
116
|
+
? validateProjectRelativePath(projectRoot, options.componentsDir, "componentsDir")
|
|
117
|
+
: undefined;
|
|
118
|
+
const docsExamplesDir = path.join(projectRoot, docsDir, "examples");
|
|
119
|
+
const agentsDir = path.join(projectRoot, "agents");
|
|
120
|
+
const scaffoldOptions = { docsDir, componentsDir };
|
|
121
|
+
await mkdir(docsExamplesDir, { recursive: true });
|
|
122
|
+
await mkdir(agentsDir, { recursive: true });
|
|
123
|
+
if (componentsDir) {
|
|
124
|
+
await mkdir(path.join(projectRoot, componentsDir), { recursive: true });
|
|
125
|
+
}
|
|
126
|
+
await writeFile(path.join(projectRoot, "mdx-artifacts.config.mjs"), `/** @type {import("mdx-artifacts").MdxArtifactsConfig} */
|
|
127
|
+
const config = {
|
|
128
|
+
docsDir: ${JSON.stringify(docsDir)},
|
|
129
|
+
outDir: "dist/artifacts",
|
|
130
|
+
includeDefaultStyles: true,
|
|
131
|
+
styles: [],
|
|
132
|
+
tailwindSources: ${formatTailwindSources(componentsDir)}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export default config;
|
|
136
|
+
`, { flag: "wx" }).catch(ignoreExisting);
|
|
137
|
+
await writeFile(path.join(agentsDir, "AGENTS.snippet.md"), agentSnippetContent(scaffoldOptions), { flag: "wx" }).catch(ignoreExisting);
|
|
138
|
+
await writeFile(path.join(docsExamplesDir, "hello.mdx"), `import { ContentSet, ExportPanel } from "mdx-artifacts/react";
|
|
139
|
+
|
|
140
|
+
# Hello Artifact
|
|
141
|
+
|
|
142
|
+
<ContentSet
|
|
143
|
+
id="set.initialized"
|
|
144
|
+
title="Has MDX Artifacts been initialized?"
|
|
145
|
+
columns={2}
|
|
146
|
+
>
|
|
147
|
+
<ContentSet.Item
|
|
148
|
+
id="initialized"
|
|
149
|
+
title="Initialized"
|
|
150
|
+
badge="Ready"
|
|
151
|
+
tone="positive"
|
|
152
|
+
emphasis="primary"
|
|
153
|
+
summary="Ready to continue generating artifacts."
|
|
154
|
+
>
|
|
155
|
+
- MDX source exists
|
|
156
|
+
- Export panel exists
|
|
157
|
+
- Real content still needs to be added
|
|
158
|
+
</ContentSet.Item>
|
|
159
|
+
</ContentSet>
|
|
160
|
+
|
|
161
|
+
<ExportPanel
|
|
162
|
+
value={{
|
|
163
|
+
recommendation: "Continue generating interactive HTML artifacts with MDX and high-level components"
|
|
164
|
+
}}
|
|
165
|
+
/>
|
|
166
|
+
`, { flag: "wx" }).catch(ignoreExisting);
|
|
167
|
+
await installAgentGuidance(projectRoot, agent, scaffoldOptions);
|
|
168
|
+
console.log(formatInitSummary({ agent, docsDir, componentsDir }));
|
|
169
|
+
}
|
|
170
|
+
export async function parseInitOptions(args, io = { input: defaultInput, output: defaultOutput }) {
|
|
171
|
+
const yes = args.includes("--yes");
|
|
172
|
+
const agentValue = readFlagValue(args, "--agent");
|
|
173
|
+
const options = {
|
|
174
|
+
yes,
|
|
175
|
+
agent: agentValue ? parseInitAgent(agentValue) : undefined,
|
|
176
|
+
docsDir: readFlagValue(args, "--docs-dir"),
|
|
177
|
+
componentsDir: readFlagValue(args, "--components-dir")
|
|
178
|
+
};
|
|
179
|
+
if (shouldPromptInitOptions(options, io)) {
|
|
180
|
+
return promptInitOptions(options, io);
|
|
181
|
+
}
|
|
182
|
+
return options;
|
|
183
|
+
}
|
|
184
|
+
export function parseInitAgent(value) {
|
|
185
|
+
if (!value) {
|
|
186
|
+
return "generic";
|
|
187
|
+
}
|
|
188
|
+
const agent = value.toLowerCase().trim().replace(/[\s_]+/g, "-");
|
|
189
|
+
if (agent === "generic" || agent === "codex" || agent === "claude-code" || agent === "cursor" || agent === "all") {
|
|
190
|
+
return agent;
|
|
191
|
+
}
|
|
192
|
+
throw new Error(`Unsupported init agent: ${value}. Use generic, codex, claude-code, cursor, or all.`);
|
|
193
|
+
}
|
|
194
|
+
async function installAgentGuidance(projectRoot, agent, options) {
|
|
195
|
+
const targets = agentGuidanceTargets(projectRoot, agent, options);
|
|
196
|
+
for (const target of targets) {
|
|
197
|
+
await mkdir(path.dirname(target.path), { recursive: true });
|
|
198
|
+
await writeFile(target.path, target.content, { flag: "wx" }).catch(ignoreExisting);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function agentGuidanceTargets(projectRoot, agent, options) {
|
|
202
|
+
const targets = [];
|
|
203
|
+
if (agent === "codex" || agent === "all") {
|
|
204
|
+
targets.push({
|
|
205
|
+
path: path.join(projectRoot, ".agents", "skills", "mdx-artifacts", "SKILL.md"),
|
|
206
|
+
content: skillMarkdownContent(options)
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
if (agent === "claude-code" || agent === "all") {
|
|
210
|
+
targets.push({
|
|
211
|
+
path: path.join(projectRoot, ".claude", "skills", "mdx-artifacts", "SKILL.md"),
|
|
212
|
+
content: skillMarkdownContent(options)
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
if (agent === "cursor" || agent === "all") {
|
|
216
|
+
targets.push({
|
|
217
|
+
path: path.join(projectRoot, ".cursor", "rules", "mdx-artifacts.mdc"),
|
|
218
|
+
content: cursorRuleContent(options)
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return targets;
|
|
222
|
+
}
|
|
223
|
+
async function promptInitOptions(options, io) {
|
|
224
|
+
const rl = io.question ? undefined : createInterface({ input: io.input, output: io.output });
|
|
225
|
+
const question = io.question ?? ((query) => rl?.question(query) ?? Promise.resolve(""));
|
|
226
|
+
const style = createPromptStyle(io.output.isTTY === true && !io.question);
|
|
227
|
+
try {
|
|
228
|
+
const docsDir = options.docsDir ?? await askPathChoice(question, {
|
|
229
|
+
title: "Where should MDX artifact source files live?",
|
|
230
|
+
defaultPath: defaultDocsDir,
|
|
231
|
+
customQuestion: "Custom MDX artifact source directory:",
|
|
232
|
+
style
|
|
233
|
+
});
|
|
234
|
+
const useComponents = options.componentsDir
|
|
235
|
+
? true
|
|
236
|
+
: await askChoice(question, {
|
|
237
|
+
title: "Use project-local React components?",
|
|
238
|
+
description: "Adds a Tailwind source directory; it does not auto-register components.",
|
|
239
|
+
defaultIndex: 0,
|
|
240
|
+
choices: [
|
|
241
|
+
{ value: "no", label: "No", aliases: ["n"] },
|
|
242
|
+
{ value: "yes", label: "Yes, choose a project-local component source directory", aliases: ["y"] }
|
|
243
|
+
],
|
|
244
|
+
style
|
|
245
|
+
}) === "yes";
|
|
246
|
+
const componentsDir = options.componentsDir
|
|
247
|
+
?? (useComponents
|
|
248
|
+
? await askPathChoice(question, {
|
|
249
|
+
title: "Where should project-local component source files live?",
|
|
250
|
+
defaultPath: defaultComponentsDir,
|
|
251
|
+
customQuestion: "Custom project-local component source directory:",
|
|
252
|
+
style
|
|
253
|
+
})
|
|
254
|
+
: undefined);
|
|
255
|
+
const agent = options.agent
|
|
256
|
+
?? parseInitAgent(await askChoice(question, {
|
|
257
|
+
title: "Install agent guidance for which tool?",
|
|
258
|
+
defaultIndex: 0,
|
|
259
|
+
choices: [
|
|
260
|
+
{ value: "generic", label: "generic" },
|
|
261
|
+
{ value: "codex", label: "codex" },
|
|
262
|
+
{ value: "claude-code", label: "claude-code", aliases: ["claude_code"] },
|
|
263
|
+
{ value: "cursor", label: "cursor" },
|
|
264
|
+
{ value: "all", label: "all" }
|
|
265
|
+
],
|
|
266
|
+
style
|
|
267
|
+
}));
|
|
268
|
+
return { ...options, docsDir, componentsDir, agent };
|
|
269
|
+
}
|
|
270
|
+
finally {
|
|
271
|
+
rl?.close();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
async function askPathChoice(question, options) {
|
|
275
|
+
const selected = await askChoice(question, {
|
|
276
|
+
title: options.title,
|
|
277
|
+
defaultIndex: 0,
|
|
278
|
+
choices: [
|
|
279
|
+
{ value: "default", label: formatPromptPath(options.defaultPath) },
|
|
280
|
+
{ value: "custom", label: "Enter a custom directory" }
|
|
281
|
+
],
|
|
282
|
+
style: options.style
|
|
283
|
+
});
|
|
284
|
+
if (selected === "default") {
|
|
285
|
+
return options.defaultPath;
|
|
286
|
+
}
|
|
287
|
+
return askText(question, options.customQuestion, options.style);
|
|
288
|
+
}
|
|
289
|
+
async function askChoice(question, options) {
|
|
290
|
+
const prompt = formatChoicePrompt(options);
|
|
291
|
+
const answer = (await question(prompt)).trim();
|
|
292
|
+
if (!answer) {
|
|
293
|
+
return options.choices[options.defaultIndex]?.value ?? "";
|
|
294
|
+
}
|
|
295
|
+
const selectedIndex = Number.parseInt(answer, 10);
|
|
296
|
+
if (Number.isInteger(selectedIndex) && String(selectedIndex) === answer && selectedIndex >= 1 && selectedIndex <= options.choices.length) {
|
|
297
|
+
return options.choices[selectedIndex - 1].value;
|
|
298
|
+
}
|
|
299
|
+
const normalized = answer.toLowerCase();
|
|
300
|
+
const direct = options.choices.find((choice) => {
|
|
301
|
+
const aliases = choice.aliases ?? [];
|
|
302
|
+
return choice.value.toLowerCase() === normalized || choice.label.toLowerCase() === normalized || aliases.includes(normalized);
|
|
303
|
+
});
|
|
304
|
+
if (direct) {
|
|
305
|
+
return direct.value;
|
|
306
|
+
}
|
|
307
|
+
throw new Error(`Unsupported selection: ${answer}. Use 1-${options.choices.length}.`);
|
|
308
|
+
}
|
|
309
|
+
async function askText(question, title, style) {
|
|
310
|
+
const answer = (await question(`${style.heading(title)} `)).trim();
|
|
311
|
+
if (!answer) {
|
|
312
|
+
throw new Error(`${title} requires a value.`);
|
|
313
|
+
}
|
|
314
|
+
return answer;
|
|
315
|
+
}
|
|
316
|
+
function shouldPromptInitOptions(options, io) {
|
|
317
|
+
return !options.yes && io.input.isTTY === true && io.output.isTTY === true;
|
|
318
|
+
}
|
|
319
|
+
function readFlagValue(args, flag) {
|
|
320
|
+
const inline = args.find((arg) => arg.startsWith(`${flag}=`));
|
|
321
|
+
if (inline) {
|
|
322
|
+
return inline.slice(flag.length + 1);
|
|
323
|
+
}
|
|
324
|
+
const index = args.indexOf(flag);
|
|
325
|
+
if (index < 0) {
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
const value = args[index + 1];
|
|
329
|
+
if (!value || value.startsWith("--")) {
|
|
330
|
+
throw new Error(`mdx-artifacts init ${flag} requires a value.`);
|
|
331
|
+
}
|
|
332
|
+
return value;
|
|
333
|
+
}
|
|
334
|
+
function validateProjectRelativePath(projectRoot, value, fieldName) {
|
|
335
|
+
const normalized = value.trim().replace(/\\/g, "/").replace(/\/+$/g, "");
|
|
336
|
+
if (!normalized) {
|
|
337
|
+
throw new Error(`${fieldName} must not be empty.`);
|
|
338
|
+
}
|
|
339
|
+
if (path.isAbsolute(normalized) || normalized === "~" || normalized.startsWith("~/") || isUrlLike(normalized)) {
|
|
340
|
+
throw new Error(`${fieldName} must be a project-relative path.`);
|
|
341
|
+
}
|
|
342
|
+
const resolved = path.resolve(projectRoot, normalized);
|
|
343
|
+
const relative = path.relative(projectRoot, resolved);
|
|
344
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
345
|
+
throw new Error(`${fieldName} must stay inside the project root.`);
|
|
346
|
+
}
|
|
347
|
+
return normalized;
|
|
348
|
+
}
|
|
349
|
+
function isUrlLike(value) {
|
|
350
|
+
return /^[a-zA-Z][a-zA-Z\d+.-]*:\/\//.test(value) || /^(data|blob):/.test(value) || value.startsWith("//");
|
|
351
|
+
}
|
|
352
|
+
function formatTailwindSources(componentsDir) {
|
|
353
|
+
if (!componentsDir) {
|
|
354
|
+
return "[]";
|
|
355
|
+
}
|
|
356
|
+
return `[\n ${JSON.stringify(`${componentsDir}/**/*.{ts,tsx}`)}\n ]`;
|
|
357
|
+
}
|
|
358
|
+
function formatChoicePrompt(options) {
|
|
359
|
+
const lines = [
|
|
360
|
+
"",
|
|
361
|
+
options.style.heading(options.title),
|
|
362
|
+
options.description ? ` ${options.style.muted(options.description)}` : undefined,
|
|
363
|
+
...options.choices.map((choice, index) => {
|
|
364
|
+
const defaultLabel = index === options.defaultIndex ? options.style.muted(" (default)") : "";
|
|
365
|
+
const description = choice.description ? options.style.muted(` - ${choice.description}`) : "";
|
|
366
|
+
return ` ${options.style.option(String(index + 1))}. ${choice.label}${defaultLabel}${description}`;
|
|
367
|
+
}),
|
|
368
|
+
`Select an option (${options.defaultIndex + 1}) `
|
|
369
|
+
];
|
|
370
|
+
return lines.filter((line) => line !== undefined).join("\n");
|
|
371
|
+
}
|
|
372
|
+
function formatPromptPath(value) {
|
|
373
|
+
return `./${value.replace(/^\.?\//, "").replace(/\/+$/g, "")}/`;
|
|
374
|
+
}
|
|
375
|
+
function createPromptStyle(useColor) {
|
|
376
|
+
if (!useColor || process.env.NO_COLOR) {
|
|
377
|
+
return {
|
|
378
|
+
heading: identity,
|
|
379
|
+
muted: identity,
|
|
380
|
+
option: identity
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
return {
|
|
384
|
+
heading: (value) => `\x1b[1m${value}\x1b[22m`,
|
|
385
|
+
muted: (value) => `\x1b[2m${value}\x1b[22m`,
|
|
386
|
+
option: (value) => `\x1b[36m${value}\x1b[39m`
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function identity(value) {
|
|
390
|
+
return value;
|
|
391
|
+
}
|
|
392
|
+
function formatInitSummary(options) {
|
|
393
|
+
const examplePath = `${options.docsDir}/examples/hello.mdx`;
|
|
394
|
+
const files = [
|
|
395
|
+
" config: mdx-artifacts.config.mjs",
|
|
396
|
+
` example: ${examplePath}`,
|
|
397
|
+
" agent snippet: agents/AGENTS.snippet.md",
|
|
398
|
+
...agentGuidanceSummaryFiles(options.agent),
|
|
399
|
+
...(options.componentsDir ? [` component source: ${options.componentsDir}/`] : [])
|
|
400
|
+
];
|
|
401
|
+
return [
|
|
402
|
+
"MDX Artifacts initialized.",
|
|
403
|
+
"",
|
|
404
|
+
"Summary:",
|
|
405
|
+
` docsDir: ${options.docsDir}`,
|
|
406
|
+
` componentsDir: ${options.componentsDir ?? "none"}`,
|
|
407
|
+
` agent: ${options.agent}`,
|
|
408
|
+
"",
|
|
409
|
+
"Files:",
|
|
410
|
+
...files,
|
|
411
|
+
"",
|
|
412
|
+
"Next steps:",
|
|
413
|
+
` mdx-artifacts validate ${examplePath}`,
|
|
414
|
+
` mdx-artifacts build ${examplePath}`
|
|
415
|
+
].join("\n");
|
|
416
|
+
}
|
|
417
|
+
function agentGuidanceSummaryFiles(agent) {
|
|
418
|
+
const files = [];
|
|
419
|
+
if (agent === "codex" || agent === "all") {
|
|
420
|
+
files.push(" Codex skill: .agents/skills/mdx-artifacts/SKILL.md");
|
|
421
|
+
}
|
|
422
|
+
if (agent === "claude-code" || agent === "all") {
|
|
423
|
+
files.push(" Claude Code skill: .claude/skills/mdx-artifacts/SKILL.md");
|
|
424
|
+
}
|
|
425
|
+
if (agent === "cursor" || agent === "all") {
|
|
426
|
+
files.push(" Cursor rule: .cursor/rules/mdx-artifacts.mdc");
|
|
427
|
+
}
|
|
428
|
+
return files;
|
|
429
|
+
}
|
|
430
|
+
function projectLocalComponentGuidance(options) {
|
|
431
|
+
if (!options.componentsDir) {
|
|
432
|
+
return "If this project later adds project-local components, choose a stable project-local component source directory and include it in tailwindSources when those files use Tailwind classes.";
|
|
433
|
+
}
|
|
434
|
+
return `Project-local component source may live under ${options.componentsDir}/. This is a source location for user-owned React components, not automatic component registration. If those files use Tailwind classes, keep ${options.componentsDir}/**/*.{ts,tsx} in tailwindSources.`;
|
|
435
|
+
}
|
|
436
|
+
function ignoreExisting(error) {
|
|
437
|
+
if (error.code !== "EEXIST") {
|
|
438
|
+
throw error;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { MdxArtifactsConfig } from "../config/types";
|
|
2
|
+
import type { ArtifactDiagnostic } from "../diagnostics/diagnostics";
|
|
3
|
+
export type { ArtifactDiagnostic } from "../diagnostics/diagnostics";
|
|
4
|
+
export type ValidationResult = {
|
|
5
|
+
diagnostics: ArtifactDiagnostic[];
|
|
6
|
+
errors: string[];
|
|
7
|
+
warnings: string[];
|
|
8
|
+
};
|
|
9
|
+
export type ValidateMdxOptions = {
|
|
10
|
+
projectRoot?: string;
|
|
11
|
+
config?: Required<MdxArtifactsConfig>;
|
|
12
|
+
};
|
|
13
|
+
export declare function validateMdx(filePath: string, options?: ValidateMdxOptions): Promise<ValidationResult>;
|
|
14
|
+
export declare function formatValidationJson(result: ValidationResult): {
|
|
15
|
+
ok: boolean;
|
|
16
|
+
diagnostics: ArtifactDiagnostic[];
|
|
17
|
+
};
|
|
18
|
+
export declare function printValidationResult(result: ValidationResult): void;
|