vplan 0.1.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/core/index.ts +140 -0
- package/dist/index.js +474 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
- package/runtime/Layout.tsx +17 -0
- package/runtime/components/Callout.tsx +38 -0
- package/runtime/components/Chart.tsx +112 -0
- package/runtime/components/Checklist.tsx +34 -0
- package/runtime/components/Compare.tsx +51 -0
- package/runtime/components/ExpandButton.tsx +20 -0
- package/runtime/components/FileTree.tsx +101 -0
- package/runtime/components/Mermaid.tsx +52 -0
- package/runtime/components/Phase.tsx +34 -0
- package/runtime/components/Questions.tsx +30 -0
- package/runtime/components/validate.ts +21 -0
- package/runtime/css.d.ts +1 -0
- package/runtime/fullscreen.ts +218 -0
- package/runtime/index.html +12 -0
- package/runtime/index.tsx +42 -0
- package/runtime/main.tsx +4 -0
- package/runtime/theme.css +789 -0
- package/runtime/virtual-plan.d.ts +11 -0
package/core/index.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Single source of truth for the VisualPlan component vocabulary.
|
|
5
|
+
*
|
|
6
|
+
* This module is imported by BOTH the browser runtime (for render-time zod
|
|
7
|
+
* validation) and the Node CLI (for static `check` and the `components`
|
|
8
|
+
* catalog printer), so it must stay free of any React, recharts, or mermaid
|
|
9
|
+
* imports. Keep it isomorphic.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const STATUS_VALUES = ['planned', 'active', 'done'] as const
|
|
13
|
+
export const CHANGE_VALUES = ['add', 'modify', 'delete', 'move'] as const
|
|
14
|
+
export const CHART_TYPE_VALUES = ['bar', 'line', 'pie'] as const
|
|
15
|
+
export const CALLOUT_TYPE_VALUES = ['note', 'risk', 'decision', 'warn'] as const
|
|
16
|
+
|
|
17
|
+
export const phaseSchema = z.object({
|
|
18
|
+
title: z.string().min(1, 'title is required'),
|
|
19
|
+
status: z.enum(STATUS_VALUES).default('planned'),
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
export const fileTreeSchema = z.object({
|
|
23
|
+
files: z
|
|
24
|
+
.array(
|
|
25
|
+
z.object({
|
|
26
|
+
path: z.string().min(1, 'each file needs a path'),
|
|
27
|
+
change: z.enum(CHANGE_VALUES),
|
|
28
|
+
}),
|
|
29
|
+
)
|
|
30
|
+
.min(1, 'files must list at least one entry'),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export const chartSchema = z.object({
|
|
34
|
+
type: z.enum(CHART_TYPE_VALUES),
|
|
35
|
+
title: z.string().optional(),
|
|
36
|
+
data: z
|
|
37
|
+
.array(z.object({ label: z.string(), value: z.number() }))
|
|
38
|
+
.min(1, 'data must have at least one point'),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
export const compareSchema = z.object({
|
|
42
|
+
options: z
|
|
43
|
+
.array(
|
|
44
|
+
z.object({
|
|
45
|
+
name: z.string().min(1, 'each option needs a name'),
|
|
46
|
+
pros: z.array(z.string()).default([]),
|
|
47
|
+
cons: z.array(z.string()).default([]),
|
|
48
|
+
pick: z.boolean().optional(),
|
|
49
|
+
}),
|
|
50
|
+
)
|
|
51
|
+
.min(2, 'compare needs at least two options'),
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
export const calloutSchema = z.object({
|
|
55
|
+
type: z.enum(CALLOUT_TYPE_VALUES).default('note'),
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
export const questionsSchema = z.object({
|
|
59
|
+
items: z.array(z.string().min(1)).min(1, 'questions needs at least one item'),
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
export const checklistSchema = z.object({
|
|
63
|
+
title: z.string().optional(),
|
|
64
|
+
items: z
|
|
65
|
+
.array(
|
|
66
|
+
z.object({
|
|
67
|
+
text: z.string().min(1, 'each item needs text'),
|
|
68
|
+
done: z.boolean().default(false),
|
|
69
|
+
}),
|
|
70
|
+
)
|
|
71
|
+
.min(1, 'checklist needs at least one item'),
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
/** Describes a component for the `components` printer and the static checker. */
|
|
75
|
+
export interface CatalogEntry {
|
|
76
|
+
name: string
|
|
77
|
+
summary: string
|
|
78
|
+
/** Props the CLI can statically validate from MDX source (string-literal enums). */
|
|
79
|
+
staticEnums: Record<string, readonly string[]>
|
|
80
|
+
example: string
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const CATALOG: readonly CatalogEntry[] = [
|
|
84
|
+
{
|
|
85
|
+
name: 'Phase',
|
|
86
|
+
summary: 'A collapsible plan stage with a status badge. Wraps markdown.',
|
|
87
|
+
staticEnums: { status: STATUS_VALUES },
|
|
88
|
+
example: '<Phase title="Build the API" status="active">\n 1. Define routes\n</Phase>',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: 'FileTree',
|
|
92
|
+
summary:
|
|
93
|
+
'A nested directory tree of file changes, built from the paths, with add/modify/delete/move markers.',
|
|
94
|
+
staticEnums: {},
|
|
95
|
+
example:
|
|
96
|
+
'<FileTree files={[{ path: "src/api/routes.ts", change: "add" }, { path: "src/api/db.ts", change: "modify" }, { path: "src/legacy.ts", change: "delete" }]} />',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'Chart',
|
|
100
|
+
summary: 'A bar/line/pie chart for estimates or metrics.',
|
|
101
|
+
staticEnums: { type: CHART_TYPE_VALUES },
|
|
102
|
+
example:
|
|
103
|
+
'<Chart type="bar" title="Effort (days)" data={[{ label: "API", value: 3 }, { label: "UI", value: 2 }]} />',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'Compare',
|
|
107
|
+
summary: 'Side-by-side option cards for weighing approaches.',
|
|
108
|
+
staticEnums: {},
|
|
109
|
+
example:
|
|
110
|
+
'<Compare options={[{ name: "Postgres", pros: ["ACID"], cons: ["ops"], pick: true }, { name: "SQLite", pros: ["simple"], cons: ["scale"] }]} />',
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: 'Callout',
|
|
114
|
+
summary: 'A highlighted note/risk/decision/warning block. Wraps markdown.',
|
|
115
|
+
staticEnums: { type: CALLOUT_TYPE_VALUES },
|
|
116
|
+
example: '<Callout type="risk">\n Migration locks the table for ~2s.\n</Callout>',
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: 'Questions',
|
|
120
|
+
summary:
|
|
121
|
+
'Open questions you (Claude) want the reader to weigh in on before building, as a highlighted panel.',
|
|
122
|
+
staticEnums: {},
|
|
123
|
+
example:
|
|
124
|
+
'<Questions items={["Should refresh tokens rotate on every use?", "Is a 15-minute access-token TTL acceptable?"]} />',
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: 'Checklist',
|
|
128
|
+
summary: 'Acceptance criteria / definition of done, with done and todo states.',
|
|
129
|
+
staticEnums: {},
|
|
130
|
+
example:
|
|
131
|
+
'<Checklist title="Done when" items={[{ text: "Returns 429 over the limit", done: true }, { text: "Dashboards live" }]} />',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: 'mermaid (code fence)',
|
|
135
|
+
summary:
|
|
136
|
+
'A flowchart, sequence, state, class, ER, or XY-chart diagram. Write a ```mermaid fenced block. (gantt/pie are not supported by the renderer.)',
|
|
137
|
+
staticEnums: {},
|
|
138
|
+
example: '```mermaid\nflowchart LR\n A[Client] --> B[API] --> C[(DB)]\n```',
|
|
139
|
+
},
|
|
140
|
+
]
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// package.json
|
|
7
|
+
var package_default = {
|
|
8
|
+
name: "vplan",
|
|
9
|
+
version: "0.1.0",
|
|
10
|
+
description: "Render Claude's plans as visual MDX pages instead of walls of text",
|
|
11
|
+
author: "Brandon Burrus <brandon@burrus.io>",
|
|
12
|
+
license: "MIT",
|
|
13
|
+
type: "module",
|
|
14
|
+
engines: {
|
|
15
|
+
node: ">=20.0.0"
|
|
16
|
+
},
|
|
17
|
+
bin: {
|
|
18
|
+
vplan: "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
files: [
|
|
21
|
+
"dist",
|
|
22
|
+
"runtime",
|
|
23
|
+
"core"
|
|
24
|
+
],
|
|
25
|
+
scripts: {
|
|
26
|
+
build: "tsup",
|
|
27
|
+
dev: "tsx src/index.ts",
|
|
28
|
+
typecheck: "tsc --noEmit",
|
|
29
|
+
vendor: "node scripts/vendor.mjs",
|
|
30
|
+
prepack: "node scripts/vendor.mjs && tsup"
|
|
31
|
+
},
|
|
32
|
+
dependencies: {
|
|
33
|
+
"@mdx-js/mdx": "^3.1.1",
|
|
34
|
+
"@mdx-js/react": "^3.1.1",
|
|
35
|
+
"@mdx-js/rollup": "^3.1.1",
|
|
36
|
+
"@tabler/icons-react": "^3.44.0",
|
|
37
|
+
"beautiful-mermaid": "^1.1.3",
|
|
38
|
+
commander: "^15.0.0",
|
|
39
|
+
open: "^11.0.0",
|
|
40
|
+
react: "^19.2.7",
|
|
41
|
+
"react-dom": "^19.2.7",
|
|
42
|
+
recharts: "^3.8.1",
|
|
43
|
+
"rehype-expressive-code": "^0.43.1",
|
|
44
|
+
"remark-frontmatter": "^5.0.0",
|
|
45
|
+
"remark-gfm": "^4.0.1",
|
|
46
|
+
"remark-mdx": "^3.1.1",
|
|
47
|
+
"remark-mdx-frontmatter": "^5.2.0",
|
|
48
|
+
"remark-parse": "^11.0.0",
|
|
49
|
+
unified: "^11.0.5",
|
|
50
|
+
"unist-util-visit": "^5.1.0",
|
|
51
|
+
vite: "^8.0.16",
|
|
52
|
+
"vite-plugin-singlefile": "^2.3.3",
|
|
53
|
+
zod: "^4.4.3"
|
|
54
|
+
},
|
|
55
|
+
devDependencies: {
|
|
56
|
+
"@visualplan/core": "workspace:*",
|
|
57
|
+
"@visualplan/runtime": "workspace:*"
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// src/commands/check.ts
|
|
62
|
+
import { resolve } from "path";
|
|
63
|
+
|
|
64
|
+
// src/build/check.ts
|
|
65
|
+
import { readFile } from "fs/promises";
|
|
66
|
+
import { compile } from "@mdx-js/mdx";
|
|
67
|
+
import remarkFrontmatter from "remark-frontmatter";
|
|
68
|
+
import remarkGfm from "remark-gfm";
|
|
69
|
+
import remarkMdxFrontmatter from "remark-mdx-frontmatter";
|
|
70
|
+
import remarkMdx from "remark-mdx";
|
|
71
|
+
import remarkParse from "remark-parse";
|
|
72
|
+
import { unified } from "unified";
|
|
73
|
+
import { visit } from "unist-util-visit";
|
|
74
|
+
|
|
75
|
+
// ../core/src/index.ts
|
|
76
|
+
import { z } from "zod";
|
|
77
|
+
var STATUS_VALUES = ["planned", "active", "done"];
|
|
78
|
+
var CHANGE_VALUES = ["add", "modify", "delete", "move"];
|
|
79
|
+
var CHART_TYPE_VALUES = ["bar", "line", "pie"];
|
|
80
|
+
var CALLOUT_TYPE_VALUES = ["note", "risk", "decision", "warn"];
|
|
81
|
+
var phaseSchema = z.object({
|
|
82
|
+
title: z.string().min(1, "title is required"),
|
|
83
|
+
status: z.enum(STATUS_VALUES).default("planned")
|
|
84
|
+
});
|
|
85
|
+
var fileTreeSchema = z.object({
|
|
86
|
+
files: z.array(
|
|
87
|
+
z.object({
|
|
88
|
+
path: z.string().min(1, "each file needs a path"),
|
|
89
|
+
change: z.enum(CHANGE_VALUES)
|
|
90
|
+
})
|
|
91
|
+
).min(1, "files must list at least one entry")
|
|
92
|
+
});
|
|
93
|
+
var chartSchema = z.object({
|
|
94
|
+
type: z.enum(CHART_TYPE_VALUES),
|
|
95
|
+
title: z.string().optional(),
|
|
96
|
+
data: z.array(z.object({ label: z.string(), value: z.number() })).min(1, "data must have at least one point")
|
|
97
|
+
});
|
|
98
|
+
var compareSchema = z.object({
|
|
99
|
+
options: z.array(
|
|
100
|
+
z.object({
|
|
101
|
+
name: z.string().min(1, "each option needs a name"),
|
|
102
|
+
pros: z.array(z.string()).default([]),
|
|
103
|
+
cons: z.array(z.string()).default([]),
|
|
104
|
+
pick: z.boolean().optional()
|
|
105
|
+
})
|
|
106
|
+
).min(2, "compare needs at least two options")
|
|
107
|
+
});
|
|
108
|
+
var calloutSchema = z.object({
|
|
109
|
+
type: z.enum(CALLOUT_TYPE_VALUES).default("note")
|
|
110
|
+
});
|
|
111
|
+
var questionsSchema = z.object({
|
|
112
|
+
items: z.array(z.string().min(1)).min(1, "questions needs at least one item")
|
|
113
|
+
});
|
|
114
|
+
var checklistSchema = z.object({
|
|
115
|
+
title: z.string().optional(),
|
|
116
|
+
items: z.array(
|
|
117
|
+
z.object({
|
|
118
|
+
text: z.string().min(1, "each item needs text"),
|
|
119
|
+
done: z.boolean().default(false)
|
|
120
|
+
})
|
|
121
|
+
).min(1, "checklist needs at least one item")
|
|
122
|
+
});
|
|
123
|
+
var CATALOG = [
|
|
124
|
+
{
|
|
125
|
+
name: "Phase",
|
|
126
|
+
summary: "A collapsible plan stage with a status badge. Wraps markdown.",
|
|
127
|
+
staticEnums: { status: STATUS_VALUES },
|
|
128
|
+
example: '<Phase title="Build the API" status="active">\n 1. Define routes\n</Phase>'
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: "FileTree",
|
|
132
|
+
summary: "A nested directory tree of file changes, built from the paths, with add/modify/delete/move markers.",
|
|
133
|
+
staticEnums: {},
|
|
134
|
+
example: '<FileTree files={[{ path: "src/api/routes.ts", change: "add" }, { path: "src/api/db.ts", change: "modify" }, { path: "src/legacy.ts", change: "delete" }]} />'
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: "Chart",
|
|
138
|
+
summary: "A bar/line/pie chart for estimates or metrics.",
|
|
139
|
+
staticEnums: { type: CHART_TYPE_VALUES },
|
|
140
|
+
example: '<Chart type="bar" title="Effort (days)" data={[{ label: "API", value: 3 }, { label: "UI", value: 2 }]} />'
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: "Compare",
|
|
144
|
+
summary: "Side-by-side option cards for weighing approaches.",
|
|
145
|
+
staticEnums: {},
|
|
146
|
+
example: '<Compare options={[{ name: "Postgres", pros: ["ACID"], cons: ["ops"], pick: true }, { name: "SQLite", pros: ["simple"], cons: ["scale"] }]} />'
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: "Callout",
|
|
150
|
+
summary: "A highlighted note/risk/decision/warning block. Wraps markdown.",
|
|
151
|
+
staticEnums: { type: CALLOUT_TYPE_VALUES },
|
|
152
|
+
example: '<Callout type="risk">\n Migration locks the table for ~2s.\n</Callout>'
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: "Questions",
|
|
156
|
+
summary: "Open questions you (Claude) want the reader to weigh in on before building, as a highlighted panel.",
|
|
157
|
+
staticEnums: {},
|
|
158
|
+
example: '<Questions items={["Should refresh tokens rotate on every use?", "Is a 15-minute access-token TTL acceptable?"]} />'
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: "Checklist",
|
|
162
|
+
summary: "Acceptance criteria / definition of done, with done and todo states.",
|
|
163
|
+
staticEnums: {},
|
|
164
|
+
example: '<Checklist title="Done when" items={[{ text: "Returns 429 over the limit", done: true }, { text: "Dashboards live" }]} />'
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: "mermaid (code fence)",
|
|
168
|
+
summary: "A flowchart, sequence, state, class, ER, or XY-chart diagram. Write a ```mermaid fenced block. (gantt/pie are not supported by the renderer.)",
|
|
169
|
+
staticEnums: {},
|
|
170
|
+
example: "```mermaid\nflowchart LR\n A[Client] --> B[API] --> C[(DB)]\n```"
|
|
171
|
+
}
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
// src/build/check.ts
|
|
175
|
+
var COMPONENT_NAMES = CATALOG.filter((entry) => /^[A-Z][A-Za-z0-9]*$/.test(entry.name)).map(
|
|
176
|
+
(entry) => entry.name
|
|
177
|
+
);
|
|
178
|
+
var ENUMS_BY_COMPONENT = new Map(CATALOG.map((entry) => [entry.name, entry.staticEnums]));
|
|
179
|
+
async function checkPlan(mdxPath) {
|
|
180
|
+
const source = await readFile(mdxPath, "utf8");
|
|
181
|
+
const issues = [];
|
|
182
|
+
try {
|
|
183
|
+
await compile(source, {
|
|
184
|
+
remarkPlugins: [
|
|
185
|
+
remarkFrontmatter,
|
|
186
|
+
[remarkMdxFrontmatter, { name: "frontmatter" }],
|
|
187
|
+
remarkGfm
|
|
188
|
+
]
|
|
189
|
+
});
|
|
190
|
+
} catch (error) {
|
|
191
|
+
const vfileError = error;
|
|
192
|
+
return [
|
|
193
|
+
{
|
|
194
|
+
line: vfileError.line ?? 1,
|
|
195
|
+
column: vfileError.column ?? 1,
|
|
196
|
+
message: vfileError.reason ?? vfileError.message ?? "MDX failed to compile"
|
|
197
|
+
}
|
|
198
|
+
];
|
|
199
|
+
}
|
|
200
|
+
const tree = unified().use(remarkParse).use(remarkFrontmatter).use(remarkMdx).parse(source);
|
|
201
|
+
visit(tree, (node) => {
|
|
202
|
+
const element = node;
|
|
203
|
+
if (element.type !== "mdxJsxFlowElement" && element.type !== "mdxJsxTextElement") return;
|
|
204
|
+
const name = element.name;
|
|
205
|
+
if (!name) return;
|
|
206
|
+
const at = element.position?.start ?? { line: 1, column: 1 };
|
|
207
|
+
if (!COMPONENT_NAMES.includes(name)) {
|
|
208
|
+
issues.push({
|
|
209
|
+
line: at.line,
|
|
210
|
+
column: at.column,
|
|
211
|
+
message: `Unknown component <${name}>. Valid components: ${COMPONENT_NAMES.join(", ")}.`
|
|
212
|
+
});
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const enums = ENUMS_BY_COMPONENT.get(name) ?? {};
|
|
216
|
+
for (const [prop, allowed] of Object.entries(enums)) {
|
|
217
|
+
const attribute = element.attributes?.find(
|
|
218
|
+
(candidate) => candidate.type === "mdxJsxAttribute" && candidate.name === prop
|
|
219
|
+
);
|
|
220
|
+
if (attribute && typeof attribute.value === "string" && !allowed.includes(attribute.value)) {
|
|
221
|
+
issues.push({
|
|
222
|
+
line: at.line,
|
|
223
|
+
column: at.column,
|
|
224
|
+
message: `<${name}> prop ${prop}="${attribute.value}" is invalid. Valid: ${allowed.join(", ")}.`
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
return issues;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/commands/check.ts
|
|
233
|
+
function printIssues(file, issues) {
|
|
234
|
+
for (const issue of issues) {
|
|
235
|
+
process.stderr.write(`${file}:${issue.line}:${issue.column} ${issue.message}
|
|
236
|
+
`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
async function runCheck(file) {
|
|
240
|
+
const issues = await checkPlan(resolve(file));
|
|
241
|
+
if (issues.length === 0) {
|
|
242
|
+
process.stdout.write(`${file} is valid
|
|
243
|
+
`);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
printIssues(file, issues);
|
|
247
|
+
process.stdout.write(`
|
|
248
|
+
${issues.length} issue(s) found
|
|
249
|
+
`);
|
|
250
|
+
process.exitCode = 1;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/commands/components.ts
|
|
254
|
+
function runComponents() {
|
|
255
|
+
const lines = [
|
|
256
|
+
"VisualPlan components \u2014 use these directly in a plan .mdx (no imports):",
|
|
257
|
+
""
|
|
258
|
+
];
|
|
259
|
+
for (const entry of CATALOG) {
|
|
260
|
+
lines.push(`${entry.name}`);
|
|
261
|
+
lines.push(` ${entry.summary}`);
|
|
262
|
+
const enums = Object.entries(entry.staticEnums);
|
|
263
|
+
for (const [prop, values] of enums) {
|
|
264
|
+
lines.push(` ${prop}: ${values.join(" | ")}`);
|
|
265
|
+
}
|
|
266
|
+
lines.push(" example:");
|
|
267
|
+
for (const exampleLine of entry.example.split("\n")) {
|
|
268
|
+
lines.push(` ${exampleLine}`);
|
|
269
|
+
}
|
|
270
|
+
lines.push("");
|
|
271
|
+
}
|
|
272
|
+
lines.push("Start the plan with a `# Title` heading. Do not use YAML frontmatter.");
|
|
273
|
+
process.stdout.write(`${lines.join("\n")}
|
|
274
|
+
`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/commands/render.ts
|
|
278
|
+
import { basename, dirname as dirname2, extname, join as join2, resolve as resolve3 } from "path";
|
|
279
|
+
import open from "open";
|
|
280
|
+
|
|
281
|
+
// src/build/compile.ts
|
|
282
|
+
import { cp, mkdtemp, rm } from "fs/promises";
|
|
283
|
+
import { existsSync } from "fs";
|
|
284
|
+
import { createRequire } from "module";
|
|
285
|
+
import { tmpdir } from "os";
|
|
286
|
+
import { dirname, join, resolve as resolve2 } from "path";
|
|
287
|
+
import { fileURLToPath } from "url";
|
|
288
|
+
import mdx from "@mdx-js/rollup";
|
|
289
|
+
import rehypeExpressiveCode from "rehype-expressive-code";
|
|
290
|
+
import remarkFrontmatter2 from "remark-frontmatter";
|
|
291
|
+
import remarkGfm2 from "remark-gfm";
|
|
292
|
+
import remarkMdxFrontmatter2 from "remark-mdx-frontmatter";
|
|
293
|
+
import { build, createServer } from "vite";
|
|
294
|
+
import { viteSingleFile } from "vite-plugin-singlefile";
|
|
295
|
+
|
|
296
|
+
// src/build/remark-mermaid.ts
|
|
297
|
+
import { visit as visit2 } from "unist-util-visit";
|
|
298
|
+
function remarkMermaid() {
|
|
299
|
+
return (tree) => {
|
|
300
|
+
visit2(
|
|
301
|
+
tree,
|
|
302
|
+
"code",
|
|
303
|
+
(node, index, parent) => {
|
|
304
|
+
if (node.lang !== "mermaid" || !parent || index === void 0) return;
|
|
305
|
+
parent.children[index] = {
|
|
306
|
+
type: "mdxJsxFlowElement",
|
|
307
|
+
name: "Mermaid",
|
|
308
|
+
attributes: [{ type: "mdxJsxAttribute", name: "chart", value: node.value }],
|
|
309
|
+
children: []
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
);
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// src/build/compile.ts
|
|
317
|
+
var expressiveCodeOptions = {
|
|
318
|
+
themes: ["github-dark", "github-light"],
|
|
319
|
+
useDarkModeMediaQuery: true,
|
|
320
|
+
// The copy-button script does not execute reliably in our client-rendered SPA;
|
|
321
|
+
// frames (titles) are CSS-only, so keep those and drop the interactive button.
|
|
322
|
+
frames: { showCopyToClipboardButton: false },
|
|
323
|
+
// Match the flat ink design: our borders/radius/surfaces/fonts, no shadow, no
|
|
324
|
+
// colored tab accent. Values are CSS vars so the frame chrome tracks light/dark too.
|
|
325
|
+
styleOverrides: {
|
|
326
|
+
borderRadius: "10px",
|
|
327
|
+
borderColor: "var(--vp-border)",
|
|
328
|
+
codeBackground: "var(--vp-surface)",
|
|
329
|
+
codeFontFamily: "var(--vp-mono)",
|
|
330
|
+
codeFontSize: "0.8rem",
|
|
331
|
+
codeLineHeight: "1.6",
|
|
332
|
+
codePaddingBlock: "0.9rem",
|
|
333
|
+
codePaddingInline: "1rem",
|
|
334
|
+
uiFontFamily: "var(--vp-font)",
|
|
335
|
+
uiFontSize: "0.78rem",
|
|
336
|
+
frames: {
|
|
337
|
+
frameBoxShadowCssValue: "none",
|
|
338
|
+
// A flat filename header on the same surface as the code, separated by one
|
|
339
|
+
// border. No editor-tab metaphor, no colored indicator line.
|
|
340
|
+
editorBackground: "var(--vp-surface)",
|
|
341
|
+
editorTabBarBackground: "var(--vp-surface)",
|
|
342
|
+
editorTabBarBorderBottomColor: "var(--vp-border)",
|
|
343
|
+
editorActiveTabBackground: "var(--vp-surface)",
|
|
344
|
+
editorActiveTabForeground: "var(--vp-muted)",
|
|
345
|
+
editorActiveTabBorderColor: "transparent",
|
|
346
|
+
editorActiveTabIndicatorTopColor: "transparent",
|
|
347
|
+
editorActiveTabIndicatorBottomColor: "transparent",
|
|
348
|
+
editorTabsMarginInlineStart: "0",
|
|
349
|
+
terminalBackground: "var(--vp-surface)",
|
|
350
|
+
terminalTitlebarBackground: "var(--vp-surface)",
|
|
351
|
+
terminalTitlebarForeground: "var(--vp-muted)",
|
|
352
|
+
terminalTitlebarBorderBottomColor: "var(--vp-border)"
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
var require2 = createRequire(import.meta.url);
|
|
357
|
+
function findRuntimePaths() {
|
|
358
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
359
|
+
for (let depth = 0; depth < 6; depth++) {
|
|
360
|
+
const runtimeDir2 = join(dir, "runtime");
|
|
361
|
+
const coreEntry = join(dir, "core", "index.ts");
|
|
362
|
+
if (existsSync(join(runtimeDir2, "index.html")) && existsSync(coreEntry)) {
|
|
363
|
+
return { runtimeDir: runtimeDir2, coreEntry };
|
|
364
|
+
}
|
|
365
|
+
dir = dirname(dir);
|
|
366
|
+
}
|
|
367
|
+
const runtimeDir = dirname(require2.resolve("@visualplan/runtime/package.json"));
|
|
368
|
+
const coreDir = dirname(require2.resolve("@visualplan/core/package.json"));
|
|
369
|
+
return { runtimeDir, coreEntry: join(coreDir, "src", "index.ts") };
|
|
370
|
+
}
|
|
371
|
+
function mdxPlugin() {
|
|
372
|
+
return {
|
|
373
|
+
enforce: "pre",
|
|
374
|
+
...mdx({
|
|
375
|
+
providerImportSource: "@mdx-js/react",
|
|
376
|
+
remarkPlugins: [
|
|
377
|
+
remarkFrontmatter2,
|
|
378
|
+
[remarkMdxFrontmatter2, { name: "frontmatter" }],
|
|
379
|
+
remarkGfm2,
|
|
380
|
+
// Must run before rehype-expressive-code so mermaid never reaches the highlighter.
|
|
381
|
+
remarkMermaid
|
|
382
|
+
],
|
|
383
|
+
rehypePlugins: [[rehypeExpressiveCode, expressiveCodeOptions]]
|
|
384
|
+
})
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
function baseConfig(paths, mdxPath) {
|
|
388
|
+
return {
|
|
389
|
+
root: paths.runtimeDir,
|
|
390
|
+
configFile: false,
|
|
391
|
+
logLevel: "silent",
|
|
392
|
+
resolve: { alias: { "virtual:plan": mdxPath, "@visualplan/core": paths.coreEntry } },
|
|
393
|
+
esbuild: { jsx: "automatic", jsxImportSource: "react" },
|
|
394
|
+
plugins: [mdxPlugin()],
|
|
395
|
+
// The runtime, core, and the user's plan span sibling dirs (and a hoisted
|
|
396
|
+
// node_modules) in the monorepo, so the dev server cannot use a single allow
|
|
397
|
+
// root. This is a local tool rendering the user's own file, so fs is unrestricted.
|
|
398
|
+
server: { fs: { strict: false }, open: false }
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
async function renderToFile(mdxPath, outPath) {
|
|
402
|
+
const paths = findRuntimePaths();
|
|
403
|
+
const absMdx = resolve2(mdxPath);
|
|
404
|
+
const outDir = await mkdtemp(join(tmpdir(), "visualplan-build-"));
|
|
405
|
+
try {
|
|
406
|
+
const config = baseConfig(paths, absMdx);
|
|
407
|
+
await build({
|
|
408
|
+
...config,
|
|
409
|
+
plugins: [...config.plugins ?? [], viteSingleFile()],
|
|
410
|
+
build: {
|
|
411
|
+
outDir,
|
|
412
|
+
emptyOutDir: true,
|
|
413
|
+
rollupOptions: { input: join(paths.runtimeDir, "index.html") }
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
await cp(join(outDir, "index.html"), resolve2(outPath));
|
|
417
|
+
} finally {
|
|
418
|
+
await rm(outDir, { recursive: true, force: true });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
async function startDevServer(mdxPath) {
|
|
422
|
+
const paths = findRuntimePaths();
|
|
423
|
+
const absMdx = resolve2(mdxPath);
|
|
424
|
+
const server = await createServer(baseConfig(paths, absMdx));
|
|
425
|
+
await server.listen();
|
|
426
|
+
const url = server.resolvedUrls?.local[0] ?? `http://localhost:${server.config.server.port ?? 5173}`;
|
|
427
|
+
return {
|
|
428
|
+
url,
|
|
429
|
+
close: () => server.close()
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// src/commands/render.ts
|
|
434
|
+
function defaultOutPath(absMdx) {
|
|
435
|
+
const stem = basename(absMdx, extname(absMdx));
|
|
436
|
+
return join2(dirname2(absMdx), `${stem}.plan.html`);
|
|
437
|
+
}
|
|
438
|
+
async function runRender(file, options) {
|
|
439
|
+
const absMdx = resolve3(file);
|
|
440
|
+
const issues = await checkPlan(absMdx);
|
|
441
|
+
if (issues.length > 0) {
|
|
442
|
+
printIssues(file, issues);
|
|
443
|
+
process.exitCode = 1;
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (options.watch) {
|
|
447
|
+
const server = await startDevServer(absMdx);
|
|
448
|
+
process.stdout.write(
|
|
449
|
+
`VisualPlan watching ${file}
|
|
450
|
+
${server.url}
|
|
451
|
+
(edit the file to hot-reload; Ctrl+C to stop)
|
|
452
|
+
`
|
|
453
|
+
);
|
|
454
|
+
if (options.open !== false) await open(server.url);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const out = options.out ? resolve3(options.out) : defaultOutPath(absMdx);
|
|
458
|
+
await renderToFile(absMdx, out);
|
|
459
|
+
process.stdout.write(`Rendered ${out}
|
|
460
|
+
`);
|
|
461
|
+
if (options.open !== false) await open(out);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// src/index.ts
|
|
465
|
+
var program = new Command("vplan").description("Render Claude plans as visual MDX pages instead of walls of text").version(package_default.version);
|
|
466
|
+
program.command("render", { isDefault: true }).description("Compile a plan .mdx to a self-contained HTML page (default command)").argument("<file>", "the plan .mdx file to render").option("--watch", "start a hot-reloading dev server instead of writing a file").option("--out <path>", "output HTML path (defaults to <file>.plan.html)").option("--no-open", "do not open the result in a browser").action((file, options) => runRender(file, options));
|
|
467
|
+
program.command("check").description("Validate a plan .mdx (compile + component checks) without rendering").argument("<file>", "the plan .mdx file to validate").action((file) => runCheck(file));
|
|
468
|
+
program.command("components").description("Print the available plan components and their props").action(() => runComponents());
|
|
469
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
470
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}
|
|
471
|
+
`);
|
|
472
|
+
process.exitCode = 1;
|
|
473
|
+
});
|
|
474
|
+
//# sourceMappingURL=index.js.map
|