Built-in design generation now runs as an agentic MCP tool-loop (reusing the agent-rs BuiltInProvider), gated behind OPENPENCIL_DESIGN_AGENT_LOOP / the Settings experimental toggle; the orchestrator stays the default. - design-agent system prompt + in-process design toolset (parity-locked with the MCP surface) + flag-gated Intent::Design routing - spawn_agents execution as sequential sub-loops + live creation-mode badges (per-agent glow + 'N/M designing...' header) - new MCP tools: get_guidelines, ToolSearch, get_screenshot, get_editor_state, export_nodes, spawn_agents; style-guide local audit - #27 AI panel restyle: rounded tool cards + green check-rings, gray user bubbles, model-pill bottom toolbar, header, empty-state pills, the PARALLEL AGENTS (agent_team_size) 1x-6x chip dropdown - multi-chat tabs: ChatSessions model (Deref-to-active) + tab row UI (switch / close / + / Cmd+T) with each run bound to its tab Large checkpoint commit spanning the working tree (Rust shell crates).
456 lines
13 KiB
JavaScript
456 lines
13 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import pkg from '../package.json';
|
|
import { setPretty, output, outputError } from './output';
|
|
|
|
// --- Arg parsing ---
|
|
|
|
interface ParsedArgs {
|
|
command: string;
|
|
positionals: string[];
|
|
flags: Record<string, string | boolean>;
|
|
}
|
|
|
|
function parseArgs(argv: string[]): ParsedArgs {
|
|
const args = argv.slice(2);
|
|
const positionals: string[] = [];
|
|
const flags: Record<string, string | boolean> = {};
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
const arg = args[i];
|
|
if (arg.startsWith('--')) {
|
|
const key = arg.slice(2);
|
|
const next = args[i + 1];
|
|
if (next && !next.startsWith('--')) {
|
|
flags[key] = next;
|
|
i++;
|
|
} else {
|
|
flags[key] = true;
|
|
}
|
|
} else {
|
|
positionals.push(arg);
|
|
}
|
|
}
|
|
|
|
return {
|
|
command: positionals[0] ?? '',
|
|
positionals: positionals.slice(1),
|
|
flags,
|
|
};
|
|
}
|
|
|
|
// --- Help ---
|
|
|
|
const HELP = `OpenPencil CLI v${pkg.version}
|
|
|
|
Usage: op <command> [options]
|
|
|
|
App:
|
|
op start [--desktop|--web] Launch OpenPencil
|
|
op stop Stop running instance
|
|
op status Check if running
|
|
|
|
Document:
|
|
op open [file.op] Open file or connect to live canvas
|
|
op save <file.op> Save current document to file
|
|
op get [--type X] [--name Y] [--id Z] [--depth N]
|
|
op selection Get current canvas selection
|
|
|
|
Nodes:
|
|
op insert <json> [--parent P] [--index N] [--post-process]
|
|
op update <id> <json> [--post-process]
|
|
op delete <id>
|
|
op move <id> --parent <P> [--index N]
|
|
op copy <id> [--parent P]
|
|
op replace <id> <json> [--post-process]
|
|
|
|
Design:
|
|
op design <dsl|@file|->
|
|
op design:skeleton <json|@file|->
|
|
op design:content <section-id> <json|@file|->
|
|
op design:refine --root-id <id>
|
|
|
|
Read Nodes:
|
|
op read-nodes [ids] [--depth N] [--vars] [--page P] [--file F]
|
|
ids: comma-separated node IDs (omit to read all)
|
|
|
|
Codegen Pipeline:
|
|
op codegen:plan <plan-json|@file|->
|
|
op codegen:submit <planId> <chunk-result|@file|->
|
|
op codegen:assemble <planId> [--framework react]
|
|
op codegen:clean <planId>
|
|
|
|
Variables & Themes:
|
|
op vars Get variables
|
|
op vars:set <json> Set variables
|
|
op themes Get themes
|
|
op themes:set <json> Set themes
|
|
op theme:save <file.optheme> Save theme preset
|
|
op theme:load <file.optheme> Load theme preset
|
|
op theme:list [dir] List theme presets
|
|
|
|
Pages:
|
|
op page list
|
|
op page add [--name N]
|
|
op page remove <id>
|
|
op page rename <id> <name>
|
|
op page reorder <id> <index>
|
|
op page duplicate <id>
|
|
|
|
Import:
|
|
op import:svg <file.svg> Import SVG file
|
|
op import:figma <file.fig> Import Figma file
|
|
|
|
Skill:
|
|
op install [--target T] Install openpencil-skill for AI agents
|
|
op uninstall [--target T] Uninstall openpencil-skill
|
|
Targets: claude, codex, cursor, gemini, opencode (auto-detected if omitted)
|
|
|
|
Layout:
|
|
op layout [--parent P] [--depth N]
|
|
op find-space [--direction right|bottom|left|top]
|
|
|
|
Arguments that accept JSON or DSL can be passed as:
|
|
<value> Inline string
|
|
@filepath Read from file (e.g. @design.txt)
|
|
- Read from stdin (e.g. cat design.txt | op design -)
|
|
|
|
Global Flags:
|
|
--file <path> Target .op file (default: live canvas)
|
|
--page <id> Target page ID
|
|
--pretty Human-readable JSON output
|
|
--help Show this help
|
|
--version Show version
|
|
`;
|
|
|
|
// --- Main ---
|
|
|
|
async function main(): Promise<void> {
|
|
const { command, positionals, flags } = parseArgs(process.argv);
|
|
|
|
if (flags.pretty) setPretty(true);
|
|
if (flags.help || command === 'help') {
|
|
process.stdout.write(HELP);
|
|
return;
|
|
}
|
|
if (flags.version || command === 'version') {
|
|
output({ version: pkg.version });
|
|
return;
|
|
}
|
|
if (!command) {
|
|
process.stdout.write(HELP);
|
|
return;
|
|
}
|
|
|
|
const globalFlags = {
|
|
file: flags.file as string | undefined,
|
|
page: flags.page as string | undefined,
|
|
};
|
|
|
|
switch (command) {
|
|
// --- App ---
|
|
case 'start': {
|
|
const { cmdStart } = await import('./commands/app');
|
|
await cmdStart({ desktop: !!flags.desktop, web: !!flags.web });
|
|
break;
|
|
}
|
|
case 'stop': {
|
|
const { cmdStop } = await import('./commands/app');
|
|
await cmdStop();
|
|
break;
|
|
}
|
|
case 'status': {
|
|
const { cmdStatus } = await import('./commands/app');
|
|
await cmdStatus();
|
|
break;
|
|
}
|
|
|
|
// --- Document ---
|
|
case 'open': {
|
|
const { cmdOpen } = await import('./commands/document');
|
|
await cmdOpen(positionals, globalFlags);
|
|
break;
|
|
}
|
|
case 'save': {
|
|
const { cmdSave } = await import('./commands/document');
|
|
await cmdSave(positionals, globalFlags);
|
|
break;
|
|
}
|
|
case 'get': {
|
|
const { cmdGet } = await import('./commands/document');
|
|
await cmdGet(positionals, {
|
|
...globalFlags,
|
|
type: flags.type as string | undefined,
|
|
name: flags.name as string | undefined,
|
|
id: flags.id as string | undefined,
|
|
depth: flags.depth as string | undefined,
|
|
parent: flags.parent as string | undefined,
|
|
});
|
|
break;
|
|
}
|
|
case 'selection': {
|
|
const { cmdSelection } = await import('./commands/document');
|
|
await cmdSelection({ ...globalFlags, depth: flags.depth as string | undefined });
|
|
break;
|
|
}
|
|
|
|
// --- Nodes ---
|
|
case 'insert': {
|
|
const { cmdInsert } = await import('./commands/nodes');
|
|
await cmdInsert(positionals, {
|
|
...globalFlags,
|
|
parent: flags.parent as string | undefined,
|
|
index: flags.index as string | undefined,
|
|
postProcess: !!flags['post-process'],
|
|
});
|
|
break;
|
|
}
|
|
case 'update': {
|
|
const { cmdUpdate } = await import('./commands/nodes');
|
|
await cmdUpdate(positionals, {
|
|
...globalFlags,
|
|
postProcess: !!flags['post-process'],
|
|
});
|
|
break;
|
|
}
|
|
case 'delete': {
|
|
const { cmdDelete } = await import('./commands/nodes');
|
|
await cmdDelete(positionals, globalFlags);
|
|
break;
|
|
}
|
|
case 'move': {
|
|
const { cmdMove } = await import('./commands/nodes');
|
|
await cmdMove(positionals, {
|
|
...globalFlags,
|
|
parent: flags.parent as string | undefined,
|
|
index: flags.index as string | undefined,
|
|
});
|
|
break;
|
|
}
|
|
case 'copy': {
|
|
const { cmdCopy } = await import('./commands/nodes');
|
|
await cmdCopy(positionals, {
|
|
...globalFlags,
|
|
parent: flags.parent as string | undefined,
|
|
});
|
|
break;
|
|
}
|
|
case 'replace': {
|
|
const { cmdReplace } = await import('./commands/nodes');
|
|
await cmdReplace(positionals, {
|
|
...globalFlags,
|
|
postProcess: !!flags['post-process'],
|
|
});
|
|
break;
|
|
}
|
|
|
|
// --- Design ---
|
|
case 'design': {
|
|
const { cmdDesign } = await import('./commands/design');
|
|
await cmdDesign(positionals, {
|
|
...globalFlags,
|
|
postProcess: flags['post-process'] !== false ? true : undefined,
|
|
canvasWidth: flags['canvas-width'] as string | undefined,
|
|
});
|
|
break;
|
|
}
|
|
case 'design:skeleton': {
|
|
const { cmdDesignSkeleton } = await import('./commands/design');
|
|
await cmdDesignSkeleton(positionals, globalFlags);
|
|
break;
|
|
}
|
|
case 'design:content': {
|
|
const { cmdDesignContent } = await import('./commands/design');
|
|
await cmdDesignContent(positionals, {
|
|
...globalFlags,
|
|
canvasWidth: flags['canvas-width'] as string | undefined,
|
|
});
|
|
break;
|
|
}
|
|
case 'design:refine': {
|
|
const { cmdDesignRefine } = await import('./commands/design');
|
|
await cmdDesignRefine(positionals, {
|
|
...globalFlags,
|
|
rootId: flags['root-id'] as string | undefined,
|
|
canvasWidth: flags['canvas-width'] as string | undefined,
|
|
});
|
|
break;
|
|
}
|
|
|
|
// --- Read Nodes ---
|
|
case 'read-nodes': {
|
|
const { cmdReadNodes } = await import('./commands/read-nodes');
|
|
await cmdReadNodes(positionals, {
|
|
file: globalFlags.file,
|
|
page: globalFlags.page,
|
|
depth: flags.depth as string | undefined,
|
|
vars: !!flags.vars,
|
|
});
|
|
break;
|
|
}
|
|
|
|
// --- Codegen Pipeline ---
|
|
case 'codegen:plan': {
|
|
const { cmdCodegenPlan } = await import('./commands/codegen');
|
|
await cmdCodegenPlan(positionals, globalFlags);
|
|
break;
|
|
}
|
|
case 'codegen:submit': {
|
|
const { cmdCodegenSubmit } = await import('./commands/codegen');
|
|
await cmdCodegenSubmit(positionals, globalFlags);
|
|
break;
|
|
}
|
|
case 'codegen:assemble': {
|
|
const { cmdCodegenAssemble } = await import('./commands/codegen');
|
|
await cmdCodegenAssemble(positionals, {
|
|
...globalFlags,
|
|
framework: flags.framework as string | undefined,
|
|
});
|
|
break;
|
|
}
|
|
case 'codegen:clean': {
|
|
const { cmdCodegenClean } = await import('./commands/codegen');
|
|
await cmdCodegenClean(positionals, globalFlags);
|
|
break;
|
|
}
|
|
|
|
// --- Variables & Themes ---
|
|
case 'vars': {
|
|
const { cmdVars } = await import('./commands/variables');
|
|
await cmdVars(globalFlags);
|
|
break;
|
|
}
|
|
case 'vars:set': {
|
|
const { cmdVarsSet } = await import('./commands/variables');
|
|
await cmdVarsSet(positionals, { ...globalFlags, replace: !!flags.replace });
|
|
break;
|
|
}
|
|
case 'themes': {
|
|
const { cmdThemes } = await import('./commands/variables');
|
|
await cmdThemes(globalFlags);
|
|
break;
|
|
}
|
|
case 'themes:set': {
|
|
const { cmdThemesSet } = await import('./commands/variables');
|
|
await cmdThemesSet(positionals, { ...globalFlags, replace: !!flags.replace });
|
|
break;
|
|
}
|
|
case 'theme:save': {
|
|
const { cmdThemeSave } = await import('./commands/variables');
|
|
await cmdThemeSave(positionals, globalFlags);
|
|
break;
|
|
}
|
|
case 'theme:load': {
|
|
const { cmdThemeLoad } = await import('./commands/variables');
|
|
await cmdThemeLoad(positionals, globalFlags);
|
|
break;
|
|
}
|
|
case 'theme:list': {
|
|
const { cmdThemeList } = await import('./commands/variables');
|
|
await cmdThemeList(positionals);
|
|
break;
|
|
}
|
|
|
|
// --- Pages ---
|
|
case 'page': {
|
|
const subCmd = positionals[0];
|
|
const subArgs = positionals.slice(1);
|
|
switch (subCmd) {
|
|
case 'list': {
|
|
const { cmdPageList } = await import('./commands/pages');
|
|
await cmdPageList(globalFlags);
|
|
break;
|
|
}
|
|
case 'add': {
|
|
const { cmdPageAdd } = await import('./commands/pages');
|
|
await cmdPageAdd(subArgs, { ...globalFlags, name: flags.name as string | undefined });
|
|
break;
|
|
}
|
|
case 'remove': {
|
|
const { cmdPageRemove } = await import('./commands/pages');
|
|
await cmdPageRemove(subArgs, globalFlags);
|
|
break;
|
|
}
|
|
case 'rename': {
|
|
const { cmdPageRename } = await import('./commands/pages');
|
|
await cmdPageRename(subArgs, globalFlags);
|
|
break;
|
|
}
|
|
case 'reorder': {
|
|
const { cmdPageReorder } = await import('./commands/pages');
|
|
await cmdPageReorder(subArgs, globalFlags);
|
|
break;
|
|
}
|
|
case 'duplicate': {
|
|
const { cmdPageDuplicate } = await import('./commands/pages');
|
|
await cmdPageDuplicate(subArgs, globalFlags);
|
|
break;
|
|
}
|
|
default:
|
|
outputError(
|
|
`Unknown page subcommand: "${subCmd}". Use: list, add, remove, rename, reorder, duplicate`,
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
|
|
// --- Import ---
|
|
case 'import:svg': {
|
|
const { cmdImportSvg } = await import('./commands/import');
|
|
await cmdImportSvg(positionals, {
|
|
...globalFlags,
|
|
parent: flags.parent as string | undefined,
|
|
});
|
|
break;
|
|
}
|
|
case 'import:figma': {
|
|
const { cmdImportFigma } = await import('./commands/import');
|
|
await cmdImportFigma(positionals, {
|
|
...globalFlags,
|
|
out: flags.out as string | undefined,
|
|
});
|
|
break;
|
|
}
|
|
|
|
// --- Skill install ---
|
|
case 'install': {
|
|
const { cmdInstall } = await import('./commands/install');
|
|
cmdInstall({ target: flags.target as string | undefined });
|
|
break;
|
|
}
|
|
case 'uninstall': {
|
|
const { cmdUninstall } = await import('./commands/install');
|
|
cmdUninstall({ target: flags.target as string | undefined });
|
|
break;
|
|
}
|
|
|
|
// --- Layout ---
|
|
case 'layout': {
|
|
const { cmdLayout } = await import('./commands/layout');
|
|
await cmdLayout({
|
|
...globalFlags,
|
|
parent: flags.parent as string | undefined,
|
|
depth: flags.depth as string | undefined,
|
|
});
|
|
break;
|
|
}
|
|
case 'find-space': {
|
|
const { cmdFindSpace } = await import('./commands/layout');
|
|
await cmdFindSpace({
|
|
...globalFlags,
|
|
direction: flags.direction as string | undefined,
|
|
width: flags.width as string | undefined,
|
|
height: flags.height as string | undefined,
|
|
});
|
|
break;
|
|
}
|
|
|
|
default:
|
|
outputError(`Unknown command: "${command}". Run "op --help" for usage.`);
|
|
}
|
|
}
|
|
|
|
main().catch((err) => {
|
|
outputError(err instanceof Error ? err.message : String(err));
|
|
});
|