@zseven-w/openpencil 0.7.0 → 0.7.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/dist/openpencil-cli.cjs +495 -26
- package/dist/openpencil-cli.cjs.map +4 -4
- package/package.json +12 -3
package/dist/openpencil-cli.cjs
CHANGED
|
@@ -1344,10 +1344,16 @@ async function pushLiveDocument(doc) {
|
|
|
1344
1344
|
const syncUrl = cachedUrl ?? await getSyncUrl();
|
|
1345
1345
|
if (!syncUrl) return;
|
|
1346
1346
|
try {
|
|
1347
|
+
const body = JSON.stringify({ document: doc });
|
|
1348
|
+
const bodyBytes = new TextEncoder().encode(body).byteLength;
|
|
1347
1349
|
await fetch(`${syncUrl}/api/mcp/document`, {
|
|
1348
1350
|
method: "POST",
|
|
1349
|
-
headers: {
|
|
1350
|
-
|
|
1351
|
+
headers: {
|
|
1352
|
+
"Content-Type": "application/json",
|
|
1353
|
+
"x-openpencil-client-id": "mcp-server:live-canvas",
|
|
1354
|
+
"x-openpencil-body-bytes": String(bodyBytes)
|
|
1355
|
+
},
|
|
1356
|
+
body
|
|
1351
1357
|
});
|
|
1352
1358
|
} catch {
|
|
1353
1359
|
clearSyncUrl();
|
|
@@ -3032,9 +3038,18 @@ var init_design_prompt = __esm({
|
|
|
3032
3038
|
cjk: "cjk-typography",
|
|
3033
3039
|
examples: "examples"
|
|
3034
3040
|
};
|
|
3035
|
-
INTRO = `You are
|
|
3036
|
-
|
|
3037
|
-
|
|
3041
|
+
INTRO = `You are working with OpenPencil, a vector design tool.
|
|
3042
|
+
|
|
3043
|
+
TOOL SELECTION \u2014 match the user's intent:
|
|
3044
|
+
- READ/INSPECT the canvas: batch_get (search nodes, get IDs), snapshot_layout (spatial overview), get_selection (selected nodes)
|
|
3045
|
+
- CREATE new designs: batch_design (DSL, multi-node), insert_node (JSON, single tree) \u2014 both support postProcess=true
|
|
3046
|
+
- MODIFY existing nodes: update_node (change properties), replace_node (swap entirely)
|
|
3047
|
+
- DELETE/REMOVE elements: delete_node (remove by ID) \u2014 always batch_get first to find the correct ID
|
|
3048
|
+
- MOVE/COPY: move_node, copy_node
|
|
3049
|
+
|
|
3050
|
+
IMPORTANT: When the user asks to read, inspect, find, or look at existing content, use batch_get or snapshot_layout \u2014 do NOT create new nodes.
|
|
3051
|
+
When the user asks to delete or remove something, use batch_get to find it, then delete_node \u2014 do NOT create new nodes.
|
|
3052
|
+
|
|
3038
3053
|
Each node must follow the PenNode schema below.`;
|
|
3039
3054
|
DESIGN_TYPE_DETECTION = `DESIGN TYPE DETECTION:
|
|
3040
3055
|
Classify by the design's PURPOSE to choose the correct root frame size \u2014 reason about intent, do not keyword-match:
|
|
@@ -3272,7 +3287,7 @@ async function handleOpenDocument(params) {
|
|
|
3272
3287
|
hasThemes: !!doc.themes && Object.keys(doc.themes).length > 0
|
|
3273
3288
|
},
|
|
3274
3289
|
context: buildDocumentContext(doc),
|
|
3275
|
-
designPrompt: isEmpty ? buildDesignPrompt() : 'Document has existing content. Use batch_design or insert_node with postProcess=true
|
|
3290
|
+
designPrompt: isEmpty ? buildDesignPrompt() : 'Document has existing content. Match your action to the user intent:\n- READ/INSPECT: Use batch_get (search by type/name/ID) or snapshot_layout to see what is on the canvas.\n- DELETE/REMOVE: Use batch_get to find the target node ID, then delete_node to remove it.\n- MODIFY: Use update_node to change properties of existing nodes.\n- ADD NEW: Use batch_design or insert_node with postProcess=true.\nFor complex multi-section designs, use the layered workflow: design_skeleton \u2192 design_content \u2192 design_refine. Call get_design_prompt(section="planning") for layered workflow guide, or get_design_prompt() for full guidelines.'
|
|
3276
3291
|
};
|
|
3277
3292
|
}
|
|
3278
3293
|
function buildDocumentContext(doc) {
|
|
@@ -3676,14 +3691,17 @@ async function handleBatchDesign(params) {
|
|
|
3676
3691
|
const pageId = params.pageId;
|
|
3677
3692
|
const bindings = /* @__PURE__ */ new Map();
|
|
3678
3693
|
const results = [];
|
|
3679
|
-
const
|
|
3694
|
+
const errors = [];
|
|
3695
|
+
const lines = splitOperations(params.operations);
|
|
3680
3696
|
for (const line of lines) {
|
|
3681
3697
|
try {
|
|
3682
3698
|
await executeLine(line, doc, bindings, results, pageId);
|
|
3683
3699
|
} catch (err2) {
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3700
|
+
const preview = line.length > 200 ? `${line.slice(0, 200)}...` : line;
|
|
3701
|
+
errors.push({
|
|
3702
|
+
line: preview,
|
|
3703
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
3704
|
+
});
|
|
3687
3705
|
}
|
|
3688
3706
|
}
|
|
3689
3707
|
let postProcessed = false;
|
|
@@ -3716,14 +3734,56 @@ async function handleBatchDesign(params) {
|
|
|
3716
3734
|
return {
|
|
3717
3735
|
results,
|
|
3718
3736
|
nodeCount: countNodes2(getDocChildren(doc, pageId)),
|
|
3719
|
-
postProcessed: postProcessed || void 0
|
|
3737
|
+
postProcessed: postProcessed || void 0,
|
|
3738
|
+
errors: errors.length > 0 ? errors : void 0
|
|
3720
3739
|
};
|
|
3721
3740
|
}
|
|
3741
|
+
function splitOperations(raw) {
|
|
3742
|
+
const result = [];
|
|
3743
|
+
let buf = "";
|
|
3744
|
+
let depth = 0;
|
|
3745
|
+
let inString = false;
|
|
3746
|
+
let escape = false;
|
|
3747
|
+
for (let i = 0; i < raw.length; i++) {
|
|
3748
|
+
const ch = raw[i];
|
|
3749
|
+
buf += ch;
|
|
3750
|
+
if (escape) {
|
|
3751
|
+
escape = false;
|
|
3752
|
+
continue;
|
|
3753
|
+
}
|
|
3754
|
+
if (ch === "\\" && inString) {
|
|
3755
|
+
escape = true;
|
|
3756
|
+
continue;
|
|
3757
|
+
}
|
|
3758
|
+
if (ch === '"') {
|
|
3759
|
+
inString = !inString;
|
|
3760
|
+
continue;
|
|
3761
|
+
}
|
|
3762
|
+
if (inString) continue;
|
|
3763
|
+
if (ch === "(" || ch === "[" || ch === "{") depth++;
|
|
3764
|
+
else if (ch === ")" || ch === "]" || ch === "}") depth--;
|
|
3765
|
+
else if (ch === "\n" && depth === 0) {
|
|
3766
|
+
const trimmed = buf.trim();
|
|
3767
|
+
if (trimmed && !trimmed.startsWith("//")) result.push(trimmed);
|
|
3768
|
+
buf = "";
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
const tail = buf.trim();
|
|
3772
|
+
if (tail && !tail.startsWith("//")) result.push(tail);
|
|
3773
|
+
return result;
|
|
3774
|
+
}
|
|
3722
3775
|
async function executeLine(line, doc, bindings, results, pageId) {
|
|
3723
3776
|
const assignMatch = line.match(/^(\w+)\s*=\s*([ICRMG])\((.+)\)$/);
|
|
3777
|
+
const bindlessAssignMatch = !assignMatch && line.match(/^([ICRG])\((.+)\)$/);
|
|
3724
3778
|
const callMatch = line.match(/^([UDM])\((.+)\)$/);
|
|
3725
|
-
|
|
3726
|
-
|
|
3779
|
+
const effectiveAssign = assignMatch ?? (bindlessAssignMatch ? [
|
|
3780
|
+
line,
|
|
3781
|
+
`_auto_${results.length}_${bindlessAssignMatch[1]}`,
|
|
3782
|
+
bindlessAssignMatch[1],
|
|
3783
|
+
bindlessAssignMatch[2]
|
|
3784
|
+
] : null);
|
|
3785
|
+
if (effectiveAssign) {
|
|
3786
|
+
const [, binding, op, argsStr] = effectiveAssign;
|
|
3727
3787
|
switch (op) {
|
|
3728
3788
|
case "I": {
|
|
3729
3789
|
const { parent, data } = parseInsertArgs(argsStr, bindings);
|
|
@@ -3993,18 +4053,73 @@ function applyDescendantOverrides(node, descendants) {
|
|
|
3993
4053
|
}
|
|
3994
4054
|
function parseJsonArg2(str) {
|
|
3995
4055
|
const trimmed = str.trim();
|
|
4056
|
+
let parsed;
|
|
3996
4057
|
try {
|
|
3997
|
-
|
|
4058
|
+
parsed = JSON.parse(trimmed);
|
|
3998
4059
|
} catch {
|
|
3999
4060
|
}
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4061
|
+
if (parsed === void 0) {
|
|
4062
|
+
let normalized = trimmed;
|
|
4063
|
+
normalized = normalized.replace(/(?<=\{|,)\s*(\w+)\s*:/g, ' "$1":');
|
|
4064
|
+
normalized = replaceSingleQuoteDelimiters(normalized);
|
|
4065
|
+
normalized = normalized.replace(/,\s*""\s*:\s*[^,}\]]+/g, "");
|
|
4066
|
+
normalized = normalized.replace(/,(\s*[}\]])/g, "$1");
|
|
4067
|
+
try {
|
|
4068
|
+
parsed = JSON.parse(normalized);
|
|
4069
|
+
} catch (err2) {
|
|
4070
|
+
const snippet = str.slice(0, 300);
|
|
4071
|
+
throw new Error(
|
|
4072
|
+
`Failed to parse JSON (${err2 instanceof Error ? err2.message : "unknown"}): ${snippet}${str.length > 300 ? "..." : ""}`
|
|
4073
|
+
);
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
4076
|
+
return sanitizeObject(normalizeNodeShape(parsed));
|
|
4077
|
+
}
|
|
4078
|
+
function normalizeNodeShape(input) {
|
|
4079
|
+
if (Array.isArray(input)) return input.map(normalizeNodeShape);
|
|
4080
|
+
if (!input || typeof input !== "object") return input;
|
|
4081
|
+
const obj = input;
|
|
4082
|
+
if ("fill" in obj) {
|
|
4083
|
+
obj.fill = normalizeFillField(obj.fill);
|
|
4084
|
+
}
|
|
4085
|
+
if ("stroke" in obj) {
|
|
4086
|
+
obj.stroke = normalizeStrokeField(obj.stroke);
|
|
4087
|
+
}
|
|
4088
|
+
if (Array.isArray(obj.children)) {
|
|
4089
|
+
obj.children = obj.children.map((c) => normalizeNodeShape(c));
|
|
4090
|
+
}
|
|
4091
|
+
return obj;
|
|
4092
|
+
}
|
|
4093
|
+
function normalizeFillField(value) {
|
|
4094
|
+
if (value == null) return value;
|
|
4095
|
+
if (typeof value === "string") {
|
|
4096
|
+
return [{ type: "solid", color: value }];
|
|
4097
|
+
}
|
|
4098
|
+
if (typeof value === "object" && !Array.isArray(value)) {
|
|
4099
|
+
return [value];
|
|
4007
4100
|
}
|
|
4101
|
+
return value;
|
|
4102
|
+
}
|
|
4103
|
+
function normalizeStrokeField(value) {
|
|
4104
|
+
if (value == null) return value;
|
|
4105
|
+
if (typeof value === "string") {
|
|
4106
|
+
return { thickness: 1, fill: [{ type: "solid", color: value }] };
|
|
4107
|
+
}
|
|
4108
|
+
if (typeof value !== "object") return value;
|
|
4109
|
+
const stroke = value;
|
|
4110
|
+
if (stroke.color != null && stroke.fill == null) {
|
|
4111
|
+
stroke.fill = [{ type: "solid", color: stroke.color }];
|
|
4112
|
+
delete stroke.color;
|
|
4113
|
+
}
|
|
4114
|
+
if (stroke.fill != null) {
|
|
4115
|
+
stroke.fill = normalizeFillField(stroke.fill);
|
|
4116
|
+
}
|
|
4117
|
+
if (stroke.type != null && stroke.color != null && stroke.fill == null) {
|
|
4118
|
+
stroke.fill = [{ type: stroke.type, color: stroke.color }];
|
|
4119
|
+
delete stroke.type;
|
|
4120
|
+
delete stroke.color;
|
|
4121
|
+
}
|
|
4122
|
+
return stroke;
|
|
4008
4123
|
}
|
|
4009
4124
|
function replaceSingleQuoteDelimiters(str) {
|
|
4010
4125
|
const chars = [];
|
|
@@ -11870,8 +11985,8 @@ function mapStrokeAlign(align) {
|
|
|
11870
11985
|
return void 0;
|
|
11871
11986
|
}
|
|
11872
11987
|
}
|
|
11873
|
-
function mapStrokeJoin(
|
|
11874
|
-
switch (
|
|
11988
|
+
function mapStrokeJoin(join6) {
|
|
11989
|
+
switch (join6) {
|
|
11875
11990
|
case "MITER":
|
|
11876
11991
|
return "miter";
|
|
11877
11992
|
case "BEVEL":
|
|
@@ -13266,6 +13381,335 @@ var init_import = __esm({
|
|
|
13266
13381
|
}
|
|
13267
13382
|
});
|
|
13268
13383
|
|
|
13384
|
+
// apps/cli/src/commands/skill-bundle.json
|
|
13385
|
+
var skill_bundle_default;
|
|
13386
|
+
var init_skill_bundle = __esm({
|
|
13387
|
+
"apps/cli/src/commands/skill-bundle.json"() {
|
|
13388
|
+
skill_bundle_default = {
|
|
13389
|
+
version: "0.7.0",
|
|
13390
|
+
files: {
|
|
13391
|
+
"skills/openpencil-design/SKILL.md": '---\nname: openpencil-design\ndescription: Use when designing UI with OpenPencil \u2014 creating layouts via op CLI, batch design DSL, or MCP tools. Covers PenNode schema, semantic roles, typography, color, spacing, and common component patterns.\n---\n\n# OpenPencil Design\n\nGenerate production-quality vector designs by writing PenNode JSON trees. Use the `op` CLI or MCP tools to create, read, update, and delete nodes on the OpenPencil canvas.\n\n## When to Use\n\n- Creating or modifying UI designs in `.op` files\n- Using the `op` CLI to script design operations\n- Designing via MCP tools (`batch_design`, `insert_node`, `design_skeleton`)\n- Need reference for PenNode schema, roles, or layout rules\n\n## Quick Reference \u2014 `op` CLI\n\n```bash\n# App control\nop start [--desktop|--web] # Launch app\nop stop # Stop running instance\nop status # Check if running\n\n# Document\nop open [file.op] # Open file or connect to live canvas\nop save <file.op> # Save current document\nop get [--depth N] [--pretty] # Get document tree\nop selection [--depth N] # Get current canvas selection\nop read-nodes [id...] [--depth N] [--vars] # Read node subtree(s) with optional variable resolution\nop layout [--parent P] [--depth N] # Snapshot layout tree with computed positions\nop find-space [--direction D] [--width N] [--height N] # Find empty space on canvas\n\n# Node operations\nop insert \'<json>\' [--parent P] # Insert node (--index N, --post-process)\nop update <id> \'<json>\' # Update node\nop delete <id> # Delete node\nop move <id> <parent> [index] # Move node\nop copy <id> <parent> # Deep-copy node\nop replace <id> \'<json>\' # Replace node\n\n# Batch design\nop design \'<dsl>\' # Batch design DSL (inline, @file, or stdin) [--canvas-width N]\n\n# Layered workflow\nop design:skeleton \'<json>\' # Create section structure\nop design:content <id> \'<json>\' # Populate section content\nop design:refine --root-id <id> # Validate + auto-fix (resolves icons) [--canvas-width N]\n\n# Import\nop import:svg <file.svg> [--parent P] # Import SVG as editable nodes\nop import:figma <file.fig> [--out out.op] # Convert Figma .fig to .op document\n\n# Pages\nop page list # List all pages\nop page add [--name N] # Add a new page\nop page remove <id> # Remove a page\nop page rename <id> \'<name>\' # Rename a page\nop page reorder <id> <index> # Move page to position\nop page duplicate <id> # Clone page with new IDs\n\n# Variables & Themes\nop vars / op vars:set \'<json>\' # Variables (--replace to replace all)\nop themes / op themes:set \'<json>\' # Themes (--replace to replace all)\nop theme:save <file.optheme> # Save current theme as preset file\nop theme:load <file.optheme> # Load a theme preset file\nop theme:list <directory> # List .optheme presets in directory\n\n# Codegen pipeline\nop codegen:plan \'<json>\' # Submit codegen plan (framework, rootIds, options)\nop codegen:submit \'<json>\' # Submit a code chunk for a node\nop codegen:assemble [--framework F] # Assemble all submitted chunks into final output\nop codegen:clean # Clear codegen state\n```\n\nGlobal flags: `--file <path>`, `--page <id>`, `--pretty`. Inputs: inline string, `@filepath`, or `-` (stdin).\n\n## Building Designs \u2014 Two Approaches\n\n### Approach 1: `op insert` (Recommended)\n\nThe most reliable way to build designs. Use `--parent` to specify the parent node. Capture the returned `nodeId` to reference later. **Always finish with `design:refine`** to resolve icons and validate layout.\n\n```bash\n# Create root frame, capture its ID\nROOT=$(op insert \'{"type":"frame","name":"Page","width":375,"height":812,"layout":"vertical"}\' \\\n | python3 -c "import sys,json; print(json.load(sys.stdin)[\'nodeId\'])")\n\n# Insert children using --parent\nop insert --parent "$ROOT" \'{"type":"text","content":"Hello","fontSize":28,"fontWeight":700}\'\n\n# Post-process: resolve icons, validate layout\nop design:refine --root-id "$ROOT"\n```\n\n### Approach 2: Batch Design DSL\n\nOne operation per line. Bind results with `name=` for later reference. Best for simple, flat structures.\n\n> **Limitation:** The DSL parser cannot handle deeply nested JSON (e.g., `children` arrays with nested objects, or multiple levels of array nesting). Keep each `I()` call to a **single level of nesting**. For complex nodes with children, use separate `I()` calls for parent and children, or use `op insert --parent`.\n\n```\nroot=I(null, { "type": "frame", "width": 1200, "layout": "vertical" })\nnav=I(root, { "type": "frame", "role": "navbar", "height": 72 })\nU(nav, { "fill": [{"type": "solid", "color": "#FFFFFF"}] })\ncard2=C(card1, grid, { "name": "Card 2" })\nM(sidebar, main, 0)\nD(old_section)\nR(old_btn, { "type": "rectangle", "role": "button" })\n```\n\n| Op | Syntax | Action |\n|----|--------|--------|\n| `I` | `name=I(parent, { node })` | Insert |\n| `U` | `U(ref, { updates })` | Update |\n| `C` | `name=C(source, parent, { overrides })` | Copy |\n| `R` | `name=R(ref, { node })` | Replace |\n| `M` | `M(ref, parent, index?)` | Move |\n| `D` | `D(ref)` | Delete |\n| `G` | `name=G(parent, "search", "query")` | Generate image via search |\n\n**DSL safe pattern** \u2014 always insert parent and children separately:\n\n```\nbtn=I(form, {"type":"rectangle","role":"button","width":"fill_container","height":50,"cornerRadius":12,"fill":[{"type":"solid","color":"#111111"}],"layout":"horizontal","justifyContent":"center","alignItems":"center"})\nI(btn, {"type":"text","content":"Submit","fontSize":16,"fontWeight":600,"fill":[{"type":"solid","color":"#FFFFFF"}]})\n```\n\n## PenNode Schema\n\n### Common Properties\n\n```json\n{\n "type": "frame|rectangle|text|ellipse|line|polygon|path|image|icon_font|group|ref",\n "name": "Display Name",\n "role": "semantic-role",\n "x": 0, "y": 0,\n "rotation": 0, "opacity": 1, "visible": true\n}\n```\n\n### Container Properties (frame, rectangle, group, ellipse)\n\n```json\n{\n "width": 400, // number | "fill_container" | "fit_content"\n "height": 300,\n "layout": "vertical", // "none" | "vertical" | "horizontal"\n "gap": 16,\n "padding": [16, 24], // number | [v, h] | [top, right, bottom, left]\n "justifyContent": "center", // "start" | "center" | "end" | "space_between" | "space_around"\n "alignItems": "center", // "start" | "center" | "end"\n "clipContent": true,\n "cornerRadius": 12, // number | [tl, tr, br, bl]\n "fill": [{ "type": "solid", "color": "#FFFFFF" }],\n "stroke": { "thickness": 1, "fill": [{ "type": "solid", "color": "#E5E7EB" }], "align": "inside", "dashPattern": [5, 3] },\n "effects": [{ "type": "shadow", "offsetX": 0, "offsetY": 4, "blur": 12, "spread": 0, "color": "rgba(0,0,0,0.08)" }],\n "children": []\n}\n```\n\n### Text\n\n```json\n{\n "type": "text",\n "content": "Hello", // string or StyledTextSegment[]\n "fontSize": 16, "fontFamily": "Inter", "fontWeight": 600,\n "textAlign": "center", // "left" | "center" | "right"\n "textGrowth": "fixed-width", // "auto" | "fixed-width" | "fixed-width-height"\n "lineHeight": 1.5, "letterSpacing": 0,\n "fill": [{ "type": "solid", "color": "#111111" }]\n}\n```\n\nRich text: `"content": [{ "text": "Bold ", "fontWeight": "bold" }, { "text": "normal" }]`\n\n### Path (Icons)\n\n```json\n{ "type": "path", "name": "HeartIcon", "width": 24, "height": 24,\n "fill": [{ "type": "solid", "color": "#111111" }] }\n```\n\nPascalCase + "Icon" suffix. Auto-resolved from Lucide set. Common: `SearchIcon`, `MenuIcon`, `HomeIcon`, `UserIcon`, `SettingsIcon`, `MailIcon`, `HeartIcon`, `StarIcon`, `CheckIcon`, `XIcon`, `ChevronRightIcon`, `ArrowRightIcon`, `ZapIcon`, `ShieldIcon`, `CodeIcon`, `LockIcon`, `SparklesIcon`, `PlayIcon`, `BellIcon`, `EyeIcon`, `DownloadIcon`, `PlusIcon`, `GlobeIcon`, `LayersIcon`.\n\n> **Icon rendering requires post-processing.** After inserting path nodes, you MUST run `op design:refine --root-id <id>` or use `op insert --post-process` to resolve icon names into actual SVG paths. Without this step, icons will exist in the tree but not render visually. Lucide icons use stroke rendering \u2014 the engine will clear `fill` and set `stroke` automatically during post-processing.\n\n### Image\n\n```json\n{ "type": "image", "src": "https://example.com/photo.jpg", "width": 400, "height": 300,\n "objectFit": "crop", "cornerRadius": 12 }\n```\n\nAI image placeholders (resolved by `design:refine`):\n\n```json\n{ "type": "image", "width": 400, "height": 300,\n "imagePrompt": "A modern office workspace with natural light",\n "imageSearchQuery": "modern office workspace" }\n```\n\nImage adjustments (all -100 to 100): `exposure`, `contrast`, `saturation`, `temperature`, `tint`, `highlights`, `shadows`.\n\n### Polygon\n\n```json\n{ "type": "polygon", "polygonCount": 6, "width": 80, "height": 80, "cornerRadius": 4,\n "fill": [{ "type": "solid", "color": "#6366F1" }] }\n```\n\n### Icon Font\n\n```json\n{ "type": "icon_font", "iconFontName": "lucide:home", "width": 24, "height": 24,\n "fill": [{ "type": "solid", "color": "#111111" }] }\n```\n\n### Line\n\n```json\n{ "type": "line", "x2": 200, "y2": 0,\n "stroke": { "thickness": 1, "fill": [{ "type": "solid", "color": "#E5E7EB" }] } }\n```\n\n### Fill Types\n\n```json\n{ "type": "solid", "color": "#3B82F6" }\n{ "type": "linear_gradient", "angle": 135,\n "stops": [{ "offset": 0, "color": "#6366F1" }, { "offset": 1, "color": "#8B5CF6" }] }\n{ "type": "radial_gradient", "cx": 0.5, "cy": 0.5, "radius": 0.5,\n "stops": [{ "offset": 0, "color": "#FFF" }, { "offset": 1, "color": "#000" }] }\n{ "type": "image", "url": "https://example.com/texture.jpg", "mode": "fill" }\n```\n\nImage fill modes: `fill`, `fit`, `crop`, `tile`, `stretch`. Image fill also supports adjustment filters (`exposure`, `contrast`, `saturation`, etc.).\n\n### Ref Node (Component Instance)\n\n```json\n{ "type": "ref", "ref": "reusable-frame-id",\n "descendants": { "child-id": { "content": "Override text" } } }\n```\n\nReferences a `frame` with `reusable: true`. Override specific descendant properties via `descendants`.\n\n### Design Variables\n\nReference with `$` prefix: `"color": "$primaryColor"`, `"gap": "$spacing"`.\n\n## Semantic Roles\n\nRoles declare intent \u2014 the engine applies smart defaults. Always prefer roles over manual styling.\n\n| Category | Roles |\n|----------|-------|\n| **Layout** | `section`, `row`, `column`, `centered-content`, `divider`, `spacer` |\n| **Navigation** | `navbar`, `nav-links`, `nav-link` |\n| **Interactive** | `button`, `icon-button`, `badge`, `tag`, `pill`, `input`, `form-input`, `search-bar` |\n| **Cards** | `card`, `feature-card`, `stat-card`, `pricing-card`, `image-card` |\n| **Content** | `hero`, `feature-grid`, `cta-section`, `footer`, `testimonial`, `stats-section` |\n| **Typography** | `heading`, `subheading`, `body-text`, `caption`, `label` |\n| **Media** | `avatar`, `icon`, `phone-mockup`, `screenshot-frame` |\n| **Table** | `table`, `table-row`, `table-header`, `table-cell` |\n| **Form** | `form-group` |\n\nKey defaults:\n- `navbar` \u2192 height: 56-72, horizontal, space_between, center-aligned\n- `button` \u2192 padding: [12, 24], cornerRadius: 8, centered\n- `card` \u2192 vertical, gap: 12, cornerRadius: 12, padding: 24\n- `heading` \u2192 lineHeight: 1.2, letterSpacing: -0.5\n- `body-text` \u2192 fill_container, textGrowth: fixed-width, lineHeight: 1.5\n\n## Layout Rules\n\n1. **NEVER set x/y on children inside layout containers** \u2014 engine positions them\n2. **Siblings must use same width strategy** \u2014 all `fill_container` or all fixed\n3. **NEVER `fill_container` inside `fit_content` parent** \u2014 circular dependency\n4. Cards in horizontal row: ALL `width: "fill_container"`, `height: "fill_container"`\n\n### Sizing Decision\n\n| Question | Answer |\n|----------|--------|\n| Stretch to fill? | `"fill_container"` |\n| Shrink to content? | `"fit_content"` |\n| Exact size? | number (px) |\n\n### Design Type Sizing\n\n| Type | Width | Height |\n|------|-------|--------|\n| Landing page | 1200 | 0 (auto) |\n| Mobile screen | 375 | 812 |\n| Dashboard | 1200 | 0 (auto) |\n\n## Design Principles\n\n### Typography\n\n```\nDisplay: 40-56px 700 letterSpacing: -1.5 lineHeight: 1.1 "Space Grotesk"\nHeading: 28-36px 700 letterSpacing: -0.5 lineHeight: 1.2 "Space Grotesk"\nSubheading: 20-24px 600 letterSpacing: -0.25 lineHeight: 1.3 "Space Grotesk"\nBody: 15-18px 400 letterSpacing: 0 lineHeight: 1.5 "Inter"\nCaption: 13-14px 400 letterSpacing: 0 lineHeight: 1.4 "Inter"\n```\n\nCJK: use `"Noto Sans SC/JP/KR"`, lineHeight >= 1.3, letterSpacing: 0 always.\n\n### Color\n\n```\nPrimary text: #111111 Secondary: #6B7280 Subtle: #9CA3AF\nBackground: #FFFFFF Surface: #F9FAFB Border: #E5E7EB\n```\n\nMax 2 saturated colors. WCAG AA: 4.5:1 body, 3:1 large. Dark bg: `#0F172A`, not `#000000`.\n\n### Spacing (8px grid)\n\n```\nRelated: 8-16px Components: 16-24px\nGroups: 24-32px Sections: 48-80px Page padding: 80px\n```\n\n### Shadows\n\n```json\n// Subtle (cards)\n{ "type": "shadow", "offsetY": 1, "blur": 3, "color": "rgba(0,0,0,0.05)" }\n// Medium (dropdowns)\n{ "type": "shadow", "offsetY": 4, "blur": 12, "color": "rgba(0,0,0,0.08)" }\n// Elevated (modals)\n{ "type": "shadow", "offsetY": 8, "blur": 24, "spread": -4, "color": "rgba(0,0,0,0.12)" }\n```\n\n### Copy Rules\n\nHeadlines: 2-6 words. Subtitles: max 15 words. Buttons: 1-3 words. No lorem ipsum. No emoji as icons.\n\n## Layered Workflow\n\nFor complex multi-section pages, use the three-step skeleton \u2192 content \u2192 refine flow:\n\n| Step | MCP Tool | CLI Equivalent |\n|------|----------|----------------|\n| 1. Create section structure | `design_skeleton` | `op design:skeleton \'<json>\'` |\n| 2. Populate each section | `design_content` (with `postProcess: true`) | `op design:content <section-id> \'<json>\'` |\n| 3. Validate + auto-fix | `design_refine` | `op design:refine --root-id <id>` |\n\n`design:refine` resolves icon names \u2192 SVG paths, fixes layout issues, and validates the tree. **Always run as the final step.**\n\n## Codegen Pipeline\n\nFor incremental, framework-aware code generation from the design tree:\n\n| Step | CLI Command | MCP Tool | Description |\n|------|------------|----------|-------------|\n| 1. Plan | `op codegen:plan \'<json>\'` | `codegen_plan` | Declare framework, root node IDs, and options |\n| 2. Submit | `op codegen:submit \'<json>\'` | `codegen_submit_chunk` | Submit generated code for individual nodes |\n| 3. Assemble | `op codegen:assemble --framework react` | `codegen_assemble` | Combine all chunks into the final output |\n| 4. Clean | `op codegen:clean` | `codegen_clean` | Clear server-side codegen state |\n\nThe plan JSON shape:\n```json\n{ "framework": "react", "rootIds": ["frame-1"], "options": { "tailwind": true } }\n```\n\nThe submit JSON shape:\n```json\n{ "nodeId": "card-1", "code": "<Card className=\\"...\\">...</Card>", "imports": ["Card"] }\n```\n\nSupported frameworks: `react`, `html`, `vue`, `svelte`, `flutter`, `swiftui`, `compose`, `rn` (React Native), `css`.\n\n## Multi-Page Documents\n\n```bash\nop page list # List all pages with IDs\nop page add --name "Settings" # Add a new page\nop page remove <page-id> # Remove a page\nop page rename <page-id> \'New Name\' # Rename a page\nop page reorder <page-id> 2 # Move page to index 2\nop page duplicate <page-id> # Clone page with new IDs\n```\n\nUse `--page <id>` on any command to target a specific page. Without it, commands operate on the first page.\n\n## Common Patterns\n\nPatterns below show `op insert --parent` commands. Each pattern is copy-paste ready.\n\n### Navbar\n\n```bash\nNAV=$(op insert --parent "$ROOT" \'{"type":"frame","role":"navbar","width":"fill_container","height":72,"layout":"horizontal","padding":[0,80],"justifyContent":"space_between","alignItems":"center","fill":[{"type":"solid","color":"#FFFFFF"}],"stroke":{"thickness":1,"fill":[{"type":"solid","color":"#F3F4F6"}]}}\' | ID)\nop insert --parent "$NAV" \'{"type":"text","content":"Brand","fontSize":20,"fontWeight":700,"fontFamily":"Space Grotesk"}\'\nLINKS=$(op insert --parent "$NAV" \'{"type":"frame","role":"nav-links","layout":"horizontal","gap":32,"width":"fit_content","height":"fit_content"}\' | ID)\nop insert --parent "$LINKS" \'{"type":"text","role":"nav-link","content":"Features","fontSize":15}\'\nop insert --parent "$LINKS" \'{"type":"text","role":"nav-link","content":"Pricing","fontSize":15}\'\nCTA=$(op insert --parent "$NAV" \'{"type":"rectangle","role":"button","padding":[10,24],"cornerRadius":8,"fill":[{"type":"solid","color":"#111111"}],"layout":"horizontal","justifyContent":"center","alignItems":"center"}\' | ID)\nop insert --parent "$CTA" \'{"type":"text","content":"Get Started","fontSize":14,"fontWeight":600,"fill":[{"type":"solid","color":"#FFFFFF"}]}\'\n```\n\n### Hero\n\n```bash\nHERO=$(op insert --parent "$ROOT" \'{"type":"frame","role":"hero","width":"fill_container","height":"fit_content","layout":"vertical","padding":[100,80],"gap":24,"alignItems":"center"}\' | ID)\nop insert --parent "$HERO" \'{"type":"text","role":"heading","content":"Build something great","fontSize":56,"fontWeight":700,"fontFamily":"Space Grotesk","textAlign":"center","letterSpacing":-1.5,"lineHeight":1.1,"textGrowth":"fixed-width","width":800}\'\nop insert --parent "$HERO" \'{"type":"text","role":"subheading","content":"The modern platform for teams who ship fast.","fontSize":18,"textAlign":"center","lineHeight":1.6,"textGrowth":"fixed-width","width":560,"fill":[{"type":"solid","color":"#6B7280"}]}\'\nBTNS=$(op insert --parent "$HERO" \'{"type":"frame","layout":"horizontal","gap":12,"width":"fit_content","height":"fit_content"}\' | ID)\nB1=$(op insert --parent "$BTNS" \'{"type":"rectangle","role":"button","padding":[14,32],"cornerRadius":10,"fill":[{"type":"solid","color":"#111111"}],"layout":"horizontal","justifyContent":"center","alignItems":"center"}\' | ID)\nop insert --parent "$B1" \'{"type":"text","content":"Start Free","fontSize":16,"fontWeight":600,"fill":[{"type":"solid","color":"#FFFFFF"}]}\'\nB2=$(op insert --parent "$BTNS" \'{"type":"rectangle","role":"button","padding":[14,32],"cornerRadius":10,"fill":[{"type":"solid","color":"#F3F4F6"}],"layout":"horizontal","justifyContent":"center","alignItems":"center"}\' | ID)\nop insert --parent "$B2" \'{"type":"text","content":"View Demo","fontSize":16,"fontWeight":600}\'\n```\n\n### Feature Card (in horizontal grid, ALL cards must use fill_container)\n\n```bash\nCARD=$(op insert --parent "$GRID" \'{"type":"rectangle","role":"feature-card","width":"fill_container","height":"fill_container","layout":"vertical","padding":28,"gap":16,"cornerRadius":16,"fill":[{"type":"solid","color":"#F9FAFB"}]}\' | ID)\nop insert --parent "$CARD" \'{"type":"path","name":"ZapIcon","width":24,"height":24,"fill":[{"type":"solid","color":"#111111"}]}\'\nop insert --parent "$CARD" \'{"type":"text","content":"Lightning Fast","fontSize":20,"fontWeight":600}\'\nop insert --parent "$CARD" \'{"type":"text","role":"body-text","content":"Sub-second builds with smart caching.","fontSize":15,"lineHeight":1.6,"fill":[{"type":"solid","color":"#6B7280"}]}\'\n```\n\n### Form Input\n\n```bash\nGRP=$(op insert --parent "$FORM" \'{"type":"frame","role":"form-group","layout":"vertical","gap":8,"width":"fill_container"}\' | ID)\nop insert --parent "$GRP" \'{"type":"text","role":"label","content":"Email","fontSize":14,"fontWeight":500}\'\nINP=$(op insert --parent "$GRP" \'{"type":"rectangle","role":"form-input","width":"fill_container","height":48,"cornerRadius":10,"layout":"horizontal","padding":[0,16],"gap":10,"alignItems":"center","fill":[{"type":"solid","color":"#F9FAFB"}],"stroke":{"thickness":1,"fill":[{"type":"solid","color":"#E5E7EB"}]}}\' | ID)\nop insert --parent "$INP" \'{"type":"path","name":"MailIcon","width":18,"height":18,"fill":[{"type":"solid","color":"#9CA3AF"}]}\'\nop insert --parent "$INP" \'{"type":"text","content":"you@example.com","fontSize":15,"fill":[{"type":"solid","color":"#9CA3AF"}]}\'\n```\n\n### Footer\n\n```bash\nFOOTER=$(op insert --parent "$ROOT" \'{"type":"frame","role":"footer","width":"fill_container","height":"fit_content","layout":"horizontal","padding":[48,80],"gap":80,"fill":[{"type":"solid","color":"#F9FAFB"}]}\' | ID)\nCOL1=$(op insert --parent "$FOOTER" \'{"type":"frame","layout":"vertical","gap":16,"width":240}\' | ID)\nop insert --parent "$COL1" \'{"type":"text","content":"Brand","fontSize":20,"fontWeight":700,"fontFamily":"Space Grotesk"}\'\nop insert --parent "$COL1" \'{"type":"text","content":"Building the future of design.","fontSize":14,"lineHeight":1.6,"fill":[{"type":"solid","color":"#6B7280"}]}\'\nCOL2=$(op insert --parent "$FOOTER" \'{"type":"frame","layout":"vertical","gap":12,"width":"fit_content"}\' | ID)\nop insert --parent "$COL2" \'{"type":"text","content":"Product","fontSize":14,"fontWeight":600}\'\nop insert --parent "$COL2" \'{"type":"text","content":"Features","fontSize":14,"fill":[{"type":"solid","color":"#6B7280"}]}\'\nop insert --parent "$COL2" \'{"type":"text","content":"Pricing","fontSize":14,"fill":[{"type":"solid","color":"#6B7280"}]}\'\n```\n\n## Common Mistakes\n\n| Mistake | Fix |\n|---------|-----|\n| Setting x/y inside layout container | Remove x/y \u2014 engine auto-positions |\n| Cards with different width strategies | All siblings: same sizing (`fill_container`) |\n| `fill_container` child in `fit_content` parent | Use fixed width or switch parent to `fill_container` |\n| Pure black text `#000000` | Use `#111111` or `#0F172A` |\n| Heavy drop shadows | Use subtle `rgba(0,0,0,0.05-0.12)` |\n| Emoji as icons | Use path nodes with icon names |\n| Lorem ipsum placeholder | Write realistic, concise copy |\n| Fixed height on text | Use `textGrowth: "fixed-width"` instead |\n| Space Grotesk for CJK | Use `"Noto Sans SC/JP/KR"` |\n| Negative letterSpacing on CJK | Always 0 for CJK text |\n| Missing post-process after insert | Run `op design:refine --root-id <id>` after building the tree |\n| Icons inserted but not visible | Path nodes need `design:refine` or `--post-process` to resolve SVG |\n| Using DSL `I()` with inline `children` | DSL parser fails on nested JSON \u2014 insert parent and children separately |\n| Missing `postProcess: true` in MCP | Always set for MCP tool calls |\n\n## Full Example \u2014 `op insert` Workflow (Recommended)\n\nBuild a complete mobile login page using `op insert --parent`. This is the most reliable approach.\n\n```bash\n#!/bin/bash\nset -e\nID() { python3 -c "import sys,json; print(json.load(sys.stdin)[\'nodeId\'])"; }\n\n# Root frame (mobile)\nROOT=$(op insert \'{"type":"frame","name":"Login","width":375,"height":812,"layout":"vertical","fill":[{"type":"solid","color":"#FFFFFF"}]}\' | ID)\n\n# Header\nTOP=$(op insert --parent "$ROOT" \'{"type":"frame","width":"fill_container","height":"fit_content","layout":"vertical","padding":[80,32,40,32],"gap":14,"alignItems":"center"}\' | ID)\nop insert --parent "$TOP" \'{"type":"path","name":"ShieldIcon","width":48,"height":48,"fill":[{"type":"solid","color":"#6366F1"}]}\'\nop insert --parent "$TOP" \'{"type":"text","content":"Welcome Back","fontSize":28,"fontWeight":700,"fontFamily":"Space Grotesk","letterSpacing":-0.5,"textAlign":"center"}\'\n\n# Form\nFORM=$(op insert --parent "$ROOT" \'{"type":"frame","width":"fill_container","height":"fit_content","layout":"vertical","padding":[0,32],"gap":20}\' | ID)\n\n# Email input\nGRP=$(op insert --parent "$FORM" \'{"type":"frame","role":"form-group","layout":"vertical","gap":8,"width":"fill_container"}\' | ID)\nop insert --parent "$GRP" \'{"type":"text","role":"label","content":"Email","fontSize":14,"fontWeight":500}\'\nINP=$(op insert --parent "$GRP" \'{"type":"rectangle","role":"form-input","width":"fill_container","height":48,"cornerRadius":10,"layout":"horizontal","padding":[0,16],"gap":10,"alignItems":"center","fill":[{"type":"solid","color":"#F9FAFB"}],"stroke":{"thickness":1,"fill":[{"type":"solid","color":"#E5E7EB"}]}}\' | ID)\nop insert --parent "$INP" \'{"type":"path","name":"MailIcon","width":18,"height":18,"fill":[{"type":"solid","color":"#9CA3AF"}]}\'\nop insert --parent "$INP" \'{"type":"text","content":"you@example.com","fontSize":15,"fill":[{"type":"solid","color":"#9CA3AF"}]}\'\n\n# Login button\nBTN=$(op insert --parent "$FORM" \'{"type":"rectangle","role":"button","width":"fill_container","height":50,"cornerRadius":12,"fill":[{"type":"solid","color":"#111111"}],"layout":"horizontal","justifyContent":"center","alignItems":"center"}\' | ID)\nop insert --parent "$BTN" \'{"type":"text","content":"Sign In","fontSize":16,"fontWeight":600,"fill":[{"type":"solid","color":"#FFFFFF"}]}\'\n\n# IMPORTANT: resolve icons + validate layout\nop design:refine --root-id "$ROOT"\n```\n\n## DSL Example \u2014 Landing Page\n\nDSL is suitable for simpler structures. **Avoid inline `children`** \u2014 insert parent and children as separate operations.\n\n```\nroot=I(null, {"type":"frame","name":"Landing","width":1200,"height":0,"layout":"vertical","fill":[{"type":"solid","color":"#FFFFFF"}]})\n\nnav=I(root, {"type":"frame","role":"navbar","width":"fill_container","height":72,"layout":"horizontal","padding":[0,80],"justifyContent":"space_between","alignItems":"center"})\nI(nav, {"type":"text","content":"Acme","fontSize":20,"fontWeight":700,"fontFamily":"Space Grotesk"})\nlinks=I(nav, {"type":"frame","role":"nav-links","layout":"horizontal","gap":32,"width":"fit_content","height":"fit_content"})\nI(links, {"type":"text","role":"nav-link","content":"Features","fontSize":15})\nI(links, {"type":"text","role":"nav-link","content":"Pricing","fontSize":15})\ncta=I(nav, {"type":"rectangle","role":"button","padding":[10,24],"cornerRadius":8,"fill":[{"type":"solid","color":"#111111"}],"layout":"horizontal","justifyContent":"center","alignItems":"center"})\nI(cta, {"type":"text","content":"Get Started","fontSize":14,"fontWeight":600,"fill":[{"type":"solid","color":"#FFFFFF"}]})\n\nhero=I(root, {"type":"frame","role":"hero","width":"fill_container","height":"fit_content","layout":"vertical","padding":[100,80],"gap":24,"alignItems":"center"})\nI(hero, {"type":"text","role":"heading","content":"Ship faster with Acme","fontSize":56,"fontWeight":700,"fontFamily":"Space Grotesk","textAlign":"center","letterSpacing":-1.5,"lineHeight":1.1,"textGrowth":"fixed-width","width":800})\nI(hero, {"type":"text","role":"subheading","content":"Turn ideas into production apps in minutes.","fontSize":18,"textAlign":"center","lineHeight":1.6,"textGrowth":"fixed-width","width":560,"fill":[{"type":"solid","color":"#6B7280"}]})\nbtns=I(hero, {"type":"frame","layout":"horizontal","gap":12,"width":"fit_content","height":"fit_content"})\nb1=I(btns, {"type":"rectangle","role":"button","padding":[14,32],"cornerRadius":10,"fill":[{"type":"solid","color":"#111111"}],"layout":"horizontal","justifyContent":"center","alignItems":"center"})\nI(b1, {"type":"text","content":"Start Free","fontSize":16,"fontWeight":600,"fill":[{"type":"solid","color":"#FFFFFF"}]})\nb2=I(btns, {"type":"rectangle","role":"button","padding":[14,32],"cornerRadius":10,"fill":[{"type":"solid","color":"#F3F4F6"}],"layout":"horizontal","justifyContent":"center","alignItems":"center"})\nI(b2, {"type":"text","content":"View Demo","fontSize":16,"fontWeight":600})\n\nfeat=I(root, {"type":"frame","role":"section","width":"fill_container","height":"fit_content","layout":"vertical","padding":[80,80],"gap":48,"alignItems":"center"})\nI(feat, {"type":"text","role":"heading","content":"Everything you need","fontSize":36,"fontWeight":700,"fontFamily":"Space Grotesk","textAlign":"center","letterSpacing":-0.5})\ngrid=I(feat, {"type":"frame","role":"feature-grid","width":"fill_container","layout":"horizontal","gap":24})\nc1=I(grid, {"type":"rectangle","role":"feature-card","width":"fill_container","height":"fill_container","layout":"vertical","padding":28,"gap":16,"cornerRadius":16,"fill":[{"type":"solid","color":"#F9FAFB"}]})\nI(c1, {"type":"path","name":"ZapIcon","width":24,"height":24,"fill":[{"type":"solid","color":"#111111"}]})\nI(c1, {"type":"text","content":"Lightning Fast","fontSize":20,"fontWeight":600})\nI(c1, {"type":"text","role":"body-text","content":"Sub-second builds with smart caching.","fontSize":15,"lineHeight":1.6,"fill":[{"type":"solid","color":"#6B7280"}]})\nc2=C(c1, grid, {})\nU(c2+"/0", {"name":"ShieldIcon"})\nU(c2+"/1", {"content":"Enterprise Security"})\nU(c2+"/2", {"content":"SOC 2 certified with end-to-end encryption."})\nc3=C(c1, grid, {})\nU(c3+"/0", {"name":"GitBranchIcon"})\nU(c3+"/1", {"content":"Git-Native Workflow"})\nU(c3+"/2", {"content":"Preview deploys on every push with instant rollback."})\n```\n',
|
|
13392
|
+
".claude-plugin/plugin.json": '{\n "name": "openpencil-skill",\n "description": "Design skill for OpenPencil \u2014 op CLI, batch DSL, MCP tools, PenNode schema, and UI design best practices",\n "version": "0.7.0",\n "author": {\n "name": "ZSeven-W",\n "email": "xkayshen@gmail.com"\n },\n "homepage": "https://github.com/zseven-w/openpencil-skill",\n "repository": "https://github.com/zseven-w/openpencil-skill",\n "license": "MIT",\n "keywords": [\n "design",\n "ui",\n "vector",\n "openpencil",\n "cli",\n "mcp",\n "dsl"\n ]\n}\n',
|
|
13393
|
+
".claude-plugin/marketplace.json": '{\n "name": "openpencil-skill",\n "description": "Design skill for OpenPencil \u2014 op CLI, batch DSL, MCP tools, and UI best practices",\n "owner": {\n "name": "ZSeven-W",\n "email": "xkayshen@gmail.com"\n },\n "plugins": [\n {\n "name": "openpencil-skill",\n "description": "Design skill for OpenPencil \u2014 op CLI, batch DSL, MCP tools, PenNode schema, and UI design best practices",\n "version": "0.7.0",\n "source": "./",\n "author": {\n "name": "ZSeven-W",\n "email": "xkayshen@gmail.com"\n }\n }\n ]\n}\n',
|
|
13394
|
+
".cursor-plugin/plugin.json": '{\n "name": "openpencil-skill",\n "displayName": "OpenPencil Design",\n "description": "Design skill for OpenPencil \u2014 op CLI, batch DSL, MCP tools, PenNode schema, and UI design best practices",\n "version": "0.7.0",\n "author": {\n "name": "ZSeven-W",\n "email": "xkayshen@gmail.com"\n },\n "homepage": "https://github.com/zseven-w/openpencil-skill",\n "repository": "https://github.com/zseven-w/openpencil-skill",\n "license": "MIT",\n "keywords": [\n "design",\n "ui",\n "vector",\n "openpencil",\n "cli",\n "mcp",\n "dsl"\n ],\n "skills": "./skills/"\n}\n',
|
|
13395
|
+
"package.json": '{\n "name": "openpencil-skill",\n "version": "0.7.0",\n "type": "module"\n}\n',
|
|
13396
|
+
"GEMINI.md": "@./skills/openpencil-design/SKILL.md\n",
|
|
13397
|
+
"gemini-extension.json": '{\n "name": "openpencil-skill",\n "description": "Design skill for OpenPencil \u2014 op CLI, batch DSL, MCP tools, and UI best practices",\n "version": "0.7.0",\n "contextFileName": "GEMINI.md"\n}\n'
|
|
13398
|
+
}
|
|
13399
|
+
};
|
|
13400
|
+
}
|
|
13401
|
+
});
|
|
13402
|
+
|
|
13403
|
+
// apps/cli/src/commands/install.ts
|
|
13404
|
+
var install_exports = {};
|
|
13405
|
+
__export(install_exports, {
|
|
13406
|
+
cmdInstall: () => cmdInstall,
|
|
13407
|
+
cmdUninstall: () => cmdUninstall
|
|
13408
|
+
});
|
|
13409
|
+
function which(cmd) {
|
|
13410
|
+
try {
|
|
13411
|
+
return (0, import_node_child_process2.execSync)(`which ${cmd} 2>/dev/null`, { encoding: "utf-8" }).trim() || null;
|
|
13412
|
+
} catch {
|
|
13413
|
+
return null;
|
|
13414
|
+
}
|
|
13415
|
+
}
|
|
13416
|
+
function detectTargets() {
|
|
13417
|
+
const found = [];
|
|
13418
|
+
if (which("claude")) found.push("claude");
|
|
13419
|
+
if (which("codex")) found.push("codex");
|
|
13420
|
+
if ((0, import_node_fs3.existsSync)((0, import_node_path7.join)((0, import_node_os4.homedir)(), ".cursor"))) found.push("cursor");
|
|
13421
|
+
if (which("gemini")) found.push("gemini");
|
|
13422
|
+
if (which("opencode")) found.push("opencode");
|
|
13423
|
+
return found;
|
|
13424
|
+
}
|
|
13425
|
+
function log(msg) {
|
|
13426
|
+
process.stderr.write(msg + "\n");
|
|
13427
|
+
}
|
|
13428
|
+
function logOk(target, msg) {
|
|
13429
|
+
log(` \u2713 ${target}: ${msg}`);
|
|
13430
|
+
}
|
|
13431
|
+
function logSkip(target, msg) {
|
|
13432
|
+
log(` - ${target}: ${msg}`);
|
|
13433
|
+
}
|
|
13434
|
+
function logErr(target, msg) {
|
|
13435
|
+
log(` \u2717 ${target}: ${msg}`);
|
|
13436
|
+
}
|
|
13437
|
+
function writeBundleTo(dest, fileFilter) {
|
|
13438
|
+
const files = skill_bundle_default.files;
|
|
13439
|
+
for (const [relativePath, content] of Object.entries(files)) {
|
|
13440
|
+
if (fileFilter && !fileFilter(relativePath)) continue;
|
|
13441
|
+
const fullPath = (0, import_node_path7.join)(dest, relativePath);
|
|
13442
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path7.dirname)(fullPath), { recursive: true });
|
|
13443
|
+
(0, import_node_fs3.writeFileSync)(fullPath, content);
|
|
13444
|
+
}
|
|
13445
|
+
}
|
|
13446
|
+
function ensureRepo(dest) {
|
|
13447
|
+
if ((0, import_node_fs3.existsSync)((0, import_node_path7.join)(dest, ".git"))) {
|
|
13448
|
+
(0, import_node_child_process2.execSync)("git pull --ff-only 2>/dev/null", { cwd: dest, stdio: "ignore" });
|
|
13449
|
+
} else {
|
|
13450
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path7.dirname)(dest), { recursive: true });
|
|
13451
|
+
(0, import_node_child_process2.execSync)(`git clone --depth 1 ${REPO_URL} "${dest}"`, { stdio: "ignore" });
|
|
13452
|
+
}
|
|
13453
|
+
}
|
|
13454
|
+
function ensureSkillDir(dest, fileFilter) {
|
|
13455
|
+
if (hasBundledFiles) {
|
|
13456
|
+
(0, import_node_fs3.mkdirSync)(dest, { recursive: true });
|
|
13457
|
+
writeBundleTo(dest, fileFilter);
|
|
13458
|
+
} else {
|
|
13459
|
+
ensureRepo(dest);
|
|
13460
|
+
}
|
|
13461
|
+
}
|
|
13462
|
+
function installClaude() {
|
|
13463
|
+
const target = "Claude Code";
|
|
13464
|
+
try {
|
|
13465
|
+
if (hasBundledFiles) {
|
|
13466
|
+
const cacheDir = (0, import_node_path7.join)(
|
|
13467
|
+
(0, import_node_os4.homedir)(),
|
|
13468
|
+
".claude",
|
|
13469
|
+
"plugins",
|
|
13470
|
+
"cache",
|
|
13471
|
+
SKILL_NAME,
|
|
13472
|
+
SKILL_NAME,
|
|
13473
|
+
skill_bundle_default.version
|
|
13474
|
+
);
|
|
13475
|
+
(0, import_node_fs3.mkdirSync)(cacheDir, { recursive: true });
|
|
13476
|
+
writeBundleTo(cacheDir);
|
|
13477
|
+
const registryPath = (0, import_node_path7.join)((0, import_node_os4.homedir)(), ".claude", "plugins", "installed_plugins.json");
|
|
13478
|
+
const registry2 = (0, import_node_fs3.existsSync)(registryPath) ? JSON.parse((0, import_node_fs3.readFileSync)(registryPath, "utf-8")) : { version: 2, plugins: {} };
|
|
13479
|
+
const key = `${SKILL_NAME}@${SKILL_NAME}`;
|
|
13480
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13481
|
+
registry2.plugins[key] = [
|
|
13482
|
+
{
|
|
13483
|
+
scope: "user",
|
|
13484
|
+
installPath: cacheDir,
|
|
13485
|
+
version: skill_bundle_default.version,
|
|
13486
|
+
installedAt: registry2.plugins[key]?.[0]?.installedAt ?? now,
|
|
13487
|
+
lastUpdated: now
|
|
13488
|
+
}
|
|
13489
|
+
];
|
|
13490
|
+
(0, import_node_fs3.writeFileSync)(registryPath, JSON.stringify(registry2, null, 2) + "\n");
|
|
13491
|
+
const marketplacesPath = (0, import_node_path7.join)((0, import_node_os4.homedir)(), ".claude", "plugins", "known_marketplaces.json");
|
|
13492
|
+
const marketplaces = (0, import_node_fs3.existsSync)(marketplacesPath) ? JSON.parse((0, import_node_fs3.readFileSync)(marketplacesPath, "utf-8")) : {};
|
|
13493
|
+
if (!marketplaces[SKILL_NAME]) {
|
|
13494
|
+
marketplaces[SKILL_NAME] = {
|
|
13495
|
+
source: { source: "github", repo: REPO },
|
|
13496
|
+
installLocation: (0, import_node_path7.join)((0, import_node_os4.homedir)(), ".claude", "plugins", "marketplaces", SKILL_NAME),
|
|
13497
|
+
lastUpdated: now
|
|
13498
|
+
};
|
|
13499
|
+
(0, import_node_fs3.writeFileSync)(marketplacesPath, JSON.stringify(marketplaces, null, 2) + "\n");
|
|
13500
|
+
}
|
|
13501
|
+
logOk(target, `installed v${skill_bundle_default.version} (bundled)`);
|
|
13502
|
+
} else {
|
|
13503
|
+
try {
|
|
13504
|
+
(0, import_node_child_process2.execSync)(`claude plugin marketplace add ${REPO}`, { stdio: "ignore" });
|
|
13505
|
+
} catch {
|
|
13506
|
+
}
|
|
13507
|
+
(0, import_node_child_process2.execSync)(`claude plugin install ${SKILL_NAME}@${SKILL_NAME}`, { stdio: "ignore" });
|
|
13508
|
+
logOk(target, "installed");
|
|
13509
|
+
}
|
|
13510
|
+
} catch (e) {
|
|
13511
|
+
logErr(target, e instanceof Error ? e.message : String(e));
|
|
13512
|
+
}
|
|
13513
|
+
}
|
|
13514
|
+
function installCodex() {
|
|
13515
|
+
const target = "Codex";
|
|
13516
|
+
try {
|
|
13517
|
+
const cloneDir = (0, import_node_path7.join)((0, import_node_os4.homedir)(), ".codex", SKILL_NAME);
|
|
13518
|
+
ensureSkillDir(cloneDir);
|
|
13519
|
+
const skillsDir = (0, import_node_path7.join)((0, import_node_os4.homedir)(), ".agents", "skills");
|
|
13520
|
+
(0, import_node_fs3.mkdirSync)(skillsDir, { recursive: true });
|
|
13521
|
+
const linkPath = (0, import_node_path7.join)(skillsDir, SKILL_NAME);
|
|
13522
|
+
const linkTarget = (0, import_node_path7.join)(cloneDir, "skills");
|
|
13523
|
+
if (!(0, import_node_fs3.existsSync)(linkPath)) {
|
|
13524
|
+
(0, import_node_fs3.symlinkSync)(linkTarget, linkPath);
|
|
13525
|
+
}
|
|
13526
|
+
logOk(target, hasBundledFiles ? `installed v${skill_bundle_default.version} (bundled)` : "installed");
|
|
13527
|
+
} catch (e) {
|
|
13528
|
+
logErr(target, e instanceof Error ? e.message : String(e));
|
|
13529
|
+
}
|
|
13530
|
+
}
|
|
13531
|
+
function installCursor() {
|
|
13532
|
+
const target = "Cursor";
|
|
13533
|
+
try {
|
|
13534
|
+
const destDir = (0, import_node_path7.join)((0, import_node_os4.homedir)(), ".cursor", "plugins", SKILL_NAME);
|
|
13535
|
+
ensureSkillDir(destDir);
|
|
13536
|
+
logOk(target, hasBundledFiles ? `installed v${skill_bundle_default.version} (bundled)` : "installed");
|
|
13537
|
+
} catch (e) {
|
|
13538
|
+
logErr(target, e instanceof Error ? e.message : String(e));
|
|
13539
|
+
}
|
|
13540
|
+
}
|
|
13541
|
+
function installGemini() {
|
|
13542
|
+
const target = "Gemini CLI";
|
|
13543
|
+
try {
|
|
13544
|
+
const destDir = (0, import_node_path7.join)((0, import_node_os4.homedir)(), ".gemini", "extensions", SKILL_NAME);
|
|
13545
|
+
ensureSkillDir(destDir);
|
|
13546
|
+
logOk(target, hasBundledFiles ? `installed v${skill_bundle_default.version} (bundled)` : "installed");
|
|
13547
|
+
} catch (e) {
|
|
13548
|
+
logErr(target, e instanceof Error ? e.message : String(e));
|
|
13549
|
+
}
|
|
13550
|
+
}
|
|
13551
|
+
function installOpenCode() {
|
|
13552
|
+
const target = "OpenCode";
|
|
13553
|
+
try {
|
|
13554
|
+
const configPath = (0, import_node_path7.join)((0, import_node_os4.homedir)(), ".config", "opencode", "opencode.json");
|
|
13555
|
+
const pluginEntry = `${SKILL_NAME}@git+${REPO_URL}`;
|
|
13556
|
+
if ((0, import_node_fs3.existsSync)(configPath)) {
|
|
13557
|
+
const config = JSON.parse((0, import_node_fs3.readFileSync)(configPath, "utf-8"));
|
|
13558
|
+
const plugins = config.plugin ?? [];
|
|
13559
|
+
if (!plugins.some((p) => p.includes(SKILL_NAME))) {
|
|
13560
|
+
plugins.push(pluginEntry);
|
|
13561
|
+
config.plugin = plugins;
|
|
13562
|
+
(0, import_node_fs3.writeFileSync)(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
13563
|
+
logOk(target, "added to opencode.json");
|
|
13564
|
+
} else {
|
|
13565
|
+
logSkip(target, "already configured");
|
|
13566
|
+
}
|
|
13567
|
+
} else {
|
|
13568
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path7.join)((0, import_node_os4.homedir)(), ".config", "opencode"), { recursive: true });
|
|
13569
|
+
(0, import_node_fs3.writeFileSync)(configPath, JSON.stringify({ plugin: [pluginEntry] }, null, 2) + "\n");
|
|
13570
|
+
logOk(target, "created opencode.json");
|
|
13571
|
+
}
|
|
13572
|
+
} catch (e) {
|
|
13573
|
+
logErr(target, e instanceof Error ? e.message : String(e));
|
|
13574
|
+
}
|
|
13575
|
+
}
|
|
13576
|
+
function uninstallClaude() {
|
|
13577
|
+
const target = "Claude Code";
|
|
13578
|
+
try {
|
|
13579
|
+
if (hasBundledFiles) {
|
|
13580
|
+
const cacheParent = (0, import_node_path7.join)((0, import_node_os4.homedir)(), ".claude", "plugins", "cache", SKILL_NAME);
|
|
13581
|
+
if ((0, import_node_fs3.existsSync)(cacheParent)) (0, import_node_fs3.rmSync)(cacheParent, { recursive: true });
|
|
13582
|
+
const registryPath = (0, import_node_path7.join)((0, import_node_os4.homedir)(), ".claude", "plugins", "installed_plugins.json");
|
|
13583
|
+
if ((0, import_node_fs3.existsSync)(registryPath)) {
|
|
13584
|
+
const registry2 = JSON.parse((0, import_node_fs3.readFileSync)(registryPath, "utf-8"));
|
|
13585
|
+
delete registry2.plugins[`${SKILL_NAME}@${SKILL_NAME}`];
|
|
13586
|
+
(0, import_node_fs3.writeFileSync)(registryPath, JSON.stringify(registry2, null, 2) + "\n");
|
|
13587
|
+
}
|
|
13588
|
+
logOk(target, "uninstalled");
|
|
13589
|
+
} else {
|
|
13590
|
+
(0, import_node_child_process2.execSync)(`claude plugin uninstall ${SKILL_NAME}@${SKILL_NAME}`, { stdio: "ignore" });
|
|
13591
|
+
logOk(target, "uninstalled");
|
|
13592
|
+
}
|
|
13593
|
+
} catch (e) {
|
|
13594
|
+
logErr(target, e instanceof Error ? e.message : String(e));
|
|
13595
|
+
}
|
|
13596
|
+
}
|
|
13597
|
+
function uninstallCodex() {
|
|
13598
|
+
const target = "Codex";
|
|
13599
|
+
try {
|
|
13600
|
+
const linkPath = (0, import_node_path7.join)((0, import_node_os4.homedir)(), ".agents", "skills", SKILL_NAME);
|
|
13601
|
+
if ((0, import_node_fs3.existsSync)(linkPath)) (0, import_node_fs3.unlinkSync)(linkPath);
|
|
13602
|
+
const cloneDir = (0, import_node_path7.join)((0, import_node_os4.homedir)(), ".codex", SKILL_NAME);
|
|
13603
|
+
if ((0, import_node_fs3.existsSync)(cloneDir)) (0, import_node_fs3.rmSync)(cloneDir, { recursive: true });
|
|
13604
|
+
logOk(target, "uninstalled");
|
|
13605
|
+
} catch (e) {
|
|
13606
|
+
logErr(target, e instanceof Error ? e.message : String(e));
|
|
13607
|
+
}
|
|
13608
|
+
}
|
|
13609
|
+
function uninstallCursor() {
|
|
13610
|
+
const target = "Cursor";
|
|
13611
|
+
try {
|
|
13612
|
+
const dir = (0, import_node_path7.join)((0, import_node_os4.homedir)(), ".cursor", "plugins", SKILL_NAME);
|
|
13613
|
+
if ((0, import_node_fs3.existsSync)(dir)) (0, import_node_fs3.rmSync)(dir, { recursive: true });
|
|
13614
|
+
logOk(target, "uninstalled");
|
|
13615
|
+
} catch (e) {
|
|
13616
|
+
logErr(target, e instanceof Error ? e.message : String(e));
|
|
13617
|
+
}
|
|
13618
|
+
}
|
|
13619
|
+
function uninstallGemini() {
|
|
13620
|
+
const target = "Gemini CLI";
|
|
13621
|
+
try {
|
|
13622
|
+
const dir = (0, import_node_path7.join)((0, import_node_os4.homedir)(), ".gemini", "extensions", SKILL_NAME);
|
|
13623
|
+
if ((0, import_node_fs3.existsSync)(dir)) (0, import_node_fs3.rmSync)(dir, { recursive: true });
|
|
13624
|
+
logOk(target, "uninstalled");
|
|
13625
|
+
} catch (e) {
|
|
13626
|
+
logErr(target, e instanceof Error ? e.message : String(e));
|
|
13627
|
+
}
|
|
13628
|
+
}
|
|
13629
|
+
function uninstallOpenCode() {
|
|
13630
|
+
const target = "OpenCode";
|
|
13631
|
+
try {
|
|
13632
|
+
const configPath = (0, import_node_path7.join)((0, import_node_os4.homedir)(), ".config", "opencode", "opencode.json");
|
|
13633
|
+
if ((0, import_node_fs3.existsSync)(configPath)) {
|
|
13634
|
+
const config = JSON.parse((0, import_node_fs3.readFileSync)(configPath, "utf-8"));
|
|
13635
|
+
const plugins = config.plugin ?? [];
|
|
13636
|
+
config.plugin = plugins.filter((p) => !p.includes(SKILL_NAME));
|
|
13637
|
+
(0, import_node_fs3.writeFileSync)(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
13638
|
+
logOk(target, "removed from opencode.json");
|
|
13639
|
+
} else {
|
|
13640
|
+
logSkip(target, "not configured");
|
|
13641
|
+
}
|
|
13642
|
+
} catch (e) {
|
|
13643
|
+
logErr(target, e instanceof Error ? e.message : String(e));
|
|
13644
|
+
}
|
|
13645
|
+
}
|
|
13646
|
+
function resolveTargets(targetFlag, mode) {
|
|
13647
|
+
if (targetFlag) {
|
|
13648
|
+
const t = targetFlag.toLowerCase();
|
|
13649
|
+
if (!ALL_TARGETS.includes(t)) {
|
|
13650
|
+
log(`Unknown target: "${targetFlag}". Available: ${ALL_TARGETS.join(", ")}`);
|
|
13651
|
+
process.exit(1);
|
|
13652
|
+
}
|
|
13653
|
+
return [t];
|
|
13654
|
+
}
|
|
13655
|
+
const detected = detectTargets();
|
|
13656
|
+
if (detected.length === 0 && mode === "install") {
|
|
13657
|
+
log("No supported AI coding agents detected.");
|
|
13658
|
+
log(`Supported: ${ALL_TARGETS.join(", ")}`);
|
|
13659
|
+
log("Use --target <name> to install for a specific agent.");
|
|
13660
|
+
process.exit(1);
|
|
13661
|
+
}
|
|
13662
|
+
return detected;
|
|
13663
|
+
}
|
|
13664
|
+
function cmdInstall(flags) {
|
|
13665
|
+
const targets = resolveTargets(flags.target, "install");
|
|
13666
|
+
log(`Installing ${SKILL_NAME} for: ${targets.join(", ")}`);
|
|
13667
|
+
if (!hasBundledFiles) log("(no embedded bundle \u2014 using git clone fallback)");
|
|
13668
|
+
log("");
|
|
13669
|
+
for (const t of targets) INSTALLERS[t]();
|
|
13670
|
+
log("");
|
|
13671
|
+
log("Done. Restart your agent to load the skill.");
|
|
13672
|
+
}
|
|
13673
|
+
function cmdUninstall(flags) {
|
|
13674
|
+
const targets = resolveTargets(flags.target, "uninstall");
|
|
13675
|
+
log(`Uninstalling ${SKILL_NAME} from: ${targets.join(", ")}`);
|
|
13676
|
+
log("");
|
|
13677
|
+
for (const t of targets) UNINSTALLERS[t]();
|
|
13678
|
+
log("");
|
|
13679
|
+
log("Done.");
|
|
13680
|
+
}
|
|
13681
|
+
var import_node_child_process2, import_node_fs3, import_node_path7, import_node_os4, REPO, REPO_URL, SKILL_NAME, ALL_TARGETS, hasBundledFiles, INSTALLERS, UNINSTALLERS;
|
|
13682
|
+
var init_install = __esm({
|
|
13683
|
+
"apps/cli/src/commands/install.ts"() {
|
|
13684
|
+
"use strict";
|
|
13685
|
+
init_define_import_meta_env();
|
|
13686
|
+
import_node_child_process2 = require("node:child_process");
|
|
13687
|
+
import_node_fs3 = require("node:fs");
|
|
13688
|
+
import_node_path7 = require("node:path");
|
|
13689
|
+
import_node_os4 = require("node:os");
|
|
13690
|
+
init_skill_bundle();
|
|
13691
|
+
REPO = "zseven-w/openpencil-skill";
|
|
13692
|
+
REPO_URL = `https://github.com/${REPO}.git`;
|
|
13693
|
+
SKILL_NAME = "openpencil-skill";
|
|
13694
|
+
ALL_TARGETS = ["claude", "codex", "cursor", "gemini", "opencode"];
|
|
13695
|
+
hasBundledFiles = Object.keys(skill_bundle_default.files).length > 0;
|
|
13696
|
+
INSTALLERS = {
|
|
13697
|
+
claude: installClaude,
|
|
13698
|
+
codex: installCodex,
|
|
13699
|
+
cursor: installCursor,
|
|
13700
|
+
gemini: installGemini,
|
|
13701
|
+
opencode: installOpenCode
|
|
13702
|
+
};
|
|
13703
|
+
UNINSTALLERS = {
|
|
13704
|
+
claude: uninstallClaude,
|
|
13705
|
+
codex: uninstallCodex,
|
|
13706
|
+
cursor: uninstallCursor,
|
|
13707
|
+
gemini: uninstallGemini,
|
|
13708
|
+
opencode: uninstallOpenCode
|
|
13709
|
+
};
|
|
13710
|
+
}
|
|
13711
|
+
});
|
|
13712
|
+
|
|
13269
13713
|
// apps/cli/src/commands/layout.ts
|
|
13270
13714
|
var layout_exports = {};
|
|
13271
13715
|
__export(layout_exports, {
|
|
@@ -13307,13 +13751,22 @@ init_define_import_meta_env();
|
|
|
13307
13751
|
// apps/cli/package.json
|
|
13308
13752
|
var package_default = {
|
|
13309
13753
|
name: "@zseven-w/openpencil",
|
|
13310
|
-
version: "0.7.
|
|
13754
|
+
version: "0.7.1",
|
|
13311
13755
|
description: "CLI for OpenPencil \u2014 control the design tool from your terminal",
|
|
13756
|
+
homepage: "https://github.com/ZSeven-W/openpencil/tree/main/apps/cli",
|
|
13757
|
+
bugs: {
|
|
13758
|
+
url: "https://github.com/ZSeven-W/openpencil/issues"
|
|
13759
|
+
},
|
|
13312
13760
|
license: "MIT",
|
|
13313
13761
|
author: {
|
|
13314
13762
|
name: "ZSeven-W",
|
|
13315
13763
|
email: "xkayshen@gmail.com"
|
|
13316
13764
|
},
|
|
13765
|
+
repository: {
|
|
13766
|
+
type: "git",
|
|
13767
|
+
url: "https://github.com/ZSeven-W/openpencil.git",
|
|
13768
|
+
directory: "apps/cli"
|
|
13769
|
+
},
|
|
13317
13770
|
bin: {
|
|
13318
13771
|
op: "dist/openpencil-cli.cjs"
|
|
13319
13772
|
},
|
|
@@ -13325,8 +13778,8 @@ var package_default = {
|
|
|
13325
13778
|
compile: "cd ../.. && bun run cli:compile"
|
|
13326
13779
|
},
|
|
13327
13780
|
dependencies: {
|
|
13328
|
-
"@zseven-w/pen-figma": "0.7.
|
|
13329
|
-
"@zseven-w/pen-mcp": "0.7.
|
|
13781
|
+
"@zseven-w/pen-figma": "0.7.1",
|
|
13782
|
+
"@zseven-w/pen-mcp": "0.7.1"
|
|
13330
13783
|
}
|
|
13331
13784
|
};
|
|
13332
13785
|
|
|
@@ -13417,6 +13870,11 @@ Import:
|
|
|
13417
13870
|
op import:svg <file.svg> Import SVG file
|
|
13418
13871
|
op import:figma <file.fig> Import Figma file
|
|
13419
13872
|
|
|
13873
|
+
Skill:
|
|
13874
|
+
op install [--target T] Install openpencil-skill for AI agents
|
|
13875
|
+
op uninstall [--target T] Uninstall openpencil-skill
|
|
13876
|
+
Targets: claude, codex, cursor, gemini, opencode (auto-detected if omitted)
|
|
13877
|
+
|
|
13420
13878
|
Layout:
|
|
13421
13879
|
op layout [--parent P] [--depth N]
|
|
13422
13880
|
op find-space [--direction right|bottom|left|top]
|
|
@@ -13708,6 +14166,17 @@ async function main() {
|
|
|
13708
14166
|
});
|
|
13709
14167
|
break;
|
|
13710
14168
|
}
|
|
14169
|
+
// --- Skill install ---
|
|
14170
|
+
case "install": {
|
|
14171
|
+
const { cmdInstall: cmdInstall2 } = await Promise.resolve().then(() => (init_install(), install_exports));
|
|
14172
|
+
cmdInstall2({ target: flags.target });
|
|
14173
|
+
break;
|
|
14174
|
+
}
|
|
14175
|
+
case "uninstall": {
|
|
14176
|
+
const { cmdUninstall: cmdUninstall2 } = await Promise.resolve().then(() => (init_install(), install_exports));
|
|
14177
|
+
cmdUninstall2({ target: flags.target });
|
|
14178
|
+
break;
|
|
14179
|
+
}
|
|
13711
14180
|
// --- Layout ---
|
|
13712
14181
|
case "layout": {
|
|
13713
14182
|
const { cmdLayout: cmdLayout2 } = await Promise.resolve().then(() => (init_layout(), layout_exports));
|