pmx-canvas 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/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/Readme.md +865 -0
- package/dist/canvas/global.css +3173 -0
- package/dist/canvas/index.js +183 -0
- package/dist/json-render/index.css +2 -0
- package/dist/json-render/index.js +389 -0
- package/dist/types/cli/agent.d.ts +13 -0
- package/dist/types/cli/index.d.ts +2 -0
- package/dist/types/cli/watch.d.ts +5 -0
- package/dist/types/client/App.d.ts +1 -0
- package/dist/types/client/canvas/AttentionHistory.d.ts +1 -0
- package/dist/types/client/canvas/AttentionToast.d.ts +1 -0
- package/dist/types/client/canvas/CanvasNode.d.ts +8 -0
- package/dist/types/client/canvas/CanvasViewport.d.ts +8 -0
- package/dist/types/client/canvas/CommandPalette.d.ts +4 -0
- package/dist/types/client/canvas/ContextMenu.d.ts +24 -0
- package/dist/types/client/canvas/ContextPinBar.d.ts +1 -0
- package/dist/types/client/canvas/ContextPinHud.d.ts +1 -0
- package/dist/types/client/canvas/DockedNode.d.ts +4 -0
- package/dist/types/client/canvas/EdgeLayer.d.ts +8 -0
- package/dist/types/client/canvas/ExpandedNodeOverlay.d.ts +1 -0
- package/dist/types/client/canvas/FocusFieldLayer.d.ts +1 -0
- package/dist/types/client/canvas/Minimap.d.ts +23 -0
- package/dist/types/client/canvas/SelectionBar.d.ts +1 -0
- package/dist/types/client/canvas/ShortcutOverlay.d.ts +3 -0
- package/dist/types/client/canvas/SnapshotPanel.d.ts +7 -0
- package/dist/types/client/canvas/snap-guides.d.ts +23 -0
- package/dist/types/client/canvas/use-node-drag.d.ts +15 -0
- package/dist/types/client/canvas/use-node-resize.d.ts +15 -0
- package/dist/types/client/canvas/use-pan-zoom.d.ts +16 -0
- package/dist/types/client/ext-app/bridge.d.ts +161 -0
- package/dist/types/client/icons.d.ts +70 -0
- package/dist/types/client/index.d.ts +1 -0
- package/dist/types/client/nodes/ContextNode.d.ts +34 -0
- package/dist/types/client/nodes/ExtAppFrame.d.ts +18 -0
- package/dist/types/client/nodes/FileNode.d.ts +5 -0
- package/dist/types/client/nodes/GroupNode.d.ts +6 -0
- package/dist/types/client/nodes/ImageNode.d.ts +10 -0
- package/dist/types/client/nodes/InlineFormatBar.d.ts +7 -0
- package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +14 -0
- package/dist/types/client/nodes/LedgerNode.d.ts +4 -0
- package/dist/types/client/nodes/MarkdownNode.d.ts +6 -0
- package/dist/types/client/nodes/McpAppNode.d.ts +4 -0
- package/dist/types/client/nodes/MdFormatBar.d.ts +8 -0
- package/dist/types/client/nodes/PromptNode.d.ts +5 -0
- package/dist/types/client/nodes/ResponseNode.d.ts +5 -0
- package/dist/types/client/nodes/StatusNode.d.ts +4 -0
- package/dist/types/client/nodes/StatusSummary.d.ts +4 -0
- package/dist/types/client/nodes/TraceNode.d.ts +4 -0
- package/dist/types/client/nodes/WebpageNode.d.ts +5 -0
- package/dist/types/client/nodes/image-warnings.d.ts +6 -0
- package/dist/types/client/nodes/inline-editor-commands.d.ts +11 -0
- package/dist/types/client/nodes/md-format.d.ts +25 -0
- package/dist/types/client/state/attention-bridge.d.ts +3 -0
- package/dist/types/client/state/attention-store.d.ts +25 -0
- package/dist/types/client/state/canvas-store.d.ts +74 -0
- package/dist/types/client/state/intent-bridge.d.ts +158 -0
- package/dist/types/client/state/sse-bridge.d.ts +5 -0
- package/dist/types/client/theme/tokens.d.ts +27 -0
- package/dist/types/client/types.d.ts +40 -0
- package/dist/types/client/utils/ext-app-tool-result.d.ts +1 -0
- package/dist/types/client/utils/placement.d.ts +1 -0
- package/dist/types/client/utils/platform.d.ts +2 -0
- package/dist/types/json-render/catalog.d.ts +815 -0
- package/dist/types/json-render/charts/components.d.ts +54 -0
- package/dist/types/json-render/charts/definitions.d.ts +103 -0
- package/dist/types/json-render/charts/extra-components.d.ts +58 -0
- package/dist/types/json-render/charts/extra-definitions.d.ts +181 -0
- package/dist/types/json-render/renderer/index.d.ts +16 -0
- package/dist/types/json-render/schema.d.ts +46 -0
- package/dist/types/json-render/server.d.ts +55 -0
- package/dist/types/mcp/server.d.ts +22 -0
- package/dist/types/server/agent-context.d.ts +21 -0
- package/dist/types/server/artifact-paths.d.ts +3 -0
- package/dist/types/server/canvas-operations.d.ts +154 -0
- package/dist/types/server/canvas-provenance.d.ts +13 -0
- package/dist/types/server/canvas-schema.d.ts +49 -0
- package/dist/types/server/canvas-serialization.d.ts +25 -0
- package/dist/types/server/canvas-state.d.ts +174 -0
- package/dist/types/server/canvas-validation.d.ts +33 -0
- package/dist/types/server/chart-template.d.ts +29 -0
- package/dist/types/server/code-graph.d.ts +67 -0
- package/dist/types/server/context-cards.d.ts +24 -0
- package/dist/types/server/diagram-presets.d.ts +28 -0
- package/dist/types/server/ext-app-call-registry.d.ts +16 -0
- package/dist/types/server/ext-app-tool-result.d.ts +1 -0
- package/dist/types/server/file-watcher.d.ts +16 -0
- package/dist/types/server/index.d.ts +243 -0
- package/dist/types/server/mcp-app-candidate.d.ts +25 -0
- package/dist/types/server/mcp-app-host.d.ts +65 -0
- package/dist/types/server/mcp-app-runtime.d.ts +47 -0
- package/dist/types/server/mutation-history.d.ts +105 -0
- package/dist/types/server/placement.d.ts +37 -0
- package/dist/types/server/server.d.ts +103 -0
- package/dist/types/server/spatial-analysis.d.ts +87 -0
- package/dist/types/server/trace-manager.d.ts +48 -0
- package/dist/types/server/web-artifacts.d.ts +50 -0
- package/dist/types/server/webpage-node.d.ts +25 -0
- package/dist/types/shared/auto-arrange.d.ts +29 -0
- package/dist/types/shared/ext-app-tool-result.d.ts +9 -0
- package/dist/types/shared/placement.d.ts +26 -0
- package/dist/types/shared/semantic-attention.d.ts +97 -0
- package/package.json +109 -0
- package/skills/data-analysis/SKILL.md +324 -0
- package/skills/doc-coauthoring/SKILL.md +375 -0
- package/skills/frontend-design/SKILL.md +45 -0
- package/skills/json-render-codegen/SKILL.md +112 -0
- package/skills/json-render-core/SKILL.md +265 -0
- package/skills/json-render-ink/SKILL.md +273 -0
- package/skills/json-render-mcp/SKILL.md +132 -0
- package/skills/json-render-react/SKILL.md +264 -0
- package/skills/json-render-shadcn/SKILL.md +159 -0
- package/skills/playwright-cli/SKILL.md +67 -0
- package/skills/pmx-canvas/SKILL.md +668 -0
- package/skills/pmx-canvas/evals/evals.json +186 -0
- package/skills/pmx-canvas-testing/SKILL.md +78 -0
- package/skills/published-consumer-e2e/SKILL.md +43 -0
- package/skills/published-consumer-e2e/scripts/run-published-consumer-e2e.sh +241 -0
- package/skills/web-artifacts-builder/SKILL.md +80 -0
- package/skills/web-artifacts-builder/scripts/bundle-artifact.sh +167 -0
- package/skills/web-artifacts-builder/scripts/init-artifact.sh +425 -0
- package/skills/web-artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
- package/skills/web-design-guidelines/SKILL.md +39 -0
- package/src/cli/agent.ts +2144 -0
- package/src/cli/index.ts +622 -0
- package/src/cli/watch.ts +88 -0
- package/src/client/App.tsx +507 -0
- package/src/client/canvas/AttentionHistory.tsx +81 -0
- package/src/client/canvas/AttentionToast.tsx +19 -0
- package/src/client/canvas/CanvasNode.tsx +363 -0
- package/src/client/canvas/CanvasViewport.tsx +590 -0
- package/src/client/canvas/CommandPalette.tsx +302 -0
- package/src/client/canvas/ContextMenu.tsx +601 -0
- package/src/client/canvas/ContextPinBar.tsx +25 -0
- package/src/client/canvas/ContextPinHud.tsx +22 -0
- package/src/client/canvas/DockedNode.tsx +66 -0
- package/src/client/canvas/EdgeLayer.tsx +280 -0
- package/src/client/canvas/ExpandedNodeOverlay.tsx +260 -0
- package/src/client/canvas/FocusFieldLayer.tsx +107 -0
- package/src/client/canvas/Minimap.tsx +301 -0
- package/src/client/canvas/SelectionBar.tsx +69 -0
- package/src/client/canvas/ShortcutOverlay.tsx +69 -0
- package/src/client/canvas/SnapshotPanel.tsx +236 -0
- package/src/client/canvas/snap-guides.ts +170 -0
- package/src/client/canvas/use-node-drag.ts +51 -0
- package/src/client/canvas/use-node-resize.ts +59 -0
- package/src/client/canvas/use-pan-zoom.ts +191 -0
- package/src/client/ext-app/bridge.ts +542 -0
- package/src/client/icons.tsx +424 -0
- package/src/client/index.tsx +7 -0
- package/src/client/nodes/ContextNode.tsx +412 -0
- package/src/client/nodes/ExtAppFrame.tsx +509 -0
- package/src/client/nodes/FileNode.tsx +256 -0
- package/src/client/nodes/GroupNode.tsx +39 -0
- package/src/client/nodes/ImageNode.tsx +160 -0
- package/src/client/nodes/InlineFormatBar.tsx +169 -0
- package/src/client/nodes/InlineMarkdownEditor.tsx +123 -0
- package/src/client/nodes/LedgerNode.tsx +37 -0
- package/src/client/nodes/MarkdownNode.tsx +359 -0
- package/src/client/nodes/McpAppNode.tsx +85 -0
- package/src/client/nodes/MdFormatBar.tsx +109 -0
- package/src/client/nodes/PromptNode.tsx +597 -0
- package/src/client/nodes/ResponseNode.tsx +153 -0
- package/src/client/nodes/StatusNode.tsx +84 -0
- package/src/client/nodes/StatusSummary.tsx +38 -0
- package/src/client/nodes/TraceNode.tsx +120 -0
- package/src/client/nodes/WebpageNode.tsx +288 -0
- package/src/client/nodes/image-warnings.ts +95 -0
- package/src/client/nodes/inline-editor-commands.ts +37 -0
- package/src/client/nodes/md-format.ts +206 -0
- package/src/client/state/attention-bridge.ts +328 -0
- package/src/client/state/attention-store.ts +73 -0
- package/src/client/state/canvas-store.ts +631 -0
- package/src/client/state/intent-bridge.ts +315 -0
- package/src/client/state/sse-bridge.ts +965 -0
- package/src/client/theme/global.css +3173 -0
- package/src/client/theme/tokens.ts +72 -0
- package/src/client/types-shims.d.ts +5 -0
- package/src/client/types.ts +81 -0
- package/src/client/utils/ext-app-tool-result.ts +4 -0
- package/src/client/utils/placement.ts +4 -0
- package/src/client/utils/platform.ts +2 -0
- package/src/json-render/catalog.ts +256 -0
- package/src/json-render/charts/components.tsx +198 -0
- package/src/json-render/charts/definitions.ts +81 -0
- package/src/json-render/charts/extra-components.tsx +267 -0
- package/src/json-render/charts/extra-definitions.ts +145 -0
- package/src/json-render/renderer/index.css +174 -0
- package/src/json-render/renderer/index.tsx +86 -0
- package/src/json-render/schema.ts +62 -0
- package/src/json-render/server.ts +597 -0
- package/src/mcp/server.ts +1377 -0
- package/src/server/agent-context.ts +242 -0
- package/src/server/artifact-paths.ts +17 -0
- package/src/server/canvas-operations.ts +1279 -0
- package/src/server/canvas-provenance.ts +243 -0
- package/src/server/canvas-schema.ts +432 -0
- package/src/server/canvas-serialization.ts +95 -0
- package/src/server/canvas-state.ts +1134 -0
- package/src/server/canvas-validation.ts +114 -0
- package/src/server/chart-template.ts +449 -0
- package/src/server/code-graph.ts +370 -0
- package/src/server/context-cards.ts +31 -0
- package/src/server/diagram-presets.ts +71 -0
- package/src/server/ext-app-call-registry.ts +77 -0
- package/src/server/ext-app-tool-result.ts +4 -0
- package/src/server/file-watcher.ts +121 -0
- package/src/server/index.ts +647 -0
- package/src/server/mcp-app-candidate.ts +174 -0
- package/src/server/mcp-app-host.ts +814 -0
- package/src/server/mcp-app-runtime.ts +459 -0
- package/src/server/mutation-history.ts +350 -0
- package/src/server/placement.ts +125 -0
- package/src/server/server.ts +3846 -0
- package/src/server/spatial-analysis.ts +356 -0
- package/src/server/trace-manager.ts +333 -0
- package/src/server/web-artifacts/scripts/bundle-artifact.sh +167 -0
- package/src/server/web-artifacts/scripts/init-artifact.sh +426 -0
- package/src/server/web-artifacts/scripts/shadcn-components.tar.gz +0 -0
- package/src/server/web-artifacts.ts +442 -0
- package/src/server/webpage-node.ts +328 -0
- package/src/shared/auto-arrange.ts +439 -0
- package/src/shared/ext-app-tool-result.ts +76 -0
- package/src/shared/placement.ts +81 -0
- package/src/shared/semantic-attention.ts +598 -0
package/src/cli/agent.ts
ADDED
|
@@ -0,0 +1,2144 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Agent-native CLI for pmx-canvas.
|
|
4
|
+
*
|
|
5
|
+
* Designed for non-interactive use by coding agents:
|
|
6
|
+
* - Every input is a flag (no interactive prompts)
|
|
7
|
+
* - JSON output by default
|
|
8
|
+
* - Progressive --help discovery
|
|
9
|
+
* - Fail fast with actionable errors
|
|
10
|
+
* - Idempotent operations where possible
|
|
11
|
+
* - --yes for destructive actions, --dry-run for preview
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { openUrlInExternalBrowser } from '../server/server.js';
|
|
16
|
+
import {
|
|
17
|
+
ALL_SEMANTIC_WATCH_EVENT_TYPES,
|
|
18
|
+
formatCompactWatchEvent,
|
|
19
|
+
parseSemanticEventFilter,
|
|
20
|
+
parseSseStream,
|
|
21
|
+
SemanticWatchReducer,
|
|
22
|
+
} from './watch.js';
|
|
23
|
+
|
|
24
|
+
// ── Helpers ──────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
const DEFAULT_PORT = 4313;
|
|
27
|
+
|
|
28
|
+
interface CanvasSchemaField {
|
|
29
|
+
name: string;
|
|
30
|
+
type: string;
|
|
31
|
+
required: boolean;
|
|
32
|
+
description: string;
|
|
33
|
+
aliases?: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface CanvasSchemaType {
|
|
37
|
+
type: string;
|
|
38
|
+
kind: 'node' | 'virtual-node';
|
|
39
|
+
description: string;
|
|
40
|
+
endpoint: string;
|
|
41
|
+
fields: CanvasSchemaField[];
|
|
42
|
+
example: Record<string, unknown>;
|
|
43
|
+
notes?: string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface JsonRenderComponentSchema {
|
|
47
|
+
type: string;
|
|
48
|
+
description: string;
|
|
49
|
+
slots: string[];
|
|
50
|
+
example: unknown;
|
|
51
|
+
props: Array<{
|
|
52
|
+
name: string;
|
|
53
|
+
type: string;
|
|
54
|
+
required: boolean;
|
|
55
|
+
nullable: boolean;
|
|
56
|
+
}>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface CanvasSchemaResponse {
|
|
60
|
+
ok: true;
|
|
61
|
+
source: 'running-server';
|
|
62
|
+
version: string | null;
|
|
63
|
+
nodeTypes: CanvasSchemaType[];
|
|
64
|
+
jsonRender: {
|
|
65
|
+
rootShape: Record<string, string>;
|
|
66
|
+
components: JsonRenderComponentSchema[];
|
|
67
|
+
};
|
|
68
|
+
graph: {
|
|
69
|
+
graphTypes: Array<
|
|
70
|
+
'line' | 'bar' | 'pie' | 'area' | 'scatter' | 'radar' | 'stacked-bar' | 'composed'
|
|
71
|
+
>;
|
|
72
|
+
};
|
|
73
|
+
mcp: {
|
|
74
|
+
tools: string[];
|
|
75
|
+
resources: string[];
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getBaseUrl(): string {
|
|
80
|
+
const envUrl = process.env.PMX_CANVAS_URL;
|
|
81
|
+
if (envUrl) return envUrl.replace(/\/$/, '');
|
|
82
|
+
const port = process.env.PMX_CANVAS_PORT || DEFAULT_PORT;
|
|
83
|
+
return `http://localhost:${port}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function die(message: string, hint?: string): never {
|
|
87
|
+
const out: Record<string, string> = { error: message };
|
|
88
|
+
if (hint) out.hint = hint;
|
|
89
|
+
console.error(JSON.stringify(out));
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function output(data: unknown): void {
|
|
94
|
+
console.log(JSON.stringify(data, null, 2));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function api(
|
|
98
|
+
method: string,
|
|
99
|
+
path: string,
|
|
100
|
+
body?: Record<string, unknown>,
|
|
101
|
+
): Promise<unknown> {
|
|
102
|
+
const base = getBaseUrl();
|
|
103
|
+
const url = `${base}${path}`;
|
|
104
|
+
const opts: RequestInit = {
|
|
105
|
+
method,
|
|
106
|
+
headers: { 'Content-Type': 'application/json' },
|
|
107
|
+
};
|
|
108
|
+
if (body) opts.body = JSON.stringify(body);
|
|
109
|
+
|
|
110
|
+
let res: Response;
|
|
111
|
+
try {
|
|
112
|
+
res = await fetch(url, opts);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
die(
|
|
115
|
+
`Cannot connect to pmx-canvas at ${base}: ${error instanceof Error ? error.message : String(error)}`,
|
|
116
|
+
`Start the server first: pmx-canvas --no-open`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const text = await res.text();
|
|
121
|
+
let json: unknown;
|
|
122
|
+
try {
|
|
123
|
+
json = JSON.parse(text);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (!res.ok) die(`HTTP ${res.status}: ${text}`);
|
|
126
|
+
console.debug('[cli] response was not JSON', error);
|
|
127
|
+
return text;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!res.ok) {
|
|
131
|
+
const err = json as Record<string, unknown>;
|
|
132
|
+
die(
|
|
133
|
+
err.error ? String(err.error) : `HTTP ${res.status}`,
|
|
134
|
+
typeof err.hint === 'string' ? err.hint : undefined,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return json;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── Flag parsing ─────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
function parseFlags(args: string[]): { positional: string[]; flags: Record<string, string | true> } {
|
|
143
|
+
const positional: string[] = [];
|
|
144
|
+
const flags: Record<string, string | true> = {};
|
|
145
|
+
// Boolean-only flags (never take a value argument)
|
|
146
|
+
const BOOL_FLAGS = new Set([
|
|
147
|
+
'help', 'h', 'ids', 'stdin', 'yes', 'list', 'clear', 'set', 'animated', 'dry-run',
|
|
148
|
+
'no-open-in-canvas', 'lock-arrange', 'unlock-arrange', 'json', 'compact', 'summary',
|
|
149
|
+
'verbose', 'include-logs',
|
|
150
|
+
]);
|
|
151
|
+
for (let i = 0; i < args.length; i++) {
|
|
152
|
+
const arg = args[i];
|
|
153
|
+
if (arg.startsWith('--')) {
|
|
154
|
+
const eq = arg.indexOf('=');
|
|
155
|
+
if (eq !== -1) {
|
|
156
|
+
flags[arg.slice(2, eq)] = arg.slice(eq + 1);
|
|
157
|
+
} else {
|
|
158
|
+
const key = arg.slice(2);
|
|
159
|
+
// If not a boolean flag and next arg exists and isn't a flag, consume it as value
|
|
160
|
+
if (!BOOL_FLAGS.has(key) && i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
|
161
|
+
flags[key] = args[++i];
|
|
162
|
+
} else {
|
|
163
|
+
flags[key] = true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} else if (arg.startsWith('-') && arg.length === 2) {
|
|
167
|
+
flags[arg.slice(1)] = true;
|
|
168
|
+
} else {
|
|
169
|
+
positional.push(arg);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return { positional, flags };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function requireFlag(flags: Record<string, string | true>, name: string, hint: string): string {
|
|
176
|
+
const val = flags[name];
|
|
177
|
+
if (!val || val === true) {
|
|
178
|
+
die(`Missing required flag: --${name}`, hint);
|
|
179
|
+
}
|
|
180
|
+
return val;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function getStringFlag(
|
|
184
|
+
flags: Record<string, string | true>,
|
|
185
|
+
...names: string[]
|
|
186
|
+
): string | undefined {
|
|
187
|
+
for (const name of names) {
|
|
188
|
+
const value = flags[name];
|
|
189
|
+
if (typeof value === 'string' && value.length > 0) return value;
|
|
190
|
+
}
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function optionalNumberFlag(flags: Record<string, string | true>, name: string, hint: string): number | undefined {
|
|
195
|
+
const val = flags[name];
|
|
196
|
+
if (!val || val === true) return undefined;
|
|
197
|
+
const parsed = Number(val);
|
|
198
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
199
|
+
die(`Invalid value for --${name}: ${String(val)}`, hint);
|
|
200
|
+
}
|
|
201
|
+
return Math.floor(parsed);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function optionalFiniteFlag(flags: Record<string, string | true>, name: string, hint: string): number | undefined {
|
|
205
|
+
const val = flags[name];
|
|
206
|
+
if (!val || val === true) return undefined;
|
|
207
|
+
const parsed = Number(val);
|
|
208
|
+
if (!Number.isFinite(parsed)) {
|
|
209
|
+
die(`Invalid value for --${name}: ${String(val)}`, hint);
|
|
210
|
+
}
|
|
211
|
+
return parsed;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function optionalPositiveFiniteFlag(flags: Record<string, string | true>, name: string, hint: string): number | undefined {
|
|
215
|
+
const parsed = optionalFiniteFlag(flags, name, hint);
|
|
216
|
+
if (parsed === undefined) return undefined;
|
|
217
|
+
if (parsed <= 0) {
|
|
218
|
+
die(`Invalid value for --${name}: ${String(flags[name])}`, hint);
|
|
219
|
+
}
|
|
220
|
+
return parsed;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
224
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function parseStringListFlag(
|
|
228
|
+
flags: Record<string, string | true>,
|
|
229
|
+
name: string,
|
|
230
|
+
hint: string,
|
|
231
|
+
): string[] | undefined {
|
|
232
|
+
const raw = getStringFlag(flags, name);
|
|
233
|
+
if (raw === undefined) return undefined;
|
|
234
|
+
const trimmed = raw.trim();
|
|
235
|
+
if (!trimmed) {
|
|
236
|
+
die(`Invalid value for --${name}: expected at least one string.`, hint);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (trimmed.startsWith('[')) {
|
|
240
|
+
const parsed = parseJsonValue(trimmed, `value for --${name}`, hint);
|
|
241
|
+
if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== 'string' || item.trim().length === 0)) {
|
|
242
|
+
die(`Invalid value for --${name}: expected a JSON array of non-empty strings.`, hint);
|
|
243
|
+
}
|
|
244
|
+
return parsed.map((item) => item.trim());
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const values = trimmed
|
|
248
|
+
.split(',')
|
|
249
|
+
.map((value) => value.trim())
|
|
250
|
+
.filter(Boolean);
|
|
251
|
+
if (values.length === 0) {
|
|
252
|
+
die(`Invalid value for --${name}: expected a comma-separated list of keys.`, hint);
|
|
253
|
+
}
|
|
254
|
+
return values;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function truncateText(value: string, maxLength = 240): string {
|
|
258
|
+
const normalized = value.replace(/\s+/g, ' ').trim();
|
|
259
|
+
if (normalized.length <= maxLength) return normalized;
|
|
260
|
+
if (maxLength <= 1) return normalized.slice(0, maxLength);
|
|
261
|
+
return `${normalized.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function summarizeGraphConfig(config: Record<string, unknown>): Record<string, unknown> {
|
|
265
|
+
const summary: Record<string, unknown> = {};
|
|
266
|
+
for (const [key, value] of Object.entries(config)) {
|
|
267
|
+
if (key === 'data' && Array.isArray(value)) {
|
|
268
|
+
summary.dataPoints = value.length;
|
|
269
|
+
const first = value[0];
|
|
270
|
+
if (isRecord(first)) summary.dataKeys = Object.keys(first);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
summary[key] = value;
|
|
274
|
+
}
|
|
275
|
+
return summary;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function summarizeNodeResult(node: Record<string, unknown>): Record<string, unknown> {
|
|
279
|
+
const data = isRecord(node.data) ? node.data : {};
|
|
280
|
+
const hiddenDataKeys = new Set(['content', 'fileContent', 'html', 'rendered', 'spec', 'toolResult']);
|
|
281
|
+
const dataKeys = Object.keys(data)
|
|
282
|
+
.filter((key) => !hiddenDataKeys.has(key))
|
|
283
|
+
.sort();
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
...(node.ok !== undefined ? { ok: node.ok } : {}),
|
|
287
|
+
id: node.id ?? null,
|
|
288
|
+
type: node.type ?? null,
|
|
289
|
+
title: node.title ?? null,
|
|
290
|
+
...(typeof node.content === 'string' ? { contentPreview: truncateText(node.content) } : {}),
|
|
291
|
+
...(node.position !== undefined ? { position: node.position } : {}),
|
|
292
|
+
...(node.size !== undefined ? { size: node.size } : {}),
|
|
293
|
+
...(node.collapsed !== undefined ? { collapsed: node.collapsed } : {}),
|
|
294
|
+
...(node.pinned !== undefined ? { pinned: node.pinned } : {}),
|
|
295
|
+
...(node.dockPosition !== undefined ? { dockPosition: node.dockPosition } : {}),
|
|
296
|
+
...(node.path !== undefined ? { path: node.path } : {}),
|
|
297
|
+
...(node.url !== undefined ? { url: node.url } : {}),
|
|
298
|
+
...(node.provenance !== undefined ? { provenance: node.provenance } : {}),
|
|
299
|
+
...(typeof data.mode === 'string' ? { mode: data.mode } : {}),
|
|
300
|
+
...(typeof data.viewerType === 'string' ? { viewerType: data.viewerType } : {}),
|
|
301
|
+
...(typeof data.serverName === 'string' ? { serverName: data.serverName } : {}),
|
|
302
|
+
...(typeof data.toolName === 'string' ? { toolName: data.toolName } : {}),
|
|
303
|
+
...(typeof data.appSessionId === 'string' ? { appSessionId: data.appSessionId } : {}),
|
|
304
|
+
...(typeof data.sessionStatus === 'string' ? { sessionStatus: data.sessionStatus } : {}),
|
|
305
|
+
...(typeof data.hostMode === 'string' ? { hostMode: data.hostMode } : {}),
|
|
306
|
+
...(typeof data.resourceUri === 'string' ? { resourceUri: data.resourceUri } : {}),
|
|
307
|
+
...(isRecord(data.graphConfig) ? { graph: summarizeGraphConfig(data.graphConfig) } : {}),
|
|
308
|
+
...(dataKeys.length > 0 ? { dataKeys } : {}),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function collectFlagValues(args: string[], name: string): string[] {
|
|
313
|
+
const values: string[] = [];
|
|
314
|
+
for (let i = 0; i < args.length; i++) {
|
|
315
|
+
const arg = args[i];
|
|
316
|
+
const prefix = `--${name}=`;
|
|
317
|
+
if (arg.startsWith(prefix)) {
|
|
318
|
+
const value = arg.slice(prefix.length).trim();
|
|
319
|
+
if (value) values.push(value);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (arg === `--${name}` && i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
|
323
|
+
values.push(args[i + 1] as string);
|
|
324
|
+
i++;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return values;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function collectRequestedFields(
|
|
331
|
+
args: string[],
|
|
332
|
+
flags: Record<string, string | true>,
|
|
333
|
+
): string[] {
|
|
334
|
+
const requested = [
|
|
335
|
+
...collectFlagValues(args, 'field'),
|
|
336
|
+
...((typeof flags.fields === 'string')
|
|
337
|
+
? flags.fields.split(',').map((value) => value.trim()).filter(Boolean)
|
|
338
|
+
: []),
|
|
339
|
+
];
|
|
340
|
+
return Array.from(new Set(requested));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function resolvePathValue(source: unknown, path: string[]): unknown {
|
|
344
|
+
let current = source;
|
|
345
|
+
for (const segment of path) {
|
|
346
|
+
if (!isRecord(current) && !Array.isArray(current)) return undefined;
|
|
347
|
+
current = (current as Record<string, unknown>)[segment];
|
|
348
|
+
}
|
|
349
|
+
return current;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function resolveNodeFieldValue(node: Record<string, unknown>, field: string): unknown {
|
|
353
|
+
if (field.includes('.')) {
|
|
354
|
+
const direct = resolvePathValue(node, field.split('.'));
|
|
355
|
+
if (direct !== undefined) return direct;
|
|
356
|
+
}
|
|
357
|
+
if (field in node) return node[field];
|
|
358
|
+
|
|
359
|
+
const data = isRecord(node.data) ? node.data : null;
|
|
360
|
+
if (!data) return undefined;
|
|
361
|
+
if (field in data) return data[field];
|
|
362
|
+
return field.includes('.') ? resolvePathValue(data, field.split('.')) : undefined;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function listAvailableNodeFields(node: Record<string, unknown>): string[] {
|
|
366
|
+
const topLevel = Object.keys(node).filter((key) => key !== 'data');
|
|
367
|
+
const data = isRecord(node.data) ? Object.keys(node.data).flatMap((key) => [key, `data.${key}`]) : [];
|
|
368
|
+
return Array.from(new Set([...topLevel, ...data])).sort();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function summarizeHistoryResult(result: Record<string, unknown>): Record<string, unknown> {
|
|
372
|
+
const entries = Array.isArray(result.entries)
|
|
373
|
+
? result.entries.filter(isRecord)
|
|
374
|
+
: [];
|
|
375
|
+
const countsByOperation: Record<string, number> = {};
|
|
376
|
+
let currentIndex = 0;
|
|
377
|
+
|
|
378
|
+
entries.forEach((entry, index) => {
|
|
379
|
+
const op = typeof entry.operationType === 'string' ? entry.operationType : 'unknown';
|
|
380
|
+
countsByOperation[op] = (countsByOperation[op] ?? 0) + 1;
|
|
381
|
+
if (entry.isCurrent === true) currentIndex = index + 1;
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const recent = entries.slice(-10).map((entry, index) => ({
|
|
385
|
+
index: entries.length - Math.min(entries.length, 10) + index + 1,
|
|
386
|
+
operationType: entry.operationType,
|
|
387
|
+
description: entry.description,
|
|
388
|
+
status: entry.isCurrent === true ? 'current' : entry.isUndone === true ? 'undone' : 'applied',
|
|
389
|
+
}));
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
totalMutations: entries.length,
|
|
393
|
+
currentIndex,
|
|
394
|
+
canUndo: result.canUndo === true,
|
|
395
|
+
canRedo: result.canRedo === true,
|
|
396
|
+
countsByOperation,
|
|
397
|
+
recent,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function compactHistoryResult(result: Record<string, unknown>): Record<string, unknown> {
|
|
402
|
+
const entries = Array.isArray(result.entries)
|
|
403
|
+
? result.entries.filter(isRecord)
|
|
404
|
+
: [];
|
|
405
|
+
return {
|
|
406
|
+
totalMutations: entries.length,
|
|
407
|
+
canUndo: result.canUndo === true,
|
|
408
|
+
canRedo: result.canRedo === true,
|
|
409
|
+
entries: entries.slice(-20).map((entry, index) => ({
|
|
410
|
+
index: entries.length - Math.min(entries.length, 20) + index + 1,
|
|
411
|
+
operationType: entry.operationType,
|
|
412
|
+
description: entry.description,
|
|
413
|
+
status: entry.isCurrent === true ? 'current' : entry.isUndone === true ? 'undone' : 'applied',
|
|
414
|
+
})),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function parseRecordArrayJson(raw: string, hint: string): Array<Record<string, unknown>> {
|
|
419
|
+
let parsed: unknown;
|
|
420
|
+
try {
|
|
421
|
+
parsed = JSON.parse(raw);
|
|
422
|
+
} catch (error) {
|
|
423
|
+
die(
|
|
424
|
+
`Invalid JSON dataset: ${error instanceof Error ? error.message : String(error)}`,
|
|
425
|
+
hint,
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (!Array.isArray(parsed) || parsed.some((item) => !isRecord(item))) {
|
|
430
|
+
die('Graph data must be a JSON array of objects.', hint);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return parsed;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function parseJsonValue(raw: string, label: string, hint: string): unknown {
|
|
437
|
+
try {
|
|
438
|
+
return JSON.parse(raw);
|
|
439
|
+
} catch (error) {
|
|
440
|
+
die(
|
|
441
|
+
`Invalid ${label}: ${error instanceof Error ? error.message : String(error)}`,
|
|
442
|
+
hint,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async function readTextInput(
|
|
448
|
+
flags: Record<string, string | true>,
|
|
449
|
+
options: {
|
|
450
|
+
fileFlags?: string[];
|
|
451
|
+
valueFlags?: string[];
|
|
452
|
+
allowStdin?: boolean;
|
|
453
|
+
label: string;
|
|
454
|
+
hint: string;
|
|
455
|
+
requiredMessage: string;
|
|
456
|
+
},
|
|
457
|
+
): Promise<string> {
|
|
458
|
+
for (const name of options.fileFlags ?? []) {
|
|
459
|
+
const path = getStringFlag(flags, name);
|
|
460
|
+
if (!path) continue;
|
|
461
|
+
try {
|
|
462
|
+
return readFileSync(path, 'utf-8');
|
|
463
|
+
} catch (error) {
|
|
464
|
+
die(
|
|
465
|
+
`Unable to read --${name}: ${error instanceof Error ? error.message : String(error)}`,
|
|
466
|
+
options.hint,
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
for (const name of options.valueFlags ?? []) {
|
|
472
|
+
const value = getStringFlag(flags, name);
|
|
473
|
+
if (value !== undefined) return value;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (options.allowStdin && flags.stdin) {
|
|
477
|
+
return await readStdin();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
die(options.requiredMessage, options.hint);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function applyCommonGeometryFlags(
|
|
484
|
+
body: Record<string, unknown>,
|
|
485
|
+
flags: Record<string, string | true>,
|
|
486
|
+
hints: { x: string; y: string; width: string; height: string },
|
|
487
|
+
): void {
|
|
488
|
+
const x = optionalFiniteFlag(flags, 'x', hints.x);
|
|
489
|
+
const y = optionalFiniteFlag(flags, 'y', hints.y);
|
|
490
|
+
const width = optionalPositiveFiniteFlag(flags, 'width', hints.width);
|
|
491
|
+
const height = optionalPositiveFiniteFlag(flags, 'height', hints.height);
|
|
492
|
+
if (x !== undefined) body.x = x;
|
|
493
|
+
if (y !== undefined) body.y = y;
|
|
494
|
+
if (width !== undefined) body.width = width;
|
|
495
|
+
if (height !== undefined) body.height = height;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async function buildJsonRenderRequestBody(
|
|
499
|
+
flags: Record<string, string | true>,
|
|
500
|
+
): Promise<Record<string, unknown>> {
|
|
501
|
+
const hint =
|
|
502
|
+
'Use: pmx-canvas node add --type json-render --title "Ops Dashboard" --spec-file ./dashboard.json';
|
|
503
|
+
const title = typeof flags.title === 'string' ? flags.title.trim() : '';
|
|
504
|
+
if (!title) {
|
|
505
|
+
die('json-render nodes require --title.', hint);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const rawSpec = await readTextInput(flags, {
|
|
509
|
+
fileFlags: ['spec-file'],
|
|
510
|
+
valueFlags: ['spec-json'],
|
|
511
|
+
allowStdin: true,
|
|
512
|
+
label: 'JSON spec',
|
|
513
|
+
hint,
|
|
514
|
+
requiredMessage: 'json-render nodes require --spec-file, --spec-json, or --stdin.',
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
const spec = parseJsonValue(rawSpec, 'JSON spec', hint);
|
|
518
|
+
const body: Record<string, unknown> = { title, spec };
|
|
519
|
+
applyCommonGeometryFlags(body, flags, {
|
|
520
|
+
x: 'Use a finite number, e.g. --x 500',
|
|
521
|
+
y: 'Use a finite number, e.g. --y 300',
|
|
522
|
+
width: 'Use a positive number, e.g. --width 840',
|
|
523
|
+
height: 'Use a positive number, e.g. --height 620',
|
|
524
|
+
});
|
|
525
|
+
return body;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async function buildGraphRequestBody(
|
|
529
|
+
flags: Record<string, string | true>,
|
|
530
|
+
): Promise<Record<string, unknown>> {
|
|
531
|
+
const hint =
|
|
532
|
+
'Use: pmx-canvas node add --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value';
|
|
533
|
+
const rawData = await readTextInput(flags, {
|
|
534
|
+
fileFlags: ['data-file'],
|
|
535
|
+
valueFlags: ['data-json'],
|
|
536
|
+
allowStdin: true,
|
|
537
|
+
label: 'graph JSON dataset',
|
|
538
|
+
hint,
|
|
539
|
+
requiredMessage: 'Graph nodes require --data-file, --data-json, or --stdin JSON data.',
|
|
540
|
+
});
|
|
541
|
+
const data = parseRecordArrayJson(rawData, hint);
|
|
542
|
+
|
|
543
|
+
const body: Record<string, unknown> = {
|
|
544
|
+
graphType: getStringFlag(flags, 'graph-type') ?? 'line',
|
|
545
|
+
data,
|
|
546
|
+
};
|
|
547
|
+
if (typeof flags.title === 'string') body.title = flags.title;
|
|
548
|
+
if (typeof flags['x-key'] === 'string') body.xKey = flags['x-key'];
|
|
549
|
+
if (typeof flags['y-key'] === 'string') body.yKey = flags['y-key'];
|
|
550
|
+
if (typeof flags['z-key'] === 'string') body.zKey = flags['z-key'];
|
|
551
|
+
if (typeof flags['name-key'] === 'string') body.nameKey = flags['name-key'];
|
|
552
|
+
if (typeof flags['value-key'] === 'string') body.valueKey = flags['value-key'];
|
|
553
|
+
if (typeof flags['axis-key'] === 'string') body.axisKey = flags['axis-key'];
|
|
554
|
+
const metrics = parseStringListFlag(flags, 'metrics', 'Use a comma-separated list, e.g. --metrics north,south');
|
|
555
|
+
const series = parseStringListFlag(flags, 'series', 'Use a comma-separated list, e.g. --series north,south');
|
|
556
|
+
if (metrics) body.metrics = metrics;
|
|
557
|
+
if (series) body.series = series;
|
|
558
|
+
if (typeof flags['bar-key'] === 'string') body.barKey = flags['bar-key'];
|
|
559
|
+
if (typeof flags['line-key'] === 'string') body.lineKey = flags['line-key'];
|
|
560
|
+
if (flags.aggregate === 'sum' || flags.aggregate === 'count' || flags.aggregate === 'avg') {
|
|
561
|
+
body.aggregate = flags.aggregate;
|
|
562
|
+
}
|
|
563
|
+
if (typeof flags.color === 'string') body.color = flags.color;
|
|
564
|
+
if (typeof flags['bar-color'] === 'string') body.barColor = flags['bar-color'];
|
|
565
|
+
if (typeof flags['line-color'] === 'string') body.lineColor = flags['line-color'];
|
|
566
|
+
|
|
567
|
+
const chartHeight = optionalPositiveFiniteFlag(flags, 'chart-height', 'Use a positive number, e.g. --chart-height 300');
|
|
568
|
+
const x = optionalFiniteFlag(flags, 'x', 'Use a finite number, e.g. --x 500');
|
|
569
|
+
const y = optionalFiniteFlag(flags, 'y', 'Use a finite number, e.g. --y 300');
|
|
570
|
+
const width = optionalPositiveFiniteFlag(flags, 'width', 'Use a positive number, e.g. --width 760');
|
|
571
|
+
const nodeHeight = optionalPositiveFiniteFlag(flags, 'height', 'Use a positive number, e.g. --height 520');
|
|
572
|
+
if (chartHeight !== undefined) body.height = chartHeight;
|
|
573
|
+
if (x !== undefined) body.x = x;
|
|
574
|
+
if (y !== undefined) body.y = y;
|
|
575
|
+
if (width !== undefined) body.width = width;
|
|
576
|
+
if (nodeHeight !== undefined) body.nodeHeight = nodeHeight;
|
|
577
|
+
return body;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async function buildWebArtifactRequestBody(
|
|
581
|
+
flags: Record<string, string | true>,
|
|
582
|
+
): Promise<Record<string, unknown>> {
|
|
583
|
+
const hint = 'Use: pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx';
|
|
584
|
+
const title = requireFlag(flags, 'title', hint);
|
|
585
|
+
const appTsx = await readTextInput(flags, {
|
|
586
|
+
fileFlags: ['app-file'],
|
|
587
|
+
valueFlags: ['app-tsx'],
|
|
588
|
+
allowStdin: true,
|
|
589
|
+
label: 'App.tsx',
|
|
590
|
+
hint,
|
|
591
|
+
requiredMessage: 'web-artifact build requires --app-file, --app-tsx, or --stdin.',
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
const body: Record<string, unknown> = { title, appTsx };
|
|
595
|
+
|
|
596
|
+
const indexCssFile = getStringFlag(flags, 'index-css-file');
|
|
597
|
+
const indexCss = getStringFlag(flags, 'index-css');
|
|
598
|
+
if (indexCssFile) {
|
|
599
|
+
body.indexCss = readFileSync(indexCssFile, 'utf-8');
|
|
600
|
+
} else if (indexCss !== undefined) {
|
|
601
|
+
body.indexCss = indexCss;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const mainFile = getStringFlag(flags, 'main-file');
|
|
605
|
+
const mainTsx = getStringFlag(flags, 'main-tsx');
|
|
606
|
+
if (mainFile) {
|
|
607
|
+
body.mainTsx = readFileSync(mainFile, 'utf-8');
|
|
608
|
+
} else if (mainTsx !== undefined) {
|
|
609
|
+
body.mainTsx = mainTsx;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const indexHtmlFile = getStringFlag(flags, 'index-html-file');
|
|
613
|
+
const indexHtml = getStringFlag(flags, 'index-html');
|
|
614
|
+
if (indexHtmlFile) {
|
|
615
|
+
body.indexHtml = readFileSync(indexHtmlFile, 'utf-8');
|
|
616
|
+
} else if (indexHtml !== undefined) {
|
|
617
|
+
body.indexHtml = indexHtml;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (typeof flags['project-path'] === 'string') body.projectPath = flags['project-path'];
|
|
621
|
+
if (typeof flags['output-path'] === 'string') body.outputPath = flags['output-path'];
|
|
622
|
+
if (typeof flags['init-script-path'] === 'string') body.initScriptPath = flags['init-script-path'];
|
|
623
|
+
if (typeof flags['bundle-script-path'] === 'string') body.bundleScriptPath = flags['bundle-script-path'];
|
|
624
|
+
if (flags['no-open-in-canvas']) body.openInCanvas = false;
|
|
625
|
+
if (flags.verbose || flags['include-logs']) body.includeLogs = true;
|
|
626
|
+
|
|
627
|
+
const timeoutMs = optionalPositiveFiniteFlag(flags, 'timeout-ms', 'Use a positive number, e.g. --timeout-ms 600000');
|
|
628
|
+
if (timeoutMs !== undefined) body.timeoutMs = timeoutMs;
|
|
629
|
+
|
|
630
|
+
return body;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
async function runWebArtifactBuildCommand(flags: Record<string, string | true>): Promise<void> {
|
|
634
|
+
const result = await api('POST', '/api/canvas/web-artifact', await buildWebArtifactRequestBody(flags));
|
|
635
|
+
output(result);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async function loadCanvasSchema(): Promise<CanvasSchemaResponse> {
|
|
639
|
+
const result = await api('GET', '/api/canvas/schema');
|
|
640
|
+
return result as CanvasSchemaResponse;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function fieldMatches(field: { name: string; aliases?: string[] }, requested: string): boolean {
|
|
644
|
+
return field.name === requested || field.aliases?.includes(requested) === true;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function summarizeNodeSchema(schema: CanvasSchemaType): Record<string, unknown> {
|
|
648
|
+
return {
|
|
649
|
+
type: schema.type,
|
|
650
|
+
kind: schema.kind,
|
|
651
|
+
endpoint: schema.endpoint,
|
|
652
|
+
description: schema.description,
|
|
653
|
+
requiredFields: schema.fields.filter((field) => field.required).map((field) => field.name),
|
|
654
|
+
optionalFields: schema.fields.filter((field) => !field.required).map((field) => field.name),
|
|
655
|
+
exampleKeys: Object.keys(schema.example),
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function summarizeJsonRenderComponent(component: JsonRenderComponentSchema): Record<string, unknown> {
|
|
660
|
+
return {
|
|
661
|
+
type: component.type,
|
|
662
|
+
description: component.description,
|
|
663
|
+
slots: component.slots,
|
|
664
|
+
requiredProps: component.props.filter((prop) => prop.required).map((prop) => prop.name),
|
|
665
|
+
optionalProps: component.props.filter((prop) => !prop.required).map((prop) => prop.name),
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function printObjectJson(value: unknown): void {
|
|
670
|
+
output(value);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function printNodeSchemaHelp(schema: CanvasSchemaType): void {
|
|
674
|
+
console.log(`\npmx-canvas node add --type ${schema.type} — ${schema.description}\n`);
|
|
675
|
+
console.log(`Endpoint: ${schema.endpoint}`);
|
|
676
|
+
console.log('Flags:');
|
|
677
|
+
for (const field of schema.fields) {
|
|
678
|
+
const aliases = field.aliases?.length
|
|
679
|
+
? ` (aliases: ${field.aliases.map((alias) => `--${alias}`).join(', ')})`
|
|
680
|
+
: '';
|
|
681
|
+
console.log(
|
|
682
|
+
` --${field.name}${field.required ? ' [required]' : ''} <${field.type}> ${field.description}${aliases}`,
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
if (schema.notes?.length) {
|
|
686
|
+
console.log('\nNotes:');
|
|
687
|
+
for (const note of schema.notes) {
|
|
688
|
+
console.log(` - ${note}`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
console.log('\nCanonical example:');
|
|
692
|
+
console.log(JSON.stringify(schema.example, null, 2));
|
|
693
|
+
console.log('');
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
async function showNodeAddTypeHelp(flags: Record<string, string | true>): Promise<void> {
|
|
697
|
+
const requestedType = getStringFlag(flags, 'type');
|
|
698
|
+
if (!requestedType) {
|
|
699
|
+
showCommandHelp('node add');
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const schema = await loadCanvasSchema();
|
|
704
|
+
let payload: Record<string, unknown> | CanvasSchemaType | JsonRenderComponentSchema | undefined;
|
|
705
|
+
if (requestedType === 'json-render') {
|
|
706
|
+
const componentName = getStringFlag(flags, 'component');
|
|
707
|
+
if (componentName) {
|
|
708
|
+
const component = schema.jsonRender.components.find((entry) => entry.type === componentName);
|
|
709
|
+
if (!component) {
|
|
710
|
+
die(`Unknown json-render component: ${componentName}`, 'Run: pmx-canvas node schema --type json-render --summary');
|
|
711
|
+
}
|
|
712
|
+
const requestedField = getStringFlag(flags, 'field');
|
|
713
|
+
if (requestedField) {
|
|
714
|
+
const prop = component.props.find((entry) => entry.name === requestedField);
|
|
715
|
+
if (!prop) {
|
|
716
|
+
die(`Unknown json-render prop: ${requestedField}`, `Run: pmx-canvas node schema --type json-render --component ${componentName}`);
|
|
717
|
+
}
|
|
718
|
+
payload = {
|
|
719
|
+
command: 'node add',
|
|
720
|
+
type: requestedType,
|
|
721
|
+
component: componentName,
|
|
722
|
+
prop,
|
|
723
|
+
};
|
|
724
|
+
} else {
|
|
725
|
+
payload = flags.summary ? summarizeJsonRenderComponent(component) : component;
|
|
726
|
+
}
|
|
727
|
+
} else {
|
|
728
|
+
payload = flags.summary
|
|
729
|
+
? {
|
|
730
|
+
type: 'json-render',
|
|
731
|
+
description: 'Native structured UI panel rendered from a validated json-render spec.',
|
|
732
|
+
rootShape: schema.jsonRender.rootShape,
|
|
733
|
+
components: schema.jsonRender.components.map((entry) => summarizeJsonRenderComponent(entry)),
|
|
734
|
+
}
|
|
735
|
+
: {
|
|
736
|
+
type: 'json-render',
|
|
737
|
+
rootShape: schema.jsonRender.rootShape,
|
|
738
|
+
components: schema.jsonRender.components,
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
} else if (requestedType === 'graph') {
|
|
742
|
+
const graphSchema = schema.nodeTypes.find((entry) => entry.type === 'graph');
|
|
743
|
+
if (!graphSchema) die('Graph schema is unavailable on the running server.');
|
|
744
|
+
const requestedField = getStringFlag(flags, 'field');
|
|
745
|
+
if (requestedField) {
|
|
746
|
+
const field = graphSchema.fields.find((entry) => fieldMatches(entry, requestedField));
|
|
747
|
+
if (!field) {
|
|
748
|
+
die(`Unknown graph field: ${requestedField}`, 'Run: pmx-canvas node schema --type graph');
|
|
749
|
+
}
|
|
750
|
+
payload = {
|
|
751
|
+
command: 'node add',
|
|
752
|
+
...field,
|
|
753
|
+
};
|
|
754
|
+
} else {
|
|
755
|
+
payload = flags.summary ? summarizeNodeSchema(graphSchema) : graphSchema;
|
|
756
|
+
}
|
|
757
|
+
} else {
|
|
758
|
+
const nodeType = schema.nodeTypes.find((entry) => entry.type === requestedType);
|
|
759
|
+
if (!nodeType) {
|
|
760
|
+
die(`Unknown node type: ${requestedType}`, 'Run: pmx-canvas node schema --summary');
|
|
761
|
+
}
|
|
762
|
+
const requestedField = getStringFlag(flags, 'field');
|
|
763
|
+
if (requestedField) {
|
|
764
|
+
const field = nodeType.fields.find((entry) => fieldMatches(entry, requestedField));
|
|
765
|
+
if (!field) {
|
|
766
|
+
die(`Unknown node field: ${requestedField}`, `Run: pmx-canvas node schema --type ${requestedType}`);
|
|
767
|
+
}
|
|
768
|
+
payload = {
|
|
769
|
+
command: 'node add',
|
|
770
|
+
type: requestedType,
|
|
771
|
+
field,
|
|
772
|
+
};
|
|
773
|
+
} else {
|
|
774
|
+
payload = flags.summary ? summarizeNodeSchema(nodeType) : nodeType;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (flags.json) {
|
|
779
|
+
printObjectJson(payload);
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if ('fields' in (payload as CanvasSchemaType)) {
|
|
784
|
+
printNodeSchemaHelp(payload as CanvasSchemaType);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
console.log('');
|
|
789
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
790
|
+
console.log('');
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function filterNodeSchemaView(
|
|
794
|
+
schema: CanvasSchemaType,
|
|
795
|
+
flags: Record<string, string | true>,
|
|
796
|
+
): CanvasSchemaType | Record<string, unknown> {
|
|
797
|
+
const requestedField = getStringFlag(flags, 'field');
|
|
798
|
+
if (requestedField) {
|
|
799
|
+
const field = schema.fields.find((entry) => fieldMatches(entry, requestedField));
|
|
800
|
+
if (!field) {
|
|
801
|
+
die(`Unknown field: ${requestedField}`, `Run: pmx-canvas node schema --type ${schema.type}`);
|
|
802
|
+
}
|
|
803
|
+
return {
|
|
804
|
+
type: schema.type,
|
|
805
|
+
field,
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
return flags.summary ? summarizeNodeSchema(schema) : schema;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function filterJsonRenderSchemaView(
|
|
813
|
+
schema: CanvasSchemaResponse['jsonRender'],
|
|
814
|
+
flags: Record<string, string | true>,
|
|
815
|
+
): Record<string, unknown> | JsonRenderComponentSchema {
|
|
816
|
+
const componentName = getStringFlag(flags, 'component');
|
|
817
|
+
if (!componentName) {
|
|
818
|
+
return flags.summary
|
|
819
|
+
? {
|
|
820
|
+
rootShape: schema.rootShape,
|
|
821
|
+
components: schema.components.map((entry) => summarizeJsonRenderComponent(entry)),
|
|
822
|
+
}
|
|
823
|
+
: schema;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const component = schema.components.find((entry) => entry.type === componentName);
|
|
827
|
+
if (!component) {
|
|
828
|
+
die(`Unknown json-render component: ${componentName}`, 'Run: pmx-canvas node schema --type json-render --summary');
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const requestedField = getStringFlag(flags, 'field');
|
|
832
|
+
if (requestedField) {
|
|
833
|
+
const prop = component.props.find((entry) => entry.name === requestedField);
|
|
834
|
+
if (!prop) {
|
|
835
|
+
die(`Unknown json-render prop: ${requestedField}`, `Run: pmx-canvas node schema --type json-render --component ${componentName}`);
|
|
836
|
+
}
|
|
837
|
+
return {
|
|
838
|
+
component: componentName,
|
|
839
|
+
prop,
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
return flags.summary ? summarizeJsonRenderComponent(component) : component;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// ── Commands ─────────────────────────────────────────────────
|
|
847
|
+
|
|
848
|
+
const COMMANDS: Record<string, { run: (args: string[]) => Promise<void>; help: string; examples: string[] }> = {};
|
|
849
|
+
|
|
850
|
+
function cmd(
|
|
851
|
+
name: string,
|
|
852
|
+
help: string,
|
|
853
|
+
examples: string[],
|
|
854
|
+
run: (args: string[]) => Promise<void>,
|
|
855
|
+
) {
|
|
856
|
+
COMMANDS[name] = { run, help, examples };
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
cmd('open', 'Open the current workbench in the browser', [
|
|
860
|
+
'pmx-canvas open',
|
|
861
|
+
], async (args) => {
|
|
862
|
+
const { flags } = parseFlags(args);
|
|
863
|
+
if (flags.help || flags.h) return showCommandHelp('open');
|
|
864
|
+
|
|
865
|
+
const base = getBaseUrl();
|
|
866
|
+
try {
|
|
867
|
+
const response = await fetch(`${base}/health`);
|
|
868
|
+
if (!response.ok) {
|
|
869
|
+
die(`Cannot reach pmx-canvas health endpoint at ${base}: HTTP ${response.status}`);
|
|
870
|
+
}
|
|
871
|
+
} catch (error) {
|
|
872
|
+
die(
|
|
873
|
+
`Cannot connect to pmx-canvas at ${base}: ${error instanceof Error ? error.message : String(error)}`,
|
|
874
|
+
'Start the server first: pmx-canvas --no-open',
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const url = `${base}/workbench`;
|
|
879
|
+
if (!openUrlInExternalBrowser(url)) {
|
|
880
|
+
die(`Failed to open browser for ${url}`);
|
|
881
|
+
}
|
|
882
|
+
output({ ok: true, url });
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
// ── node add ─────────────────────────────────────────────────
|
|
886
|
+
cmd('node add', 'Add a node to the canvas', [
|
|
887
|
+
'pmx-canvas node add --type markdown --title "Design Doc" --content "# Overview"',
|
|
888
|
+
'pmx-canvas node add --type status --title "Build" --content "passing"',
|
|
889
|
+
'pmx-canvas node add --type file --content "src/index.ts"',
|
|
890
|
+
'pmx-canvas node add --type webpage --url "https://example.com/docs"',
|
|
891
|
+
'pmx-canvas node add --type markdown --title "Note" --x 100 --y 200',
|
|
892
|
+
'pmx-canvas node add --type json-render --title "Ops Dashboard" --spec-file ./dashboard.json',
|
|
893
|
+
'pmx-canvas node add --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
|
|
894
|
+
'pmx-canvas node add --type web-artifact --title "Dashboard" --app-file ./App.tsx',
|
|
895
|
+
], async (args) => {
|
|
896
|
+
const { flags } = parseFlags(args);
|
|
897
|
+
if (flags.help || flags.h) return showNodeAddTypeHelp(flags);
|
|
898
|
+
|
|
899
|
+
const type = (flags.type as string) || 'markdown';
|
|
900
|
+
|
|
901
|
+
if (type === 'json-render') {
|
|
902
|
+
const result = await api('POST', '/api/canvas/json-render', await buildJsonRenderRequestBody(flags));
|
|
903
|
+
output(result);
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
if (type === 'graph') {
|
|
908
|
+
const result = await api('POST', '/api/canvas/graph', await buildGraphRequestBody(flags));
|
|
909
|
+
output(result);
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
if (type === 'web-artifact') {
|
|
914
|
+
await runWebArtifactBuildCommand(flags);
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const body: Record<string, unknown> = { type };
|
|
919
|
+
if (flags.title) body.title = flags.title;
|
|
920
|
+
const webpageUrl = getStringFlag(flags, 'url');
|
|
921
|
+
if (type === 'webpage' && webpageUrl) {
|
|
922
|
+
body.url = webpageUrl;
|
|
923
|
+
} else if (flags.content) {
|
|
924
|
+
body.content = flags.content;
|
|
925
|
+
}
|
|
926
|
+
applyCommonGeometryFlags(body, flags, {
|
|
927
|
+
x: 'Use a finite number, e.g. --x 500',
|
|
928
|
+
y: 'Use a finite number, e.g. --y 300',
|
|
929
|
+
width: 'Use a positive number, e.g. --width 500',
|
|
930
|
+
height: 'Use a positive number, e.g. --height 280',
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
// Support --stdin for piping content
|
|
934
|
+
if (flags.stdin) {
|
|
935
|
+
if (type === 'webpage') {
|
|
936
|
+
body.url = await readStdin();
|
|
937
|
+
} else {
|
|
938
|
+
body.content = await readStdin();
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const result = await api('POST', '/api/canvas/node', body);
|
|
943
|
+
output(result);
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
cmd('node schema', 'Describe server-supported node create schemas and canonical examples', [
|
|
947
|
+
'pmx-canvas node schema',
|
|
948
|
+
'pmx-canvas node schema --type webpage',
|
|
949
|
+
'pmx-canvas node schema --type json-render',
|
|
950
|
+
'pmx-canvas node schema --type json-render --component Table',
|
|
951
|
+
'pmx-canvas node schema --type webpage --field url',
|
|
952
|
+
'pmx-canvas node schema --summary',
|
|
953
|
+
], async (args) => {
|
|
954
|
+
const { flags } = parseFlags(args);
|
|
955
|
+
if (flags.help || flags.h) return showCommandHelp('node schema');
|
|
956
|
+
|
|
957
|
+
const result = await loadCanvasSchema();
|
|
958
|
+
if (getStringFlag(flags, 'component') && flags.type !== 'json-render') {
|
|
959
|
+
die('--component is only supported with --type json-render.');
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (typeof flags.type !== 'string') {
|
|
963
|
+
if (flags.summary) {
|
|
964
|
+
output({
|
|
965
|
+
source: result.source,
|
|
966
|
+
version: result.version,
|
|
967
|
+
nodeTypes: result.nodeTypes.map((entry) => summarizeNodeSchema(entry)),
|
|
968
|
+
jsonRender: {
|
|
969
|
+
componentCount: result.jsonRender.components.length,
|
|
970
|
+
rootShape: result.jsonRender.rootShape,
|
|
971
|
+
},
|
|
972
|
+
graph: result.graph,
|
|
973
|
+
mcp: result.mcp,
|
|
974
|
+
});
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
output(result);
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const requested = flags.type;
|
|
982
|
+
if (requested === 'json-render') {
|
|
983
|
+
output(filterJsonRenderSchemaView(result.jsonRender, flags));
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
if (requested === 'graph') {
|
|
987
|
+
const graphSchema = result.nodeTypes.find((entry) => entry.type === 'graph');
|
|
988
|
+
if (graphSchema) {
|
|
989
|
+
output(filterNodeSchemaView(graphSchema, flags));
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
output(flags.summary ? result.graph : { ...result.graph });
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
const nodeType = result.nodeTypes.find((entry) => entry.type === requested);
|
|
996
|
+
if (nodeType) {
|
|
997
|
+
output(filterNodeSchemaView(nodeType, flags));
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
die(`Unknown schema type: ${requested}`, 'Run: pmx-canvas node schema');
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
// ── node list ────────────────────────────────────────────────
|
|
1004
|
+
cmd('node list', 'List all nodes on the canvas', [
|
|
1005
|
+
'pmx-canvas node list',
|
|
1006
|
+
'pmx-canvas node list --type markdown',
|
|
1007
|
+
'pmx-canvas node list --type mcp-app',
|
|
1008
|
+
'pmx-canvas node list --ids',
|
|
1009
|
+
], async (args) => {
|
|
1010
|
+
const { flags } = parseFlags(args);
|
|
1011
|
+
if (flags.help || flags.h) return showCommandHelp('node list');
|
|
1012
|
+
|
|
1013
|
+
const layout = (await api('GET', '/api/canvas/state')) as { nodes: Array<Record<string, unknown>> };
|
|
1014
|
+
let nodes = layout.nodes;
|
|
1015
|
+
|
|
1016
|
+
if (flags.type && flags.type !== true) {
|
|
1017
|
+
nodes = nodes.filter((n) => n.type === flags.type);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
if (flags.ids) {
|
|
1021
|
+
output(nodes.map((n) => n.id));
|
|
1022
|
+
} else {
|
|
1023
|
+
const shouldSummarize =
|
|
1024
|
+
flags.summary === true ||
|
|
1025
|
+
flags.compact === true ||
|
|
1026
|
+
flags.type === 'mcp-app';
|
|
1027
|
+
output(shouldSummarize ? nodes.map((node) => summarizeNodeResult(node)) : nodes);
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
// ── node get ─────────────────────────────────────────────────
|
|
1032
|
+
cmd('node get', 'Get a node by ID', [
|
|
1033
|
+
'pmx-canvas node get <node-id>',
|
|
1034
|
+
'pmx-canvas node get node-abc123',
|
|
1035
|
+
'pmx-canvas node get node-abc123 --summary',
|
|
1036
|
+
'pmx-canvas node get node-abc123 --field title --field graphConfig',
|
|
1037
|
+
], async (args) => {
|
|
1038
|
+
const { positional, flags } = parseFlags(args);
|
|
1039
|
+
if (flags.help || flags.h) return showCommandHelp('node get');
|
|
1040
|
+
|
|
1041
|
+
const id = positional[0];
|
|
1042
|
+
if (!id) die('Missing node ID', 'pmx-canvas node get <node-id>');
|
|
1043
|
+
|
|
1044
|
+
const result = await api('GET', `/api/canvas/node/${encodeURIComponent(id)}`) as Record<string, unknown>;
|
|
1045
|
+
const requestedFields = collectRequestedFields(args, flags);
|
|
1046
|
+
if (requestedFields.length > 0) {
|
|
1047
|
+
const picked = Object.fromEntries(requestedFields.map((field) => [field, resolveNodeFieldValue(result, field)]));
|
|
1048
|
+
const missing = requestedFields.filter((field) => picked[field] === undefined);
|
|
1049
|
+
if (missing.length > 0) {
|
|
1050
|
+
die(
|
|
1051
|
+
`Unknown node field${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}`,
|
|
1052
|
+
`Available fields: ${listAvailableNodeFields(result).join(', ')}`,
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
output({
|
|
1056
|
+
id: result.id ?? id,
|
|
1057
|
+
fields: picked,
|
|
1058
|
+
});
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
if (flags.summary || flags.compact) {
|
|
1063
|
+
output(summarizeNodeResult(result));
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
output(result);
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
// ── node update ──────────────────────────────────────────────
|
|
1070
|
+
cmd('node update', 'Update a node by ID', [
|
|
1071
|
+
'pmx-canvas node update <node-id> --title "New Title"',
|
|
1072
|
+
'pmx-canvas node update <node-id> --content "Updated content"',
|
|
1073
|
+
'pmx-canvas node update <node-id> --title "Moved" --x 500 --y 300',
|
|
1074
|
+
'pmx-canvas node update <node-id> --width 840 --height 620',
|
|
1075
|
+
'pmx-canvas node update <node-id> --lock-arrange',
|
|
1076
|
+
], async (args) => {
|
|
1077
|
+
const { positional, flags } = parseFlags(args);
|
|
1078
|
+
if (flags.help || flags.h) return showCommandHelp('node update');
|
|
1079
|
+
|
|
1080
|
+
const id = positional[0];
|
|
1081
|
+
if (!id) die('Missing node ID', 'pmx-canvas node update <node-id> --title "New Title"');
|
|
1082
|
+
|
|
1083
|
+
const body: Record<string, unknown> = {};
|
|
1084
|
+
if (flags.title && flags.title !== true) body.title = flags.title;
|
|
1085
|
+
if (flags.content && flags.content !== true) body.content = flags.content;
|
|
1086
|
+
if (flags.stdin) body.content = await readStdin();
|
|
1087
|
+
|
|
1088
|
+
const x = optionalFiniteFlag(flags, 'x', 'Use a finite number, e.g. --x 500');
|
|
1089
|
+
const y = optionalFiniteFlag(flags, 'y', 'Use a finite number, e.g. --y 300');
|
|
1090
|
+
const width = optionalPositiveFiniteFlag(flags, 'width', 'Use a positive number, e.g. --width 840');
|
|
1091
|
+
const height = optionalPositiveFiniteFlag(flags, 'height', 'Use a positive number, e.g. --height 620');
|
|
1092
|
+
if (flags['lock-arrange'] && flags['unlock-arrange']) {
|
|
1093
|
+
die('Use either --lock-arrange or --unlock-arrange, not both.');
|
|
1094
|
+
}
|
|
1095
|
+
const arrangeLocked = flags['lock-arrange']
|
|
1096
|
+
? true
|
|
1097
|
+
: flags['unlock-arrange']
|
|
1098
|
+
? false
|
|
1099
|
+
: undefined;
|
|
1100
|
+
|
|
1101
|
+
if (x !== undefined || y !== undefined || width !== undefined || height !== undefined || arrangeLocked !== undefined) {
|
|
1102
|
+
const existing = await api('GET', `/api/canvas/node/${encodeURIComponent(id)}`) as {
|
|
1103
|
+
position: { x: number; y: number };
|
|
1104
|
+
size: { width: number; height: number };
|
|
1105
|
+
data: Record<string, unknown>;
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
if (x !== undefined || y !== undefined) {
|
|
1109
|
+
body.position = {
|
|
1110
|
+
x: x ?? existing.position.x,
|
|
1111
|
+
y: y ?? existing.position.y,
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
if (width !== undefined || height !== undefined) {
|
|
1116
|
+
body.size = {
|
|
1117
|
+
width: width ?? existing.size.width,
|
|
1118
|
+
height: height ?? existing.size.height,
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
if (arrangeLocked !== undefined) {
|
|
1123
|
+
body.data = {
|
|
1124
|
+
...existing.data,
|
|
1125
|
+
arrangeLocked,
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
if (Object.keys(body).length === 0) {
|
|
1131
|
+
die(
|
|
1132
|
+
'No updates specified',
|
|
1133
|
+
'Use --title, --content, --x, --y, --width, --height, --lock-arrange, --unlock-arrange, or --stdin',
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
const result = await api('PATCH', `/api/canvas/node/${encodeURIComponent(id)}`, body);
|
|
1138
|
+
output(result);
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
// ── node remove ──────────────────────────────────────────────
|
|
1142
|
+
cmd('node remove', 'Remove a node from the canvas', [
|
|
1143
|
+
'pmx-canvas node remove <node-id>',
|
|
1144
|
+
'pmx-canvas node remove node-abc123',
|
|
1145
|
+
], async (args) => {
|
|
1146
|
+
const { positional, flags } = parseFlags(args);
|
|
1147
|
+
if (flags.help || flags.h) return showCommandHelp('node remove');
|
|
1148
|
+
|
|
1149
|
+
const id = positional[0];
|
|
1150
|
+
if (!id) die('Missing node ID', 'pmx-canvas node remove <node-id>');
|
|
1151
|
+
|
|
1152
|
+
const result = await api('DELETE', `/api/canvas/node/${encodeURIComponent(id)}`);
|
|
1153
|
+
output(result);
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
// ── edge add ─────────────────────────────────────────────────
|
|
1157
|
+
cmd('edge add', 'Add an edge between two nodes', [
|
|
1158
|
+
'pmx-canvas edge add --from <node-id> --to <node-id> --type flow',
|
|
1159
|
+
'pmx-canvas edge add --from-search "DVT O3 — GitOps" --to-search "deep work trend" --type relation',
|
|
1160
|
+
'pmx-canvas edge add --from n1 --to n2 --type depends-on --label "imports"',
|
|
1161
|
+
'pmx-canvas edge add --from n1 --to n2 --type references --style dashed --animated',
|
|
1162
|
+
], async (args) => {
|
|
1163
|
+
const { flags } = parseFlags(args);
|
|
1164
|
+
if (flags.help || flags.h) return showCommandHelp('edge add');
|
|
1165
|
+
|
|
1166
|
+
const type = (flags.type as string) || 'flow';
|
|
1167
|
+
const from = typeof flags.from === 'string' ? flags.from : undefined;
|
|
1168
|
+
const to = typeof flags.to === 'string' ? flags.to : undefined;
|
|
1169
|
+
const fromSearch = typeof flags['from-search'] === 'string' ? flags['from-search'] : undefined;
|
|
1170
|
+
const toSearch = typeof flags['to-search'] === 'string' ? flags['to-search'] : undefined;
|
|
1171
|
+
|
|
1172
|
+
if (!from && !fromSearch) {
|
|
1173
|
+
die(
|
|
1174
|
+
'Missing source selector',
|
|
1175
|
+
'Use --from <id> or --from-search "query". Search queries must resolve to exactly one node. Example: pmx-canvas edge add --from-search "DVT O3 — GitOps" --to-search "deep work trend" --type relation',
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
if (!to && !toSearch) {
|
|
1179
|
+
die(
|
|
1180
|
+
'Missing target selector',
|
|
1181
|
+
'Use --to <id> or --to-search "query". Search queries must resolve to exactly one node. Example: pmx-canvas edge add --from-search "DVT O3 — GitOps" --to-search "deep work trend" --type relation',
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
const body: Record<string, unknown> = {
|
|
1186
|
+
type,
|
|
1187
|
+
...(from ? { from } : {}),
|
|
1188
|
+
...(to ? { to } : {}),
|
|
1189
|
+
...(fromSearch ? { fromSearch } : {}),
|
|
1190
|
+
...(toSearch ? { toSearch } : {}),
|
|
1191
|
+
};
|
|
1192
|
+
if (flags.label && flags.label !== true) body.label = flags.label;
|
|
1193
|
+
if (typeof flags.style === 'string') body.style = flags.style;
|
|
1194
|
+
if (flags.animated) body.animated = true;
|
|
1195
|
+
|
|
1196
|
+
const result = await api('POST', '/api/canvas/edge', body);
|
|
1197
|
+
output(result);
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
// ── edge list ────────────────────────────────────────────────
|
|
1201
|
+
cmd('edge list', 'List all edges on the canvas', [
|
|
1202
|
+
'pmx-canvas edge list',
|
|
1203
|
+
], async (args) => {
|
|
1204
|
+
const { flags } = parseFlags(args);
|
|
1205
|
+
if (flags.help || flags.h) return showCommandHelp('edge list');
|
|
1206
|
+
|
|
1207
|
+
const layout = (await api('GET', '/api/canvas/state')) as { edges: unknown[] };
|
|
1208
|
+
output(layout.edges);
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
// ── edge remove ──────────────────────────────────────────────
|
|
1212
|
+
cmd('edge remove', 'Remove an edge by ID', [
|
|
1213
|
+
'pmx-canvas edge remove <edge-id>',
|
|
1214
|
+
], async (args) => {
|
|
1215
|
+
const { positional, flags } = parseFlags(args);
|
|
1216
|
+
if (flags.help || flags.h) return showCommandHelp('edge remove');
|
|
1217
|
+
|
|
1218
|
+
const id = positional[0];
|
|
1219
|
+
if (!id) die('Missing edge ID', 'pmx-canvas edge remove <edge-id>');
|
|
1220
|
+
|
|
1221
|
+
const result = await api('DELETE', '/api/canvas/edge', { edge_id: id });
|
|
1222
|
+
output(result);
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
// ── search ───────────────────────────────────────────────────
|
|
1226
|
+
cmd('search', 'Search nodes by title or content', [
|
|
1227
|
+
'pmx-canvas search "design doc"',
|
|
1228
|
+
'pmx-canvas search --query "TODO"',
|
|
1229
|
+
], async (args) => {
|
|
1230
|
+
const { positional, flags } = parseFlags(args);
|
|
1231
|
+
if (flags.help || flags.h) return showCommandHelp('search');
|
|
1232
|
+
|
|
1233
|
+
const query = positional[0] || (typeof flags.query === 'string' ? flags.query : '');
|
|
1234
|
+
if (!query) die('Missing search query', 'pmx-canvas search "query"');
|
|
1235
|
+
|
|
1236
|
+
const result = await api('GET', `/api/canvas/search?q=${encodeURIComponent(query)}`);
|
|
1237
|
+
output(result);
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
// ── layout ───────────────────────────────────────────────────
|
|
1241
|
+
cmd('layout', 'Get the full canvas layout (nodes, edges, viewport)', [
|
|
1242
|
+
'pmx-canvas layout',
|
|
1243
|
+
'pmx-canvas layout --summary',
|
|
1244
|
+
], async (args) => {
|
|
1245
|
+
const { flags } = parseFlags(args);
|
|
1246
|
+
if (flags.help || flags.h) return showCommandHelp('layout');
|
|
1247
|
+
|
|
1248
|
+
if (flags.summary || flags.compact) {
|
|
1249
|
+
output(await api('GET', '/api/canvas/summary'));
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
const result = await api('GET', '/api/canvas/state');
|
|
1253
|
+
output(result);
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
// ── status ───────────────────────────────────────────────────
|
|
1257
|
+
cmd('status', 'Quick canvas summary', [
|
|
1258
|
+
'pmx-canvas status',
|
|
1259
|
+
], async (args) => {
|
|
1260
|
+
const { flags } = parseFlags(args);
|
|
1261
|
+
if (flags.help || flags.h) return showCommandHelp('status');
|
|
1262
|
+
|
|
1263
|
+
const layout = (await api('GET', '/api/canvas/state')) as {
|
|
1264
|
+
nodes: Array<Record<string, unknown>>;
|
|
1265
|
+
edges: unknown[];
|
|
1266
|
+
viewport: unknown;
|
|
1267
|
+
};
|
|
1268
|
+
const pinned = (await api('GET', '/api/canvas/pinned-context')) as { count: number; nodeIds: string[] };
|
|
1269
|
+
|
|
1270
|
+
const typeCounts: Record<string, number> = {};
|
|
1271
|
+
for (const n of layout.nodes) {
|
|
1272
|
+
const t = n.type as string;
|
|
1273
|
+
typeCounts[t] = (typeCounts[t] || 0) + 1;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
output({
|
|
1277
|
+
nodes: layout.nodes.length,
|
|
1278
|
+
edges: layout.edges.length,
|
|
1279
|
+
pinned: pinned.count,
|
|
1280
|
+
types: typeCounts,
|
|
1281
|
+
viewport: layout.viewport,
|
|
1282
|
+
});
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
// ── arrange ──────────────────────────────────────────────────
|
|
1286
|
+
cmd('arrange', 'Auto-arrange nodes on the canvas', [
|
|
1287
|
+
'pmx-canvas arrange',
|
|
1288
|
+
'pmx-canvas arrange --layout column',
|
|
1289
|
+
'pmx-canvas arrange --layout flow',
|
|
1290
|
+
], async (args) => {
|
|
1291
|
+
const { flags } = parseFlags(args);
|
|
1292
|
+
if (flags.help || flags.h) return showCommandHelp('arrange');
|
|
1293
|
+
|
|
1294
|
+
const body: Record<string, unknown> = {};
|
|
1295
|
+
if (flags.layout && flags.layout !== true) body.layout = flags.layout;
|
|
1296
|
+
|
|
1297
|
+
const result = await api('POST', '/api/canvas/arrange', body);
|
|
1298
|
+
output(result);
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
// ── focus ────────────────────────────────────────────────────
|
|
1302
|
+
cmd('focus', 'Pan viewport to center on a node', [
|
|
1303
|
+
'pmx-canvas focus <node-id>',
|
|
1304
|
+
], async (args) => {
|
|
1305
|
+
const { positional, flags } = parseFlags(args);
|
|
1306
|
+
if (flags.help || flags.h) return showCommandHelp('focus');
|
|
1307
|
+
|
|
1308
|
+
const id = positional[0];
|
|
1309
|
+
if (!id) die('Missing node ID', 'pmx-canvas focus <node-id>');
|
|
1310
|
+
|
|
1311
|
+
const result = await api('POST', '/api/canvas/focus', { id });
|
|
1312
|
+
output(result);
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
// ── pin ──────────────────────────────────────────────────────
|
|
1316
|
+
cmd('pin', 'Manage context pins', [
|
|
1317
|
+
'pmx-canvas pin node1 node2 node3',
|
|
1318
|
+
'pmx-canvas pin --set node1 node2 node3',
|
|
1319
|
+
'pmx-canvas pin --list',
|
|
1320
|
+
'pmx-canvas pin --clear',
|
|
1321
|
+
], async (args) => {
|
|
1322
|
+
const { positional, flags } = parseFlags(args);
|
|
1323
|
+
if (flags.help || flags.h) return showCommandHelp('pin');
|
|
1324
|
+
|
|
1325
|
+
if (flags.list) {
|
|
1326
|
+
const result = await api('GET', '/api/canvas/pinned-context');
|
|
1327
|
+
output(result);
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
if (flags.clear) {
|
|
1332
|
+
const result = await api('POST', '/api/canvas/context-pins', { nodeIds: [] });
|
|
1333
|
+
output(result);
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// --set: positional args are node IDs
|
|
1338
|
+
if (positional.length > 0 || flags.set) {
|
|
1339
|
+
const result = await api('POST', '/api/canvas/context-pins', { nodeIds: positional });
|
|
1340
|
+
output(result);
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// Default: list
|
|
1345
|
+
const result = await api('GET', '/api/canvas/pinned-context');
|
|
1346
|
+
output(result);
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
// ── undo ─────────────────────────────────────────────────────
|
|
1350
|
+
cmd('undo', 'Undo the last canvas mutation', [
|
|
1351
|
+
'pmx-canvas undo',
|
|
1352
|
+
], async (args) => {
|
|
1353
|
+
const { flags } = parseFlags(args);
|
|
1354
|
+
if (flags.help || flags.h) return showCommandHelp('undo');
|
|
1355
|
+
|
|
1356
|
+
const result = await api('POST', '/api/canvas/undo');
|
|
1357
|
+
output(result);
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
// ── redo ─────────────────────────────────────────────────────
|
|
1361
|
+
cmd('redo', 'Redo the last undone mutation', [
|
|
1362
|
+
'pmx-canvas redo',
|
|
1363
|
+
], async (args) => {
|
|
1364
|
+
const { flags } = parseFlags(args);
|
|
1365
|
+
if (flags.help || flags.h) return showCommandHelp('redo');
|
|
1366
|
+
|
|
1367
|
+
const result = await api('POST', '/api/canvas/redo');
|
|
1368
|
+
output(result);
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
// ── history ──────────────────────────────────────────────────
|
|
1372
|
+
cmd('history', 'Show canvas mutation history', [
|
|
1373
|
+
'pmx-canvas history',
|
|
1374
|
+
'pmx-canvas history --summary',
|
|
1375
|
+
'pmx-canvas history --compact',
|
|
1376
|
+
], async (args) => {
|
|
1377
|
+
const { flags } = parseFlags(args);
|
|
1378
|
+
if (flags.help || flags.h) return showCommandHelp('history');
|
|
1379
|
+
|
|
1380
|
+
const result = await api('GET', '/api/canvas/history') as Record<string, unknown>;
|
|
1381
|
+
if (flags.summary) {
|
|
1382
|
+
output(summarizeHistoryResult(result));
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
if (flags.compact) {
|
|
1386
|
+
output(compactHistoryResult(result));
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
output(result);
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
// ── snapshot save ────────────────────────────────────────────
|
|
1393
|
+
cmd('snapshot save', 'Save a named snapshot of the current canvas', [
|
|
1394
|
+
'pmx-canvas snapshot save --name "before-refactor"',
|
|
1395
|
+
'pmx-canvas snapshot save --name checkpoint-1',
|
|
1396
|
+
], async (args) => {
|
|
1397
|
+
const { flags } = parseFlags(args);
|
|
1398
|
+
if (flags.help || flags.h) return showCommandHelp('snapshot save');
|
|
1399
|
+
|
|
1400
|
+
const name = requireFlag(flags, 'name', 'pmx-canvas snapshot save --name "my-snapshot"');
|
|
1401
|
+
const result = await api('POST', '/api/canvas/snapshots', { name });
|
|
1402
|
+
output(result);
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
// ── snapshot list ────────────────────────────────────────────
|
|
1406
|
+
cmd('snapshot list', 'List all saved snapshots', [
|
|
1407
|
+
'pmx-canvas snapshot list',
|
|
1408
|
+
], async (args) => {
|
|
1409
|
+
const { flags } = parseFlags(args);
|
|
1410
|
+
if (flags.help || flags.h) return showCommandHelp('snapshot list');
|
|
1411
|
+
|
|
1412
|
+
const result = await api('GET', '/api/canvas/snapshots');
|
|
1413
|
+
output(result);
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
// ── snapshot restore ─────────────────────────────────────────
|
|
1417
|
+
cmd('snapshot restore', 'Restore canvas from a snapshot', [
|
|
1418
|
+
'pmx-canvas snapshot restore <snapshot-id-or-name>',
|
|
1419
|
+
], async (args) => {
|
|
1420
|
+
const { positional, flags } = parseFlags(args);
|
|
1421
|
+
if (flags.help || flags.h) return showCommandHelp('snapshot restore');
|
|
1422
|
+
|
|
1423
|
+
const id = positional[0];
|
|
1424
|
+
if (!id) die('Missing snapshot ID or name', 'pmx-canvas snapshot restore <snapshot-id-or-name>');
|
|
1425
|
+
|
|
1426
|
+
const result = await api('POST', `/api/canvas/snapshots/${encodeURIComponent(id)}`);
|
|
1427
|
+
output(result);
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1430
|
+
// ── snapshot delete ──────────────────────────────────────────
|
|
1431
|
+
cmd('snapshot delete', 'Delete a saved snapshot', [
|
|
1432
|
+
'pmx-canvas snapshot delete <snapshot-id>',
|
|
1433
|
+
], async (args) => {
|
|
1434
|
+
const { positional, flags } = parseFlags(args);
|
|
1435
|
+
if (flags.help || flags.h) return showCommandHelp('snapshot delete');
|
|
1436
|
+
|
|
1437
|
+
const id = positional[0];
|
|
1438
|
+
if (!id) die('Missing snapshot ID', 'pmx-canvas snapshot delete <snapshot-id>');
|
|
1439
|
+
|
|
1440
|
+
const result = await api('DELETE', `/api/canvas/snapshots/${encodeURIComponent(id)}`);
|
|
1441
|
+
output(result);
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1444
|
+
async function runSnapshotDiff(args: string[]): Promise<void> {
|
|
1445
|
+
const { positional, flags } = parseFlags(args);
|
|
1446
|
+
const snapshot = positional[0];
|
|
1447
|
+
if (!snapshot) die('Missing snapshot ID or name', 'pmx-canvas snapshot diff <snapshot-id-or-name>');
|
|
1448
|
+
const result = await api('GET', `/api/canvas/snapshots/${encodeURIComponent(snapshot)}/diff`);
|
|
1449
|
+
output(result);
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// ── snapshot diff ────────────────────────────────────────────
|
|
1453
|
+
cmd('snapshot diff', 'Compare current canvas against a saved snapshot', [
|
|
1454
|
+
'pmx-canvas snapshot diff <snapshot-id>',
|
|
1455
|
+
'pmx-canvas snapshot diff "before-refactor"',
|
|
1456
|
+
], async (args) => {
|
|
1457
|
+
const { flags } = parseFlags(args);
|
|
1458
|
+
if (flags.help || flags.h) return showCommandHelp('snapshot diff');
|
|
1459
|
+
await runSnapshotDiff(args);
|
|
1460
|
+
});
|
|
1461
|
+
|
|
1462
|
+
// ── diff ─────────────────────────────────────────────────────
|
|
1463
|
+
cmd('diff', 'Compare current canvas against a snapshot', [
|
|
1464
|
+
'pmx-canvas diff <snapshot-id>',
|
|
1465
|
+
], async (args) => {
|
|
1466
|
+
const { flags } = parseFlags(args);
|
|
1467
|
+
if (flags.help || flags.h) return showCommandHelp('diff');
|
|
1468
|
+
await runSnapshotDiff(args);
|
|
1469
|
+
});
|
|
1470
|
+
|
|
1471
|
+
// ── group create ─────────────────────────────────────────────
|
|
1472
|
+
cmd('group create', 'Create a group node', [
|
|
1473
|
+
'pmx-canvas group create --title "API Layer" node1 node2',
|
|
1474
|
+
'pmx-canvas group create --title "Quarterly board" --x 40 --y 60 --width 1600 --height 900 --child-layout column node1 node2',
|
|
1475
|
+
'pmx-canvas group create --title "Frontend" --color "#ff6b6b"',
|
|
1476
|
+
], async (args) => {
|
|
1477
|
+
const { positional, flags } = parseFlags(args);
|
|
1478
|
+
if (flags.help || flags.h) return showCommandHelp('group create');
|
|
1479
|
+
|
|
1480
|
+
const body: Record<string, unknown> = {};
|
|
1481
|
+
if (flags.title && flags.title !== true) body.title = flags.title;
|
|
1482
|
+
if (flags.color && flags.color !== true) body.color = flags.color;
|
|
1483
|
+
const x = optionalFiniteFlag(flags, 'x', 'Use a finite number, e.g. --x 40');
|
|
1484
|
+
const y = optionalFiniteFlag(flags, 'y', 'Use a finite number, e.g. --y 60');
|
|
1485
|
+
const width = optionalPositiveFiniteFlag(flags, 'width', 'Use a positive number, e.g. --width 1600');
|
|
1486
|
+
const height = optionalPositiveFiniteFlag(flags, 'height', 'Use a positive number, e.g. --height 900');
|
|
1487
|
+
if (x !== undefined) body.x = x;
|
|
1488
|
+
if (y !== undefined) body.y = y;
|
|
1489
|
+
if (width !== undefined) body.width = width;
|
|
1490
|
+
if (height !== undefined) body.height = height;
|
|
1491
|
+
if (typeof flags['child-layout'] === 'string') body.childLayout = flags['child-layout'];
|
|
1492
|
+
if (positional.length > 0) body.childIds = positional;
|
|
1493
|
+
|
|
1494
|
+
const result = await api('POST', '/api/canvas/group', body);
|
|
1495
|
+
output(result);
|
|
1496
|
+
});
|
|
1497
|
+
|
|
1498
|
+
// ── group add ────────────────────────────────────────────────
|
|
1499
|
+
cmd('group add', 'Add nodes to an existing group', [
|
|
1500
|
+
'pmx-canvas group add --group <group-id> node1 node2',
|
|
1501
|
+
'pmx-canvas group add --group <group-id> --child-layout flow node1 node2',
|
|
1502
|
+
], async (args) => {
|
|
1503
|
+
const { positional, flags } = parseFlags(args);
|
|
1504
|
+
if (flags.help || flags.h) return showCommandHelp('group add');
|
|
1505
|
+
|
|
1506
|
+
const groupId = requireFlag(flags, 'group', 'pmx-canvas group add --group <group-id> node1 node2');
|
|
1507
|
+
if (positional.length === 0) die('No node IDs provided', 'pmx-canvas group add --group <group-id> node1 node2');
|
|
1508
|
+
|
|
1509
|
+
const result = await api('POST', '/api/canvas/group/add', {
|
|
1510
|
+
groupId,
|
|
1511
|
+
childIds: positional,
|
|
1512
|
+
...(typeof flags['child-layout'] === 'string' ? { childLayout: flags['child-layout'] } : {}),
|
|
1513
|
+
});
|
|
1514
|
+
output(result);
|
|
1515
|
+
});
|
|
1516
|
+
|
|
1517
|
+
// ── batch ────────────────────────────────────────────────────
|
|
1518
|
+
cmd('batch', 'Run a batch of canvas operations from JSON', [
|
|
1519
|
+
'pmx-canvas batch --file ./canvas-ops.json',
|
|
1520
|
+
'pmx-canvas batch --json \'[{\"op\":\"node.add\",\"assign\":\"a\",\"args\":{\"type\":\"markdown\",\"title\":\"A\"}}]\'',
|
|
1521
|
+
'pmx-canvas batch --json \'[{\"op\":\"graph.add\",\"assign\":\"g\",\"args\":{\"graphType\":\"bar\",\"data\":[{\"label\":\"Docs\",\"value\":5}],\"xKey\":\"label\",\"yKey\":\"value\"}}]\'',
|
|
1522
|
+
'cat ops.json | pmx-canvas batch --stdin',
|
|
1523
|
+
], async (args) => {
|
|
1524
|
+
const { flags } = parseFlags(args);
|
|
1525
|
+
if (flags.help || flags.h) return showCommandHelp('batch');
|
|
1526
|
+
|
|
1527
|
+
let raw = '';
|
|
1528
|
+
if (typeof flags.file === 'string') {
|
|
1529
|
+
try {
|
|
1530
|
+
raw = readFileSync(flags.file, 'utf-8');
|
|
1531
|
+
} catch (error) {
|
|
1532
|
+
die(
|
|
1533
|
+
`Unable to read --file: ${error instanceof Error ? error.message : String(error)}`,
|
|
1534
|
+
'Use: pmx-canvas batch --file ./canvas-ops.json',
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
} else if (typeof flags.json === 'string') {
|
|
1538
|
+
raw = flags.json;
|
|
1539
|
+
} else if (flags.stdin) {
|
|
1540
|
+
raw = await readStdin();
|
|
1541
|
+
} else {
|
|
1542
|
+
die(
|
|
1543
|
+
'Batch operations require --file, --json, or --stdin.',
|
|
1544
|
+
'Use: pmx-canvas batch --file ./canvas-ops.json',
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
let parsed: unknown;
|
|
1549
|
+
try {
|
|
1550
|
+
parsed = JSON.parse(raw);
|
|
1551
|
+
} catch (error) {
|
|
1552
|
+
die(
|
|
1553
|
+
`Invalid batch JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
1554
|
+
'Use a JSON array of operations or an object with an "operations" array.',
|
|
1555
|
+
);
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
const result = await api('POST', '/api/canvas/batch', Array.isArray(parsed) ? { operations: parsed } : parsed as Record<string, unknown>);
|
|
1559
|
+
output(result);
|
|
1560
|
+
});
|
|
1561
|
+
|
|
1562
|
+
// ── validate ─────────────────────────────────────────────────
|
|
1563
|
+
cmd('validate', 'Validate the current layout for collisions and missing edge endpoints', [
|
|
1564
|
+
'pmx-canvas validate',
|
|
1565
|
+
], async (args) => {
|
|
1566
|
+
const { flags } = parseFlags(args);
|
|
1567
|
+
if (flags.help || flags.h) return showCommandHelp('validate');
|
|
1568
|
+
|
|
1569
|
+
const result = await api('GET', '/api/canvas/validate');
|
|
1570
|
+
output(result);
|
|
1571
|
+
});
|
|
1572
|
+
|
|
1573
|
+
cmd('validate spec', 'Validate a json-render spec or graph payload without creating a node', [
|
|
1574
|
+
'pmx-canvas validate spec --type json-render --spec-file ./dashboard.json',
|
|
1575
|
+
'pmx-canvas validate spec --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
|
|
1576
|
+
'pmx-canvas validate spec --type json-render --spec-file ./dashboard.json --summary',
|
|
1577
|
+
], async (args) => {
|
|
1578
|
+
const { flags } = parseFlags(args);
|
|
1579
|
+
if (flags.help || flags.h) return showCommandHelp('validate spec');
|
|
1580
|
+
|
|
1581
|
+
const type = getStringFlag(flags, 'type');
|
|
1582
|
+
if (type !== 'json-render' && type !== 'graph') {
|
|
1583
|
+
die('validate spec requires --type json-render or --type graph.');
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
const body = type === 'json-render'
|
|
1587
|
+
? { type, spec: (await buildJsonRenderRequestBody({ ...flags, title: String(flags.title ?? 'Validation') })).spec }
|
|
1588
|
+
: { type, ...(await buildGraphRequestBody(flags)) };
|
|
1589
|
+
|
|
1590
|
+
const result = await api('POST', '/api/canvas/schema/validate', body) as Record<string, unknown>;
|
|
1591
|
+
if (flags.summary) {
|
|
1592
|
+
output({
|
|
1593
|
+
ok: result.ok,
|
|
1594
|
+
type: result.type,
|
|
1595
|
+
summary: result.summary,
|
|
1596
|
+
});
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
output(result);
|
|
1600
|
+
});
|
|
1601
|
+
|
|
1602
|
+
// ── group remove ─────────────────────────────────────────────
|
|
1603
|
+
cmd('group remove', 'Ungroup all children from a group', [
|
|
1604
|
+
'pmx-canvas group remove <group-id>',
|
|
1605
|
+
], async (args) => {
|
|
1606
|
+
const { positional, flags } = parseFlags(args);
|
|
1607
|
+
if (flags.help || flags.h) return showCommandHelp('group remove');
|
|
1608
|
+
|
|
1609
|
+
const id = positional[0];
|
|
1610
|
+
if (!id) die('Missing group ID', 'pmx-canvas group remove <group-id>');
|
|
1611
|
+
|
|
1612
|
+
const result = await api('POST', '/api/canvas/group/ungroup', { groupId: id });
|
|
1613
|
+
output(result);
|
|
1614
|
+
});
|
|
1615
|
+
|
|
1616
|
+
// ── web-artifact build ───────────────────────────────────────
|
|
1617
|
+
cmd('web-artifact build', 'Build a bundled HTML web artifact and optionally open it on the canvas', [
|
|
1618
|
+
'pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx',
|
|
1619
|
+
'pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx --index-css-file ./index.css',
|
|
1620
|
+
'pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx --include-logs',
|
|
1621
|
+
], async (args) => {
|
|
1622
|
+
const { flags } = parseFlags(args);
|
|
1623
|
+
if (flags.help || flags.h) return showCommandHelp('web-artifact build');
|
|
1624
|
+
await runWebArtifactBuildCommand(flags);
|
|
1625
|
+
});
|
|
1626
|
+
|
|
1627
|
+
// ── clear ────────────────────────────────────────────────────
|
|
1628
|
+
cmd('clear', 'Remove all nodes and edges from the canvas', [
|
|
1629
|
+
'pmx-canvas clear --yes',
|
|
1630
|
+
'pmx-canvas clear --dry-run',
|
|
1631
|
+
], async (args) => {
|
|
1632
|
+
const { flags } = parseFlags(args);
|
|
1633
|
+
if (flags.help || flags.h) return showCommandHelp('clear');
|
|
1634
|
+
|
|
1635
|
+
if (flags['dry-run']) {
|
|
1636
|
+
const layout = (await api('GET', '/api/canvas/state')) as { nodes: unknown[]; edges: unknown[] };
|
|
1637
|
+
output({
|
|
1638
|
+
dry_run: true,
|
|
1639
|
+
would_remove: { nodes: layout.nodes.length, edges: layout.edges.length },
|
|
1640
|
+
message: 'No changes made. Pass --yes to confirm.',
|
|
1641
|
+
});
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
if (!flags.yes) {
|
|
1646
|
+
die('Destructive operation requires --yes flag', 'pmx-canvas clear --yes (or preview with --dry-run)');
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
const result = await api('POST', '/api/canvas/clear');
|
|
1650
|
+
output(result);
|
|
1651
|
+
});
|
|
1652
|
+
|
|
1653
|
+
// ── webview status ────────────────────────────────────────────
|
|
1654
|
+
cmd('webview status', 'Show Bun.WebView automation status', [
|
|
1655
|
+
'pmx-canvas webview status',
|
|
1656
|
+
], async (args) => {
|
|
1657
|
+
const { flags } = parseFlags(args);
|
|
1658
|
+
if (flags.help || flags.h) return showCommandHelp('webview status');
|
|
1659
|
+
|
|
1660
|
+
const result = await api('GET', '/api/workbench/webview');
|
|
1661
|
+
output(result);
|
|
1662
|
+
});
|
|
1663
|
+
|
|
1664
|
+
// ── webview start ─────────────────────────────────────────────
|
|
1665
|
+
cmd('webview start', 'Start or replace the Bun.WebView automation session', [
|
|
1666
|
+
'pmx-canvas webview start',
|
|
1667
|
+
'pmx-canvas webview start --backend chrome --width 1440 --height 900',
|
|
1668
|
+
'pmx-canvas webview start --chrome-path /Applications/Google\\ Chrome.app/.../Google\\ Chrome',
|
|
1669
|
+
], async (args) => {
|
|
1670
|
+
const { flags } = parseFlags(args);
|
|
1671
|
+
if (flags.help || flags.h) return showCommandHelp('webview start');
|
|
1672
|
+
|
|
1673
|
+
const backend = flags.backend;
|
|
1674
|
+
if (backend && backend !== true && backend !== 'chrome' && backend !== 'webkit') {
|
|
1675
|
+
die('Invalid value for --backend', 'Use: --backend chrome or --backend webkit');
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
const body: Record<string, unknown> = {};
|
|
1679
|
+
if (backend && backend !== true) body.backend = backend;
|
|
1680
|
+
|
|
1681
|
+
const width = optionalNumberFlag(flags, 'width', 'Use a positive integer width, e.g. --width 1440');
|
|
1682
|
+
const height = optionalNumberFlag(flags, 'height', 'Use a positive integer height, e.g. --height 900');
|
|
1683
|
+
if (width !== undefined) body.width = width;
|
|
1684
|
+
if (height !== undefined) body.height = height;
|
|
1685
|
+
|
|
1686
|
+
if (flags['chrome-path'] && flags['chrome-path'] !== true) {
|
|
1687
|
+
body.chromePath = flags['chrome-path'];
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
if (flags['data-dir'] && flags['data-dir'] !== true) {
|
|
1691
|
+
body.dataStoreDir = flags['data-dir'];
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
if (flags['chrome-argv'] && flags['chrome-argv'] !== true) {
|
|
1695
|
+
const chromeArgv = String(flags['chrome-argv'])
|
|
1696
|
+
.split(',')
|
|
1697
|
+
.map((value) => value.trim())
|
|
1698
|
+
.filter((value) => value.length > 0);
|
|
1699
|
+
if (chromeArgv.length > 0) body.chromeArgv = chromeArgv;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
const result = await api('POST', '/api/workbench/webview/start', body);
|
|
1703
|
+
output(result);
|
|
1704
|
+
});
|
|
1705
|
+
|
|
1706
|
+
// ── webview stop ──────────────────────────────────────────────
|
|
1707
|
+
cmd('webview stop', 'Stop the active Bun.WebView automation session', [
|
|
1708
|
+
'pmx-canvas webview stop',
|
|
1709
|
+
], async (args) => {
|
|
1710
|
+
const { flags } = parseFlags(args);
|
|
1711
|
+
if (flags.help || flags.h) return showCommandHelp('webview stop');
|
|
1712
|
+
|
|
1713
|
+
const result = await api('DELETE', '/api/workbench/webview');
|
|
1714
|
+
output(result);
|
|
1715
|
+
});
|
|
1716
|
+
|
|
1717
|
+
// ── webview evaluate ──────────────────────────────────────────
|
|
1718
|
+
cmd('webview evaluate', 'Evaluate JavaScript in the active Bun.WebView automation session', [
|
|
1719
|
+
'pmx-canvas webview evaluate --expression "document.title"',
|
|
1720
|
+
'pmx-canvas webview evaluate --script "const title = document.title; return title.toUpperCase()"',
|
|
1721
|
+
'pmx-canvas webview evaluate --file ./probe.js',
|
|
1722
|
+
], async (args) => {
|
|
1723
|
+
const { flags } = parseFlags(args);
|
|
1724
|
+
if (flags.help || flags.h) return showCommandHelp('webview evaluate');
|
|
1725
|
+
|
|
1726
|
+
const sourceCount = [flags.expression, flags.script, flags.file].filter(Boolean).length;
|
|
1727
|
+
if (sourceCount > 1) {
|
|
1728
|
+
die('Use only one of --expression, --script, or --file.', 'pmx-canvas webview evaluate --expression "document.title"');
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
let expression = '';
|
|
1732
|
+
if (typeof flags.file === 'string') {
|
|
1733
|
+
let script = '';
|
|
1734
|
+
try {
|
|
1735
|
+
script = readFileSync(flags.file, 'utf-8');
|
|
1736
|
+
} catch (error) {
|
|
1737
|
+
die(
|
|
1738
|
+
`Unable to read --file: ${error instanceof Error ? error.message : String(error)}`,
|
|
1739
|
+
'pmx-canvas webview evaluate --file ./probe.js',
|
|
1740
|
+
);
|
|
1741
|
+
}
|
|
1742
|
+
expression = `(() => {\n${script}\n})()`;
|
|
1743
|
+
} else if (typeof flags.script === 'string') {
|
|
1744
|
+
expression = `(() => {\n${flags.script}\n})()`;
|
|
1745
|
+
} else {
|
|
1746
|
+
expression = requireFlag(
|
|
1747
|
+
flags,
|
|
1748
|
+
'expression',
|
|
1749
|
+
'pmx-canvas webview evaluate --expression "document.title"',
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
const result = await api('POST', '/api/workbench/webview/evaluate', { expression });
|
|
1754
|
+
output(result);
|
|
1755
|
+
});
|
|
1756
|
+
|
|
1757
|
+
// ── webview resize ────────────────────────────────────────────
|
|
1758
|
+
cmd('webview resize', 'Resize the active Bun.WebView automation session viewport', [
|
|
1759
|
+
'pmx-canvas webview resize --width 1280 --height 800',
|
|
1760
|
+
], async (args) => {
|
|
1761
|
+
const { flags } = parseFlags(args);
|
|
1762
|
+
if (flags.help || flags.h) return showCommandHelp('webview resize');
|
|
1763
|
+
|
|
1764
|
+
const width = optionalNumberFlag(flags, 'width', 'Use: pmx-canvas webview resize --width 1280 --height 800');
|
|
1765
|
+
const height = optionalNumberFlag(flags, 'height', 'Use: pmx-canvas webview resize --width 1280 --height 800');
|
|
1766
|
+
if (width === undefined || height === undefined) {
|
|
1767
|
+
die('Missing required flags: --width, --height', 'Use: pmx-canvas webview resize --width 1280 --height 800');
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
const result = await api('POST', '/api/workbench/webview/resize', { width, height });
|
|
1771
|
+
output(result);
|
|
1772
|
+
});
|
|
1773
|
+
|
|
1774
|
+
// ── webview screenshot ────────────────────────────────────────
|
|
1775
|
+
cmd('webview screenshot', 'Capture a screenshot from the active Bun.WebView automation session', [
|
|
1776
|
+
'pmx-canvas webview screenshot --output ./canvas.png',
|
|
1777
|
+
'pmx-canvas webview screenshot --output ./canvas.webp --format webp --quality 80',
|
|
1778
|
+
], async (args) => {
|
|
1779
|
+
const { flags } = parseFlags(args);
|
|
1780
|
+
if (flags.help || flags.h) return showCommandHelp('webview screenshot');
|
|
1781
|
+
|
|
1782
|
+
const outputPath = requireFlag(
|
|
1783
|
+
flags,
|
|
1784
|
+
'output',
|
|
1785
|
+
'pmx-canvas webview screenshot --output ./canvas.png',
|
|
1786
|
+
);
|
|
1787
|
+
|
|
1788
|
+
const body: Record<string, unknown> = {};
|
|
1789
|
+
if (flags.format && flags.format !== true) {
|
|
1790
|
+
const format = String(flags.format);
|
|
1791
|
+
if (format !== 'png' && format !== 'jpeg' && format !== 'webp') {
|
|
1792
|
+
die('Invalid value for --format', 'Use: --format png, jpeg, or webp');
|
|
1793
|
+
}
|
|
1794
|
+
body.format = format;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
if (flags.quality && flags.quality !== true) {
|
|
1798
|
+
const quality = Number(flags.quality);
|
|
1799
|
+
if (!Number.isFinite(quality)) {
|
|
1800
|
+
die(`Invalid value for --quality: ${String(flags.quality)}`, 'Use a numeric quality, e.g. --quality 80');
|
|
1801
|
+
}
|
|
1802
|
+
body.quality = quality;
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
const base = getBaseUrl();
|
|
1806
|
+
const response = await fetch(`${base}/api/workbench/webview/screenshot`, {
|
|
1807
|
+
method: 'POST',
|
|
1808
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1809
|
+
body: JSON.stringify(body),
|
|
1810
|
+
});
|
|
1811
|
+
|
|
1812
|
+
if (!response.ok) {
|
|
1813
|
+
const text = await response.text();
|
|
1814
|
+
try {
|
|
1815
|
+
const json = JSON.parse(text) as Record<string, unknown>;
|
|
1816
|
+
die(
|
|
1817
|
+
json.error ? String(json.error) : `HTTP ${response.status}`,
|
|
1818
|
+
typeof json.hint === 'string' ? json.hint : undefined,
|
|
1819
|
+
);
|
|
1820
|
+
} catch {
|
|
1821
|
+
die(`HTTP ${response.status}: ${text}`);
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
1826
|
+
writeFileSync(outputPath, bytes);
|
|
1827
|
+
output({
|
|
1828
|
+
ok: true,
|
|
1829
|
+
output: outputPath,
|
|
1830
|
+
bytes: bytes.byteLength,
|
|
1831
|
+
mimeType: response.headers.get('Content-Type') ?? 'application/octet-stream',
|
|
1832
|
+
});
|
|
1833
|
+
});
|
|
1834
|
+
|
|
1835
|
+
// ── code-graph ───────────────────────────────────────────────
|
|
1836
|
+
cmd('code-graph', 'Show auto-detected file dependency graph', [
|
|
1837
|
+
'pmx-canvas code-graph',
|
|
1838
|
+
], async (args) => {
|
|
1839
|
+
const { flags } = parseFlags(args);
|
|
1840
|
+
if (flags.help || flags.h) return showCommandHelp('code-graph');
|
|
1841
|
+
|
|
1842
|
+
const result = await api('GET', '/api/canvas/code-graph');
|
|
1843
|
+
output(result);
|
|
1844
|
+
});
|
|
1845
|
+
|
|
1846
|
+
// ── spatial ──────────────────────────────────────────────────
|
|
1847
|
+
cmd('spatial', 'Spatial analysis: clusters, reading order, neighborhoods', [
|
|
1848
|
+
'pmx-canvas spatial',
|
|
1849
|
+
], async (args) => {
|
|
1850
|
+
const { flags } = parseFlags(args);
|
|
1851
|
+
if (flags.help || flags.h) return showCommandHelp('spatial');
|
|
1852
|
+
|
|
1853
|
+
const result = await api('GET', '/api/canvas/spatial-context');
|
|
1854
|
+
output(result);
|
|
1855
|
+
});
|
|
1856
|
+
|
|
1857
|
+
// ── watch ────────────────────────────────────────────────────
|
|
1858
|
+
cmd('watch', 'Watch low-token semantic canvas changes over the existing SSE stream', [
|
|
1859
|
+
'pmx-canvas watch',
|
|
1860
|
+
'pmx-canvas watch --json',
|
|
1861
|
+
'pmx-canvas watch --events context-pin,move-end',
|
|
1862
|
+
'pmx-canvas watch --json --events connect --max-events 1',
|
|
1863
|
+
], async (args) => {
|
|
1864
|
+
const { flags } = parseFlags(args);
|
|
1865
|
+
if (flags.help || flags.h) return showCommandHelp('watch');
|
|
1866
|
+
|
|
1867
|
+
if (flags.json && flags.compact) {
|
|
1868
|
+
die('Use either --json or --compact, not both.');
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
const filtersRaw = typeof flags.events === 'string' ? flags.events : undefined;
|
|
1872
|
+
const requestedFilters = filtersRaw
|
|
1873
|
+
? Array.from(new Set(filtersRaw.split(',').map((value) => value.trim()).filter((value) => value.length > 0)))
|
|
1874
|
+
: [];
|
|
1875
|
+
const invalidFilter = requestedFilters.find((value) => !ALL_SEMANTIC_WATCH_EVENT_TYPES.includes(value as typeof ALL_SEMANTIC_WATCH_EVENT_TYPES[number]));
|
|
1876
|
+
if (invalidFilter) {
|
|
1877
|
+
die(
|
|
1878
|
+
`Invalid value in --events: ${invalidFilter}`,
|
|
1879
|
+
'Use a comma-separated subset of: context-pin,move-end,group,connect,remove',
|
|
1880
|
+
);
|
|
1881
|
+
}
|
|
1882
|
+
const filters = parseSemanticEventFilter(filtersRaw);
|
|
1883
|
+
if (filtersRaw && filters.size === 0) {
|
|
1884
|
+
die(
|
|
1885
|
+
`Invalid value for --events: ${filtersRaw}`,
|
|
1886
|
+
'Use a comma-separated subset of: context-pin,move-end,group,connect,remove',
|
|
1887
|
+
);
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
const maxEvents = optionalNumberFlag(flags, 'max-events', 'Use a positive integer, e.g. --max-events 1');
|
|
1891
|
+
const jsonMode = Boolean(flags.json);
|
|
1892
|
+
const reducer = new SemanticWatchReducer();
|
|
1893
|
+
const pinned = await api('GET', '/api/canvas/pinned-context') as { nodeIds?: string[] };
|
|
1894
|
+
reducer.setInitialPins(Array.isArray(pinned.nodeIds) ? pinned.nodeIds : []);
|
|
1895
|
+
|
|
1896
|
+
const base = getBaseUrl();
|
|
1897
|
+
const controller = new AbortController();
|
|
1898
|
+
let response: Response;
|
|
1899
|
+
try {
|
|
1900
|
+
response = await fetch(`${base}/api/workbench/events`, {
|
|
1901
|
+
method: 'GET',
|
|
1902
|
+
headers: { Accept: 'text/event-stream' },
|
|
1903
|
+
signal: controller.signal,
|
|
1904
|
+
});
|
|
1905
|
+
} catch (error) {
|
|
1906
|
+
die(
|
|
1907
|
+
`Cannot connect to pmx-canvas event stream at ${base}: ${error instanceof Error ? error.message : String(error)}`,
|
|
1908
|
+
'Start the server first: pmx-canvas --no-open',
|
|
1909
|
+
);
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
if (!response.ok) {
|
|
1913
|
+
const text = await response.text();
|
|
1914
|
+
die(`Failed to open event stream: HTTP ${response.status}`, text);
|
|
1915
|
+
}
|
|
1916
|
+
if (!response.body) {
|
|
1917
|
+
die('Workbench event stream did not return a readable body.');
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
let emitted = 0;
|
|
1921
|
+
try {
|
|
1922
|
+
for await (const message of parseSseStream(response.body)) {
|
|
1923
|
+
const semanticEvents = reducer
|
|
1924
|
+
.handleMessage(message)
|
|
1925
|
+
.filter((event) => filters.has(event.type));
|
|
1926
|
+
|
|
1927
|
+
for (const event of semanticEvents) {
|
|
1928
|
+
console.log(jsonMode ? JSON.stringify(event) : formatCompactWatchEvent(event));
|
|
1929
|
+
emitted++;
|
|
1930
|
+
if (maxEvents !== undefined && emitted >= maxEvents) {
|
|
1931
|
+
controller.abort();
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
} catch (error) {
|
|
1937
|
+
if (controller.signal.aborted) return;
|
|
1938
|
+
die(
|
|
1939
|
+
`Watch stream failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1940
|
+
'Ensure the server is still running and reachable.',
|
|
1941
|
+
);
|
|
1942
|
+
}
|
|
1943
|
+
});
|
|
1944
|
+
|
|
1945
|
+
// ── serve (delegates back to original CLI) ───────────────────
|
|
1946
|
+
cmd('serve', 'Start the canvas server', [
|
|
1947
|
+
'pmx-canvas serve',
|
|
1948
|
+
'pmx-canvas serve --port=8080 --no-open',
|
|
1949
|
+
'pmx-canvas serve --demo --theme=light',
|
|
1950
|
+
'pmx-canvas --no-open --webview-automation',
|
|
1951
|
+
], async (_args) => {
|
|
1952
|
+
console.log('Use: pmx-canvas [--port=PORT] [--demo] [--no-open] [--theme=THEME] [--webview-automation]');
|
|
1953
|
+
});
|
|
1954
|
+
|
|
1955
|
+
// ── Help ─────────────────────────────────────────────────────
|
|
1956
|
+
|
|
1957
|
+
function showCommandHelp(name: string): void {
|
|
1958
|
+
const cmd = COMMANDS[name];
|
|
1959
|
+
if (!cmd) return;
|
|
1960
|
+
console.log(`\npmx-canvas ${name} — ${cmd.help}\n`);
|
|
1961
|
+
console.log('Examples:');
|
|
1962
|
+
for (const ex of cmd.examples) {
|
|
1963
|
+
console.log(` ${ex}`);
|
|
1964
|
+
}
|
|
1965
|
+
if (name === 'node add') {
|
|
1966
|
+
console.log('\nSchema help:');
|
|
1967
|
+
console.log(' pmx-canvas node add --help --type webpage');
|
|
1968
|
+
console.log(' pmx-canvas node add --help --type json-render --component Table');
|
|
1969
|
+
console.log(' pmx-canvas node add --help --type webpage --json');
|
|
1970
|
+
}
|
|
1971
|
+
if (name === 'node schema') {
|
|
1972
|
+
console.log('\nFilters:');
|
|
1973
|
+
console.log(' --summary Show compact schema summaries');
|
|
1974
|
+
console.log(' --field <name> Focus on one node field');
|
|
1975
|
+
console.log(' --component <name> Focus on one json-render component');
|
|
1976
|
+
}
|
|
1977
|
+
if (name === 'validate spec') {
|
|
1978
|
+
console.log('\nOutput control:');
|
|
1979
|
+
console.log(' --summary Return only validation summary metadata');
|
|
1980
|
+
}
|
|
1981
|
+
if (name === 'web-artifact build') {
|
|
1982
|
+
console.log('\nOutput control:');
|
|
1983
|
+
console.log(' --include-logs Include raw build stdout/stderr in the response');
|
|
1984
|
+
console.log(' --verbose Alias for --include-logs');
|
|
1985
|
+
}
|
|
1986
|
+
console.log('');
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
function showTopLevelHelp(): void {
|
|
1990
|
+
console.log(`
|
|
1991
|
+
pmx-canvas — Agent-native CLI for spatial canvas workbench
|
|
1992
|
+
|
|
1993
|
+
Usage:
|
|
1994
|
+
pmx-canvas <command> [options]
|
|
1995
|
+
pmx-canvas [server-options]
|
|
1996
|
+
|
|
1997
|
+
Server:
|
|
1998
|
+
pmx-canvas Start server + open browser
|
|
1999
|
+
pmx-canvas --no-open --demo Start server headless with sample data
|
|
2000
|
+
pmx-canvas --no-open --webview-automation Start server + headless Bun.WebView automation
|
|
2001
|
+
pmx-canvas --mcp Run as MCP server (stdio)
|
|
2002
|
+
|
|
2003
|
+
Node commands:
|
|
2004
|
+
pmx-canvas node add [options] Add a node
|
|
2005
|
+
pmx-canvas node list [--type TYPE] List all nodes
|
|
2006
|
+
pmx-canvas node get <id> Get a node by ID
|
|
2007
|
+
pmx-canvas node update <id> [opts] Update a node
|
|
2008
|
+
pmx-canvas node remove <id> Remove a node
|
|
2009
|
+
|
|
2010
|
+
Edge commands:
|
|
2011
|
+
pmx-canvas edge add [options] Add an edge between nodes
|
|
2012
|
+
pmx-canvas edge list List all edges
|
|
2013
|
+
pmx-canvas edge remove <id> Remove an edge
|
|
2014
|
+
|
|
2015
|
+
Canvas commands:
|
|
2016
|
+
pmx-canvas layout Full canvas state (JSON)
|
|
2017
|
+
pmx-canvas status Quick summary
|
|
2018
|
+
pmx-canvas search <query> Search nodes by content
|
|
2019
|
+
pmx-canvas open Open the current workbench in a browser
|
|
2020
|
+
pmx-canvas arrange [--layout MODE] Auto-arrange (grid|column|flow)
|
|
2021
|
+
pmx-canvas batch [--file FILE] Run many canvas operations at once
|
|
2022
|
+
pmx-canvas validate Check collisions and containment issues
|
|
2023
|
+
pmx-canvas validate spec Validate json-render/graph payloads without creating nodes
|
|
2024
|
+
pmx-canvas watch [options] Watch semantic canvas changes over SSE
|
|
2025
|
+
pmx-canvas focus <id> Pan viewport to node
|
|
2026
|
+
pmx-canvas webview status Show WebView automation status
|
|
2027
|
+
pmx-canvas webview start [options] Start or replace automation session
|
|
2028
|
+
pmx-canvas webview evaluate Evaluate JS in automation session
|
|
2029
|
+
pmx-canvas webview resize Resize automation viewport
|
|
2030
|
+
pmx-canvas webview screenshot Save automation screenshot to disk
|
|
2031
|
+
pmx-canvas webview stop Stop automation session
|
|
2032
|
+
pmx-canvas web-artifact build Build bundled web artifact HTML
|
|
2033
|
+
pmx-canvas clear --yes Clear all nodes and edges
|
|
2034
|
+
pmx-canvas node schema Describe running-server node schemas
|
|
2035
|
+
|
|
2036
|
+
Context pins:
|
|
2037
|
+
pmx-canvas pin <id1> <id2> ... Set pinned nodes (same as --set)
|
|
2038
|
+
pmx-canvas pin --list List pinned context
|
|
2039
|
+
pmx-canvas pin --clear Clear all pins
|
|
2040
|
+
|
|
2041
|
+
History:
|
|
2042
|
+
pmx-canvas undo Undo last mutation
|
|
2043
|
+
pmx-canvas redo Redo last undone
|
|
2044
|
+
pmx-canvas history Show mutation timeline
|
|
2045
|
+
|
|
2046
|
+
Snapshots:
|
|
2047
|
+
pmx-canvas snapshot save --name X Save a named snapshot
|
|
2048
|
+
pmx-canvas snapshot list List snapshots
|
|
2049
|
+
pmx-canvas snapshot restore <id> Restore from snapshot
|
|
2050
|
+
pmx-canvas snapshot diff <id> Compare current canvas to snapshot
|
|
2051
|
+
pmx-canvas snapshot delete <id> Delete a snapshot
|
|
2052
|
+
|
|
2053
|
+
Groups:
|
|
2054
|
+
pmx-canvas group create [options] Create a group
|
|
2055
|
+
pmx-canvas group add --group <id> Add nodes to group
|
|
2056
|
+
pmx-canvas group remove <id> Ungroup children
|
|
2057
|
+
|
|
2058
|
+
Analysis:
|
|
2059
|
+
pmx-canvas code-graph File dependency graph
|
|
2060
|
+
pmx-canvas spatial Spatial clusters & neighborhoods
|
|
2061
|
+
|
|
2062
|
+
Global flags:
|
|
2063
|
+
--help, -h Show help for any command
|
|
2064
|
+
|
|
2065
|
+
Environment:
|
|
2066
|
+
PMX_CANVAS_URL Server URL (default: http://localhost:4313)
|
|
2067
|
+
PMX_CANVAS_PORT Client target port when PMX_CANVAS_URL is unset (default: 4313)
|
|
2068
|
+
|
|
2069
|
+
Examples:
|
|
2070
|
+
pmx-canvas node add --type markdown --title "API Design" --content "# REST API"
|
|
2071
|
+
pmx-canvas node add --type webpage --url "https://example.com/docs"
|
|
2072
|
+
pmx-canvas node add --type json-render --title "Dashboard" --spec-file ./dashboard.json
|
|
2073
|
+
pmx-canvas node add --type web-artifact --title "Dashboard" --app-file ./App.tsx
|
|
2074
|
+
pmx-canvas node add --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value
|
|
2075
|
+
pmx-canvas node add --help --type webpage
|
|
2076
|
+
pmx-canvas node schema --type json-render
|
|
2077
|
+
pmx-canvas node schema --type json-render --component Table --summary
|
|
2078
|
+
pmx-canvas node list --type file --ids
|
|
2079
|
+
pmx-canvas node get node-abc123 --summary
|
|
2080
|
+
pmx-canvas node get node-abc123 --field title --field graphConfig
|
|
2081
|
+
pmx-canvas edge add --from node-abc --to node-def --type depends-on
|
|
2082
|
+
pmx-canvas edge add --from-search "DVT O3 — GitOps" --to-search "deep work trend" --type relation
|
|
2083
|
+
pmx-canvas search "authentication"
|
|
2084
|
+
pmx-canvas open
|
|
2085
|
+
pmx-canvas layout --summary
|
|
2086
|
+
pmx-canvas arrange --layout column
|
|
2087
|
+
pmx-canvas batch --file ./canvas-ops.json
|
|
2088
|
+
pmx-canvas validate
|
|
2089
|
+
pmx-canvas validate spec --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value
|
|
2090
|
+
pmx-canvas validate spec --type json-render --spec-file ./dashboard.json --summary
|
|
2091
|
+
pmx-canvas history --summary
|
|
2092
|
+
pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx
|
|
2093
|
+
pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx --include-logs
|
|
2094
|
+
pmx-canvas webview evaluate --script "const title = document.title; return title"
|
|
2095
|
+
pmx-canvas snapshot save --name "pre-refactor"
|
|
2096
|
+
pmx-canvas clear --dry-run
|
|
2097
|
+
cat design.md | pmx-canvas node add --type markdown --title "Design" --stdin
|
|
2098
|
+
`);
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
// ── Stdin reader ─────────────────────────────────────────────
|
|
2102
|
+
|
|
2103
|
+
async function readStdin(): Promise<string> {
|
|
2104
|
+
const chunks: Buffer[] = [];
|
|
2105
|
+
for await (const chunk of process.stdin) {
|
|
2106
|
+
chunks.push(chunk);
|
|
2107
|
+
}
|
|
2108
|
+
return Buffer.concat(chunks).toString('utf-8');
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
// ── Router ───────────────────────────────────────────────────
|
|
2112
|
+
|
|
2113
|
+
export async function runAgentCli(args: string[]): Promise<void> {
|
|
2114
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
2115
|
+
showTopLevelHelp();
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
// Try two-word command first (e.g., "node add"), then one-word (e.g., "search")
|
|
2120
|
+
const twoWord = `${args[0]} ${args[1] ?? ''}`.trim();
|
|
2121
|
+
if (COMMANDS[twoWord]) {
|
|
2122
|
+
await COMMANDS[twoWord].run(args.slice(2));
|
|
2123
|
+
return;
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
const oneWord = args[0];
|
|
2127
|
+
if (COMMANDS[oneWord]) {
|
|
2128
|
+
await COMMANDS[oneWord].run(args.slice(1));
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
// Unknown command — show help for the resource if it exists
|
|
2133
|
+
const resourceCommands = Object.keys(COMMANDS).filter((k) => k.startsWith(oneWord + ' '));
|
|
2134
|
+
if (resourceCommands.length > 0) {
|
|
2135
|
+
console.log(`\nAvailable "${oneWord}" commands:\n`);
|
|
2136
|
+
for (const k of resourceCommands) {
|
|
2137
|
+
console.log(` pmx-canvas ${k.padEnd(20)} ${COMMANDS[k].help}`);
|
|
2138
|
+
}
|
|
2139
|
+
console.log('\nRun any command with --help for details.\n');
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
die(`Unknown command: ${oneWord}`, 'Run: pmx-canvas --help');
|
|
2144
|
+
}
|