pmx-canvas 0.3.2 → 0.3.4
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 +69 -0
- package/Readme.md +2 -1
- package/dist/canvas/index.js +85 -83
- package/dist/json-render/index.js +47 -47
- package/dist/types/cli/agent.d.ts +13 -8
- package/dist/types/cli/commands/apps.d.ts +1 -0
- package/dist/types/cli/commands/ax.d.ts +1 -0
- package/dist/types/cli/commands/copilot.d.ts +1 -0
- package/dist/types/cli/commands/edges.d.ts +1 -0
- package/dist/types/cli/commands/groups.d.ts +1 -0
- package/dist/types/cli/commands/history.d.ts +1 -0
- package/dist/types/cli/commands/nodes.d.ts +1 -0
- package/dist/types/cli/commands/pins.d.ts +1 -0
- package/dist/types/cli/commands/query.d.ts +1 -0
- package/dist/types/cli/commands/view.d.ts +1 -0
- package/dist/types/cli/commands/webview.d.ts +1 -0
- package/dist/types/cli/daemon.d.ts +17 -0
- package/dist/types/cli/shared.d.ts +73 -0
- package/dist/types/client/canvas/ExpandedNodeOverlay.d.ts +1 -1
- package/dist/types/client/nodes/ExtAppFrame.d.ts +13 -0
- package/dist/types/client/nodes/HtmlNode.d.ts +1 -1
- package/dist/types/client/nodes/McpAppNode.d.ts +1 -1
- package/dist/types/client/nodes/use-ax-surface-bridge.d.ts +20 -0
- package/dist/types/server/canvas-operations.d.ts +2 -0
- package/dist/types/server/diagram-presets.d.ts +8 -0
- package/dist/types/server/operations/composites.d.ts +0 -9
- package/dist/types/shared/ax-surface-protocol.d.ts +18 -0
- package/docs/RELEASE.md +11 -0
- package/docs/bun-webview-integration.md +5 -0
- package/docs/environment.md +48 -0
- package/docs/node-types.md +5 -0
- package/docs/plans/plan-009-tech-debt-backlog.md +27 -9
- package/docs/screenshot.png +0 -0
- package/package.json +8 -5
- package/skills/pmx-canvas/SKILL.md +11 -8
- package/skills/pmx-canvas/evals/evals.json +5 -5
- package/skills/pmx-canvas/references/full-reference.md +8 -1
- package/src/cli/agent.ts +22 -3512
- package/src/cli/commands/apps.ts +165 -0
- package/src/cli/commands/ax.ts +604 -0
- package/src/cli/commands/copilot.ts +43 -0
- package/src/cli/commands/edges.ts +73 -0
- package/src/cli/commands/groups.ts +81 -0
- package/src/cli/commands/history.ts +220 -0
- package/src/cli/commands/nodes.ts +954 -0
- package/src/cli/commands/pins.ts +42 -0
- package/src/cli/commands/query.ts +223 -0
- package/src/cli/commands/view.ts +186 -0
- package/src/cli/commands/webview.ts +227 -0
- package/src/cli/daemon.ts +39 -7
- package/src/cli/shared.ts +860 -0
- package/src/client/canvas/ExpandedNodeOverlay.tsx +3 -2
- package/src/client/nodes/ExtAppFrame.tsx +166 -52
- package/src/client/nodes/HtmlNode.tsx +8 -51
- package/src/client/nodes/McpAppNode.tsx +12 -47
- package/src/client/nodes/use-ax-surface-bridge.ts +71 -0
- package/src/json-render/renderer/index.tsx +3 -2
- package/src/mcp/server.ts +0 -47
- package/src/server/bundled-skills.ts +5 -1
- package/src/server/canvas-operations.ts +8 -0
- package/src/server/diagram-presets.ts +15 -1
- package/src/server/html-surface.ts +11 -6
- package/src/server/operations/composites.ts +1 -13
- package/src/server/operations/invoker.ts +10 -3
- package/src/server/operations/mcp.ts +51 -1
- package/src/server/operations/ops/app.ts +5 -0
- package/src/server/server.ts +61 -3
- package/src/shared/ax-surface-protocol.ts +22 -0
- package/dist/types/client/canvas/ContextPinHud.d.ts +0 -1
- package/src/client/canvas/ContextPinHud.tsx +0 -17
|
@@ -0,0 +1,954 @@
|
|
|
1
|
+
// Node/content commands: node add|schema|list|get|update|remove, json-render,
|
|
2
|
+
// html primitive add|schema, and graph add.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
applyCommonGeometryFlags,
|
|
6
|
+
applyStrictSizeFlags,
|
|
7
|
+
buildGraphRequestBody,
|
|
8
|
+
buildHtmlPrimitiveRequestBody,
|
|
9
|
+
buildJsonRenderRequestBody,
|
|
10
|
+
cmd,
|
|
11
|
+
die,
|
|
12
|
+
getStringFlag,
|
|
13
|
+
invokeOperation,
|
|
14
|
+
isRecord,
|
|
15
|
+
optionalBooleanFlag,
|
|
16
|
+
optionalFiniteFlag,
|
|
17
|
+
optionalPositiveFiniteFlag,
|
|
18
|
+
optionalPositiveFiniteFlagWithAliases,
|
|
19
|
+
output,
|
|
20
|
+
parseFlags,
|
|
21
|
+
parseJsonValue,
|
|
22
|
+
readOptionalTextInput,
|
|
23
|
+
readStdin,
|
|
24
|
+
runWebArtifactBuildCommand,
|
|
25
|
+
showCommandHelp,
|
|
26
|
+
} from '../shared.js';
|
|
27
|
+
|
|
28
|
+
const TRACE_NODE_FIELDS = ['toolName', 'category', 'status', 'duration', 'resultSummary', 'error'] as const;
|
|
29
|
+
|
|
30
|
+
interface CanvasSchemaField {
|
|
31
|
+
name: string;
|
|
32
|
+
type: string;
|
|
33
|
+
required: boolean;
|
|
34
|
+
description: string;
|
|
35
|
+
aliases?: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface CanvasSchemaType {
|
|
39
|
+
type: string;
|
|
40
|
+
kind: 'node' | 'virtual-node';
|
|
41
|
+
description: string;
|
|
42
|
+
endpoint: string;
|
|
43
|
+
fields: CanvasSchemaField[];
|
|
44
|
+
example: Record<string, unknown>;
|
|
45
|
+
notes?: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface JsonRenderComponentSchema {
|
|
49
|
+
type: string;
|
|
50
|
+
description: string;
|
|
51
|
+
slots: string[];
|
|
52
|
+
example: unknown;
|
|
53
|
+
props: Array<{
|
|
54
|
+
name: string;
|
|
55
|
+
type: string;
|
|
56
|
+
required: boolean;
|
|
57
|
+
nullable: boolean;
|
|
58
|
+
}>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface CanvasSchemaResponse {
|
|
62
|
+
ok: true;
|
|
63
|
+
source: 'running-server';
|
|
64
|
+
version: string | null;
|
|
65
|
+
nodeTypes: CanvasSchemaType[];
|
|
66
|
+
jsonRender: {
|
|
67
|
+
rootShape: Record<string, string>;
|
|
68
|
+
components: JsonRenderComponentSchema[];
|
|
69
|
+
};
|
|
70
|
+
graph: {
|
|
71
|
+
graphTypes: Array<'line' | 'bar' | 'pie' | 'area' | 'scatter' | 'radar' | 'stacked-bar' | 'composed'>;
|
|
72
|
+
};
|
|
73
|
+
htmlPrimitives?: Array<{
|
|
74
|
+
kind: string;
|
|
75
|
+
title: string;
|
|
76
|
+
description: string;
|
|
77
|
+
useWhen: string;
|
|
78
|
+
defaultSize: { width: number; height: number };
|
|
79
|
+
dataShape: string;
|
|
80
|
+
example: Record<string, unknown>;
|
|
81
|
+
}>;
|
|
82
|
+
mcp: {
|
|
83
|
+
tools: string[];
|
|
84
|
+
resources: string[];
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function truncateText(value: string, maxLength = 240): string {
|
|
89
|
+
const normalized = value.replace(/\s+/g, ' ').trim();
|
|
90
|
+
if (normalized.length <= maxLength) return normalized;
|
|
91
|
+
if (maxLength <= 1) return normalized.slice(0, maxLength);
|
|
92
|
+
return `${normalized.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function summarizeGraphConfig(config: Record<string, unknown>): Record<string, unknown> {
|
|
96
|
+
const summary: Record<string, unknown> = {};
|
|
97
|
+
for (const [key, value] of Object.entries(config)) {
|
|
98
|
+
if (key === 'data' && Array.isArray(value)) {
|
|
99
|
+
summary.dataPoints = value.length;
|
|
100
|
+
const first = value[0];
|
|
101
|
+
if (isRecord(first)) summary.dataKeys = Object.keys(first);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
summary[key] = value;
|
|
105
|
+
}
|
|
106
|
+
return summary;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function summarizeNodeResult(node: Record<string, unknown>): Record<string, unknown> {
|
|
110
|
+
const data = isRecord(node.data) ? node.data : {};
|
|
111
|
+
const hiddenDataKeys = new Set(['content', 'fileContent', 'html', 'rendered', 'spec', 'toolResult']);
|
|
112
|
+
const dataKeys = Object.keys(data)
|
|
113
|
+
.filter((key) => !hiddenDataKeys.has(key))
|
|
114
|
+
.sort();
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
...(node.ok !== undefined ? { ok: node.ok } : {}),
|
|
118
|
+
id: node.id ?? null,
|
|
119
|
+
type: node.type ?? null,
|
|
120
|
+
...(typeof node.kind === 'string' ? { kind: node.kind } : {}),
|
|
121
|
+
title: node.title ?? null,
|
|
122
|
+
...(typeof node.content === 'string' ? { contentPreview: truncateText(node.content) } : {}),
|
|
123
|
+
...(node.position !== undefined ? { position: node.position } : {}),
|
|
124
|
+
...(node.size !== undefined ? { size: node.size } : {}),
|
|
125
|
+
...(node.collapsed !== undefined ? { collapsed: node.collapsed } : {}),
|
|
126
|
+
...(node.pinned !== undefined ? { pinned: node.pinned } : {}),
|
|
127
|
+
...(node.dockPosition !== undefined ? { dockPosition: node.dockPosition } : {}),
|
|
128
|
+
...(node.path !== undefined ? { path: node.path } : {}),
|
|
129
|
+
...(node.url !== undefined ? { url: node.url } : {}),
|
|
130
|
+
...(node.provenance !== undefined ? { provenance: node.provenance } : {}),
|
|
131
|
+
...(typeof data.mode === 'string' ? { mode: data.mode } : {}),
|
|
132
|
+
...(typeof data.viewerType === 'string' ? { viewerType: data.viewerType } : {}),
|
|
133
|
+
...(typeof data.serverName === 'string' ? { serverName: data.serverName } : {}),
|
|
134
|
+
...(typeof data.toolName === 'string' ? { toolName: data.toolName } : {}),
|
|
135
|
+
...(typeof data.appSessionId === 'string' ? { appSessionId: data.appSessionId } : {}),
|
|
136
|
+
...(typeof data.sessionStatus === 'string' ? { sessionStatus: data.sessionStatus } : {}),
|
|
137
|
+
...(typeof data.hostMode === 'string' ? { hostMode: data.hostMode } : {}),
|
|
138
|
+
...(typeof data.resourceUri === 'string' ? { resourceUri: data.resourceUri } : {}),
|
|
139
|
+
...(isRecord(data.graphConfig) ? { graph: summarizeGraphConfig(data.graphConfig) } : {}),
|
|
140
|
+
...(dataKeys.length > 0 ? { dataKeys } : {}),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function collectFlagValues(args: string[], name: string): string[] {
|
|
145
|
+
const values: string[] = [];
|
|
146
|
+
for (let i = 0; i < args.length; i++) {
|
|
147
|
+
const arg = args[i];
|
|
148
|
+
const prefix = `--${name}=`;
|
|
149
|
+
if (arg.startsWith(prefix)) {
|
|
150
|
+
const value = arg.slice(prefix.length).trim();
|
|
151
|
+
if (value) values.push(value);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (arg === `--${name}` && i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
|
155
|
+
values.push(args[i + 1] as string);
|
|
156
|
+
i++;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return values;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function collectRequestedFields(args: string[], flags: Record<string, string | true>): string[] {
|
|
163
|
+
const requested = [
|
|
164
|
+
...collectFlagValues(args, 'field'),
|
|
165
|
+
...(typeof flags.fields === 'string'
|
|
166
|
+
? flags.fields
|
|
167
|
+
.split(',')
|
|
168
|
+
.map((value) => value.trim())
|
|
169
|
+
.filter(Boolean)
|
|
170
|
+
: []),
|
|
171
|
+
];
|
|
172
|
+
return Array.from(new Set(requested));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function resolvePathValue(source: unknown, path: string[]): unknown {
|
|
176
|
+
let current = source;
|
|
177
|
+
for (const segment of path) {
|
|
178
|
+
if (!isRecord(current) && !Array.isArray(current)) return undefined;
|
|
179
|
+
current = (current as Record<string, unknown>)[segment];
|
|
180
|
+
}
|
|
181
|
+
return current;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function resolveNodeFieldValue(node: Record<string, unknown>, field: string): unknown {
|
|
185
|
+
if (field.includes('.')) {
|
|
186
|
+
const direct = resolvePathValue(node, field.split('.'));
|
|
187
|
+
if (direct !== undefined) return direct;
|
|
188
|
+
}
|
|
189
|
+
if (field in node) return node[field];
|
|
190
|
+
|
|
191
|
+
const data = isRecord(node.data) ? node.data : null;
|
|
192
|
+
if (!data) return undefined;
|
|
193
|
+
if (field in data) return data[field];
|
|
194
|
+
return field.includes('.') ? resolvePathValue(data, field.split('.')) : undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function listAvailableNodeFields(node: Record<string, unknown>): string[] {
|
|
198
|
+
const topLevel = Object.keys(node).filter((key) => key !== 'data');
|
|
199
|
+
const data = isRecord(node.data) ? Object.keys(node.data).flatMap((key) => [key, `data.${key}`]) : [];
|
|
200
|
+
return Array.from(new Set([...topLevel, ...data])).sort();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function applyStructuredNodeUpdateFlags(
|
|
204
|
+
body: Record<string, unknown>,
|
|
205
|
+
flags: Record<string, string | true>,
|
|
206
|
+
): Promise<void> {
|
|
207
|
+
const specRaw = await readOptionalTextInput(flags, {
|
|
208
|
+
fileFlags: ['spec-file'],
|
|
209
|
+
valueFlags: ['spec-json'],
|
|
210
|
+
allowStdin: false,
|
|
211
|
+
label: 'JSON spec',
|
|
212
|
+
hint: 'Use: pmx-canvas node update <node-id> --spec-file ./new-spec.json',
|
|
213
|
+
});
|
|
214
|
+
if (specRaw !== undefined) {
|
|
215
|
+
body.spec = parseJsonValue(
|
|
216
|
+
specRaw,
|
|
217
|
+
'JSON spec',
|
|
218
|
+
'Use: pmx-canvas node update <node-id> --spec-file ./new-spec.json',
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const graphPatch = await buildGraphRequestBody(flags, { requireData: false, allowStdin: false });
|
|
223
|
+
for (const [key, value] of Object.entries(graphPatch)) {
|
|
224
|
+
body[key === 'height' ? 'chartHeight' : key] = value;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function loadCanvasSchema(): Promise<CanvasSchemaResponse> {
|
|
229
|
+
const result = await invokeOperation('schema.describe', {});
|
|
230
|
+
return result as CanvasSchemaResponse;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function fieldMatches(field: { name: string; aliases?: string[] }, requested: string): boolean {
|
|
234
|
+
return field.name === requested || field.aliases?.includes(requested) === true;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function summarizeNodeSchema(schema: CanvasSchemaType): Record<string, unknown> {
|
|
238
|
+
return {
|
|
239
|
+
type: schema.type,
|
|
240
|
+
kind: schema.kind,
|
|
241
|
+
endpoint: schema.endpoint,
|
|
242
|
+
description: schema.description,
|
|
243
|
+
requiredFields: schema.fields.filter((field) => field.required).map((field) => field.name),
|
|
244
|
+
optionalFields: schema.fields.filter((field) => !field.required).map((field) => field.name),
|
|
245
|
+
exampleKeys: Object.keys(schema.example),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function summarizeJsonRenderComponent(component: JsonRenderComponentSchema): Record<string, unknown> {
|
|
250
|
+
return {
|
|
251
|
+
type: component.type,
|
|
252
|
+
description: component.description,
|
|
253
|
+
slots: component.slots,
|
|
254
|
+
requiredProps: component.props.filter((prop) => prop.required).map((prop) => prop.name),
|
|
255
|
+
optionalProps: component.props.filter((prop) => !prop.required).map((prop) => prop.name),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function printObjectJson(value: unknown): void {
|
|
260
|
+
output(value);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function printNodeSchemaHelp(schema: CanvasSchemaType): void {
|
|
264
|
+
console.log(`\npmx-canvas node add --type ${schema.type} — ${schema.description}\n`);
|
|
265
|
+
console.log(`Endpoint: ${schema.endpoint}`);
|
|
266
|
+
console.log('Flags:');
|
|
267
|
+
for (const field of schema.fields) {
|
|
268
|
+
const aliases = field.aliases?.length ? ` (aliases: ${field.aliases.map((alias) => `--${alias}`).join(', ')})` : '';
|
|
269
|
+
console.log(
|
|
270
|
+
` --${field.name}${field.required ? ' [required]' : ''} <${field.type}> ${field.description}${aliases}`,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
if (schema.notes?.length) {
|
|
274
|
+
console.log('\nNotes:');
|
|
275
|
+
for (const note of schema.notes) {
|
|
276
|
+
console.log(` - ${note}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
console.log('\nCanonical example:');
|
|
280
|
+
console.log(JSON.stringify(schema.example, null, 2));
|
|
281
|
+
console.log('');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function showNodeAddTypeHelp(flags: Record<string, string | true>): Promise<void> {
|
|
285
|
+
const requestedType = getStringFlag(flags, 'type');
|
|
286
|
+
if (!requestedType) {
|
|
287
|
+
showCommandHelp('node add');
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const schema = await loadCanvasSchema();
|
|
292
|
+
let payload: Record<string, unknown> | CanvasSchemaType | JsonRenderComponentSchema | undefined;
|
|
293
|
+
if (requestedType === 'json-render') {
|
|
294
|
+
const componentName = getStringFlag(flags, 'component');
|
|
295
|
+
if (componentName) {
|
|
296
|
+
const component = schema.jsonRender.components.find((entry) => entry.type === componentName);
|
|
297
|
+
if (!component) {
|
|
298
|
+
die(
|
|
299
|
+
`Unknown json-render component: ${componentName}`,
|
|
300
|
+
'Run: pmx-canvas node schema --type json-render --summary',
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
const requestedField = getStringFlag(flags, 'field');
|
|
304
|
+
if (requestedField) {
|
|
305
|
+
const prop = component.props.find((entry) => entry.name === requestedField);
|
|
306
|
+
if (!prop) {
|
|
307
|
+
die(
|
|
308
|
+
`Unknown json-render prop: ${requestedField}`,
|
|
309
|
+
`Run: pmx-canvas node schema --type json-render --component ${componentName}`,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
payload = {
|
|
313
|
+
command: 'node add',
|
|
314
|
+
type: requestedType,
|
|
315
|
+
component: componentName,
|
|
316
|
+
prop,
|
|
317
|
+
};
|
|
318
|
+
} else {
|
|
319
|
+
payload = flags.summary ? summarizeJsonRenderComponent(component) : component;
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
payload = flags.summary
|
|
323
|
+
? {
|
|
324
|
+
type: 'json-render',
|
|
325
|
+
description: 'Native structured UI panel rendered from a validated json-render spec.',
|
|
326
|
+
rootShape: schema.jsonRender.rootShape,
|
|
327
|
+
components: schema.jsonRender.components.map((entry) => summarizeJsonRenderComponent(entry)),
|
|
328
|
+
}
|
|
329
|
+
: {
|
|
330
|
+
type: 'json-render',
|
|
331
|
+
rootShape: schema.jsonRender.rootShape,
|
|
332
|
+
components: schema.jsonRender.components,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
} else if (requestedType === 'graph') {
|
|
336
|
+
const graphSchema = schema.nodeTypes.find((entry) => entry.type === 'graph');
|
|
337
|
+
if (!graphSchema) die('Graph schema is unavailable on the running server.');
|
|
338
|
+
const requestedField = getStringFlag(flags, 'field');
|
|
339
|
+
if (requestedField) {
|
|
340
|
+
const field = graphSchema.fields.find((entry) => fieldMatches(entry, requestedField));
|
|
341
|
+
if (!field) {
|
|
342
|
+
die(`Unknown graph field: ${requestedField}`, 'Run: pmx-canvas node schema --type graph');
|
|
343
|
+
}
|
|
344
|
+
payload = {
|
|
345
|
+
command: 'node add',
|
|
346
|
+
...field,
|
|
347
|
+
};
|
|
348
|
+
} else {
|
|
349
|
+
payload = flags.summary ? summarizeNodeSchema(graphSchema) : graphSchema;
|
|
350
|
+
}
|
|
351
|
+
} else {
|
|
352
|
+
const nodeType = schema.nodeTypes.find((entry) => entry.type === requestedType);
|
|
353
|
+
if (!nodeType) {
|
|
354
|
+
die(`Unknown node type: ${requestedType}`, 'Run: pmx-canvas node schema --summary');
|
|
355
|
+
}
|
|
356
|
+
const requestedField = getStringFlag(flags, 'field');
|
|
357
|
+
if (requestedField) {
|
|
358
|
+
const field = nodeType.fields.find((entry) => fieldMatches(entry, requestedField));
|
|
359
|
+
if (!field) {
|
|
360
|
+
die(`Unknown node field: ${requestedField}`, `Run: pmx-canvas node schema --type ${requestedType}`);
|
|
361
|
+
}
|
|
362
|
+
payload = {
|
|
363
|
+
command: 'node add',
|
|
364
|
+
type: requestedType,
|
|
365
|
+
field,
|
|
366
|
+
};
|
|
367
|
+
} else {
|
|
368
|
+
payload = flags.summary ? summarizeNodeSchema(nodeType) : nodeType;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (flags.json) {
|
|
373
|
+
printObjectJson(payload);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if ('fields' in (payload as CanvasSchemaType)) {
|
|
378
|
+
printNodeSchemaHelp(payload as CanvasSchemaType);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
console.log('');
|
|
383
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
384
|
+
console.log('');
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function filterNodeSchemaView(
|
|
388
|
+
schema: CanvasSchemaType,
|
|
389
|
+
flags: Record<string, string | true>,
|
|
390
|
+
): CanvasSchemaType | Record<string, unknown> {
|
|
391
|
+
const requestedField = getStringFlag(flags, 'field');
|
|
392
|
+
if (requestedField) {
|
|
393
|
+
const field = schema.fields.find((entry) => fieldMatches(entry, requestedField));
|
|
394
|
+
if (!field) {
|
|
395
|
+
die(`Unknown field: ${requestedField}`, `Run: pmx-canvas node schema --type ${schema.type}`);
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
type: schema.type,
|
|
399
|
+
field,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return flags.summary ? summarizeNodeSchema(schema) : schema;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function filterJsonRenderSchemaView(
|
|
407
|
+
schema: CanvasSchemaResponse['jsonRender'],
|
|
408
|
+
flags: Record<string, string | true>,
|
|
409
|
+
): Record<string, unknown> | JsonRenderComponentSchema {
|
|
410
|
+
const componentName = getStringFlag(flags, 'component');
|
|
411
|
+
if (!componentName) {
|
|
412
|
+
return flags.summary
|
|
413
|
+
? {
|
|
414
|
+
rootShape: schema.rootShape,
|
|
415
|
+
components: schema.components.map((entry) => summarizeJsonRenderComponent(entry)),
|
|
416
|
+
}
|
|
417
|
+
: schema;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const component = schema.components.find((entry) => entry.type === componentName);
|
|
421
|
+
if (!component) {
|
|
422
|
+
die(`Unknown json-render component: ${componentName}`, 'Run: pmx-canvas node schema --type json-render --summary');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const requestedField = getStringFlag(flags, 'field');
|
|
426
|
+
if (requestedField) {
|
|
427
|
+
const prop = component.props.find((entry) => entry.name === requestedField);
|
|
428
|
+
if (!prop) {
|
|
429
|
+
die(
|
|
430
|
+
`Unknown json-render prop: ${requestedField}`,
|
|
431
|
+
`Run: pmx-canvas node schema --type json-render --component ${componentName}`,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
component: componentName,
|
|
436
|
+
prop,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return flags.summary ? summarizeJsonRenderComponent(component) : component;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function summarizeHtmlPrimitive(
|
|
444
|
+
primitive: NonNullable<CanvasSchemaResponse['htmlPrimitives']>[number],
|
|
445
|
+
): Record<string, unknown> {
|
|
446
|
+
return {
|
|
447
|
+
kind: primitive.kind,
|
|
448
|
+
title: primitive.title,
|
|
449
|
+
description: primitive.description,
|
|
450
|
+
useWhen: primitive.useWhen,
|
|
451
|
+
defaultSize: primitive.defaultSize,
|
|
452
|
+
dataShape: primitive.dataShape,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function filterHtmlPrimitiveSchemaView(
|
|
457
|
+
schema: CanvasSchemaResponse,
|
|
458
|
+
flags: Record<string, string | true>,
|
|
459
|
+
): Record<string, unknown> {
|
|
460
|
+
const primitives = schema.htmlPrimitives ?? [];
|
|
461
|
+
const kind = getStringFlag(flags, 'kind', 'primitive');
|
|
462
|
+
if (!kind) {
|
|
463
|
+
return {
|
|
464
|
+
primitives: flags.summary ? primitives.map((entry) => summarizeHtmlPrimitive(entry)) : primitives,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
const primitive = primitives.find((entry) => entry.kind === kind);
|
|
468
|
+
if (!primitive) {
|
|
469
|
+
die(`Unknown HTML primitive: ${kind}`, 'Run: pmx-canvas html primitive schema --summary');
|
|
470
|
+
}
|
|
471
|
+
return flags.summary ? summarizeHtmlPrimitive(primitive) : primitive;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ── node add ─────────────────────────────────────────────────
|
|
475
|
+
cmd(
|
|
476
|
+
'node add',
|
|
477
|
+
'Add a node to the canvas',
|
|
478
|
+
[
|
|
479
|
+
'pmx-canvas node add --type markdown --title "Design Doc" --content "# Overview"',
|
|
480
|
+
'pmx-canvas node add --type status --title "Build" --content "passing"',
|
|
481
|
+
'pmx-canvas node add --type file --content "src/index.ts"',
|
|
482
|
+
'pmx-canvas node add --type webpage --url "https://example.com/docs"',
|
|
483
|
+
'pmx-canvas node add --type html --title "Widget" --content "<main>Hello</main>"',
|
|
484
|
+
'pmx-canvas node add --type html --title "Showcase" --content ./report.html (a .html path is read from disk; otherwise --content is raw HTML)',
|
|
485
|
+
'pmx-canvas node add --type html --primitive choice-grid --data-file ./options.json --title "Options"',
|
|
486
|
+
'pmx-canvas node add --type markdown --title "Note" --x 100 --y 200',
|
|
487
|
+
'pmx-canvas node add --type json-render --title "Ops Dashboard" --spec-file ./dashboard.json',
|
|
488
|
+
'pmx-canvas node add --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
|
|
489
|
+
'pmx-canvas node add --type web-artifact --title "Dashboard" --app-file ./App.tsx',
|
|
490
|
+
],
|
|
491
|
+
async (args) => {
|
|
492
|
+
const { flags } = parseFlags(args);
|
|
493
|
+
if (flags.help || flags.h) return showNodeAddTypeHelp(flags);
|
|
494
|
+
|
|
495
|
+
const type = (flags.type as string) || 'markdown';
|
|
496
|
+
|
|
497
|
+
if (type === 'json-render') {
|
|
498
|
+
const result = await invokeOperation('jsonrender.add', await buildJsonRenderRequestBody(flags));
|
|
499
|
+
output(result);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (type === 'graph') {
|
|
504
|
+
const result = await invokeOperation('graph.add', await buildGraphRequestBody(flags));
|
|
505
|
+
output(result);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (type === 'web-artifact') {
|
|
510
|
+
await runWebArtifactBuildCommand(flags);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (type === 'html-primitive') {
|
|
515
|
+
const result = await invokeOperation('node.add', await buildHtmlPrimitiveRequestBody(flags));
|
|
516
|
+
output(result);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (type === 'html' && getStringFlag(flags, 'primitive', 'kind')) {
|
|
521
|
+
const result = await invokeOperation('node.add', await buildHtmlPrimitiveRequestBody(flags));
|
|
522
|
+
output(result);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (type === 'mcp-app') {
|
|
527
|
+
die(
|
|
528
|
+
'mcp-app nodes require tool-backed app metadata and cannot be created with generic node add.',
|
|
529
|
+
'Use: pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx, or pmx-canvas external-app add --kind excalidraw --title "Diagram"',
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const body: Record<string, unknown> = { type };
|
|
534
|
+
if (flags.title) body.title = flags.title;
|
|
535
|
+
const webpageUrl = getStringFlag(flags, 'url');
|
|
536
|
+
const imagePath = getStringFlag(flags, 'path');
|
|
537
|
+
if (type === 'webpage' && webpageUrl) {
|
|
538
|
+
body.url = webpageUrl;
|
|
539
|
+
} else if (type === 'image' && imagePath && !flags.content) {
|
|
540
|
+
body.content = imagePath;
|
|
541
|
+
} else if (type === 'html') {
|
|
542
|
+
const html = getStringFlag(flags, 'html') ?? getStringFlag(flags, 'content');
|
|
543
|
+
if (html !== undefined) body.html = html;
|
|
544
|
+
const summary = getStringFlag(flags, 'summary');
|
|
545
|
+
const agentSummary = getStringFlag(flags, 'agent-summary', 'agentSummary');
|
|
546
|
+
const description = getStringFlag(flags, 'description');
|
|
547
|
+
if (summary !== undefined) body.summary = summary;
|
|
548
|
+
if (agentSummary !== undefined) body.agentSummary = agentSummary;
|
|
549
|
+
if (description !== undefined) body.description = description;
|
|
550
|
+
if (optionalBooleanFlag(flags, 'presentation', 'Use --presentation true or --presentation false') === true)
|
|
551
|
+
body.presentation = true;
|
|
552
|
+
if (typeof flags['slide-title'] === 'string') body.slideTitles = [flags['slide-title']];
|
|
553
|
+
if (typeof flags['embedded-node-id'] === 'string') body.embeddedNodeIds = [flags['embedded-node-id']];
|
|
554
|
+
} else if (flags.content) {
|
|
555
|
+
body.content = flags.content;
|
|
556
|
+
}
|
|
557
|
+
applyCommonGeometryFlags(body, flags, {
|
|
558
|
+
x: 'Use a finite number, e.g. --x 500',
|
|
559
|
+
y: 'Use a finite number, e.g. --y 300',
|
|
560
|
+
width: 'Use a positive number, e.g. --width 500',
|
|
561
|
+
height: 'Use a positive number, e.g. --height 280',
|
|
562
|
+
});
|
|
563
|
+
applyStrictSizeFlags(body, flags);
|
|
564
|
+
if (type === 'trace') {
|
|
565
|
+
for (const field of TRACE_NODE_FIELDS) {
|
|
566
|
+
const value = getStringFlag(
|
|
567
|
+
flags,
|
|
568
|
+
field,
|
|
569
|
+
field.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`),
|
|
570
|
+
);
|
|
571
|
+
if (value !== undefined) body[field] = value;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Support --stdin for piping content
|
|
576
|
+
if (flags.stdin) {
|
|
577
|
+
if (type === 'webpage') {
|
|
578
|
+
body.url = await readStdin();
|
|
579
|
+
} else if (type === 'html') {
|
|
580
|
+
body.html = await readStdin();
|
|
581
|
+
} else {
|
|
582
|
+
body.content = await readStdin();
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const result = await invokeOperation('node.add', body);
|
|
587
|
+
output(result);
|
|
588
|
+
},
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
cmd(
|
|
592
|
+
'json-render',
|
|
593
|
+
'Show json-render schema and canonical examples',
|
|
594
|
+
[
|
|
595
|
+
'pmx-canvas json-render --schema --summary',
|
|
596
|
+
'pmx-canvas json-render --examples',
|
|
597
|
+
'pmx-canvas json-render --example --component Table',
|
|
598
|
+
'pmx-canvas json-render --schema --component Badge --field variant',
|
|
599
|
+
],
|
|
600
|
+
async (args) => {
|
|
601
|
+
const { flags } = parseFlags(args);
|
|
602
|
+
if (flags.help || flags.h) return showCommandHelp('json-render');
|
|
603
|
+
|
|
604
|
+
const schema = await loadCanvasSchema();
|
|
605
|
+
const componentName = getStringFlag(flags, 'component');
|
|
606
|
+
const fieldName = getStringFlag(flags, 'field');
|
|
607
|
+
|
|
608
|
+
if (flags.example || flags.examples) {
|
|
609
|
+
if (fieldName)
|
|
610
|
+
die(
|
|
611
|
+
'--field is only supported with --schema.',
|
|
612
|
+
'Use: pmx-canvas json-render --schema --component Table --field rows',
|
|
613
|
+
);
|
|
614
|
+
if (componentName) {
|
|
615
|
+
const component = schema.jsonRender.components.find((entry) => entry.type === componentName);
|
|
616
|
+
if (!component)
|
|
617
|
+
die(`Unknown json-render component: ${componentName}`, 'Run: pmx-canvas json-render --schema --summary');
|
|
618
|
+
output({ component: component.type, example: component.example });
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
output({
|
|
622
|
+
rootShape: schema.jsonRender.rootShape,
|
|
623
|
+
examples: Object.fromEntries(schema.jsonRender.components.map((entry) => [entry.type, entry.example])),
|
|
624
|
+
});
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
output(filterJsonRenderSchemaView(schema.jsonRender, flags));
|
|
629
|
+
},
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
cmd(
|
|
633
|
+
'html primitive add',
|
|
634
|
+
'Create a reusable sandboxed HTML communication primitive',
|
|
635
|
+
[
|
|
636
|
+
'pmx-canvas html primitive add --kind choice-grid --data-file ./options.json --title "Options"',
|
|
637
|
+
'pmx-canvas html primitive add --kind plan-timeline --data-json \'{"milestones":[{"title":"Ship","detail":"Implement and verify","status":"next"}]}\'',
|
|
638
|
+
'pmx-canvas html primitive add --kind triage-board --data-file ./tickets.json --strict-size',
|
|
639
|
+
],
|
|
640
|
+
async (args) => {
|
|
641
|
+
const { flags } = parseFlags(args);
|
|
642
|
+
if (flags.help || flags.h) return showCommandHelp('html primitive add');
|
|
643
|
+
const result = await invokeOperation('node.add', await buildHtmlPrimitiveRequestBody(flags));
|
|
644
|
+
output(result);
|
|
645
|
+
},
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
cmd(
|
|
649
|
+
'html primitive schema',
|
|
650
|
+
'Describe reusable HTML communication primitives',
|
|
651
|
+
[
|
|
652
|
+
'pmx-canvas html primitive schema --summary',
|
|
653
|
+
'pmx-canvas html primitive schema --kind choice-grid',
|
|
654
|
+
'pmx-canvas html primitive schema --kind triage-board --summary',
|
|
655
|
+
],
|
|
656
|
+
async (args) => {
|
|
657
|
+
const { flags } = parseFlags(args);
|
|
658
|
+
if (flags.help || flags.h) return showCommandHelp('html primitive schema');
|
|
659
|
+
const schema = await loadCanvasSchema();
|
|
660
|
+
output(filterHtmlPrimitiveSchemaView(schema, flags));
|
|
661
|
+
},
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
cmd(
|
|
665
|
+
'graph add',
|
|
666
|
+
'Add a graph node to the canvas',
|
|
667
|
+
[
|
|
668
|
+
'pmx-canvas graph add --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
|
|
669
|
+
'pmx-canvas graph add --graphType composed --data \'[{"day":"Mon","visits":10,"conversion":0.4}]\' --xKey day --barKey visits --lineKey conversion',
|
|
670
|
+
'pmx-canvas node add --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
|
|
671
|
+
],
|
|
672
|
+
async (args) => {
|
|
673
|
+
const { flags } = parseFlags(args);
|
|
674
|
+
if (flags.help || flags.h) return showCommandHelp('graph add');
|
|
675
|
+
|
|
676
|
+
const result = await invokeOperation('graph.add', await buildGraphRequestBody(flags));
|
|
677
|
+
output(result);
|
|
678
|
+
},
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
cmd(
|
|
682
|
+
'node schema',
|
|
683
|
+
'Describe server-supported node create schemas and canonical examples',
|
|
684
|
+
[
|
|
685
|
+
'pmx-canvas node schema',
|
|
686
|
+
'pmx-canvas node schema --type webpage',
|
|
687
|
+
'pmx-canvas node schema --type json-render',
|
|
688
|
+
'pmx-canvas json-render --schema --summary',
|
|
689
|
+
'pmx-canvas node schema --type json-render --component Table',
|
|
690
|
+
'pmx-canvas node schema --type webpage --field url',
|
|
691
|
+
'pmx-canvas node schema --summary',
|
|
692
|
+
],
|
|
693
|
+
async (args) => {
|
|
694
|
+
const { flags } = parseFlags(args);
|
|
695
|
+
if (flags.help || flags.h) return showCommandHelp('node schema');
|
|
696
|
+
|
|
697
|
+
const result = await loadCanvasSchema();
|
|
698
|
+
if (getStringFlag(flags, 'component') && flags.type !== 'json-render') {
|
|
699
|
+
die('--component is only supported with --type json-render.');
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (typeof flags.type !== 'string') {
|
|
703
|
+
if (flags.summary) {
|
|
704
|
+
output({
|
|
705
|
+
source: result.source,
|
|
706
|
+
version: result.version,
|
|
707
|
+
nodeTypes: result.nodeTypes.map((entry) => summarizeNodeSchema(entry)),
|
|
708
|
+
jsonRender: {
|
|
709
|
+
componentCount: result.jsonRender.components.length,
|
|
710
|
+
rootShape: result.jsonRender.rootShape,
|
|
711
|
+
},
|
|
712
|
+
graph: result.graph,
|
|
713
|
+
htmlPrimitives: result.htmlPrimitives?.map((entry) => summarizeHtmlPrimitive(entry)) ?? [],
|
|
714
|
+
mcp: result.mcp,
|
|
715
|
+
});
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
output(result);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const requested = flags.type;
|
|
723
|
+
if (requested === 'json-render') {
|
|
724
|
+
output(filterJsonRenderSchemaView(result.jsonRender, flags));
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
if (requested === 'graph') {
|
|
728
|
+
const graphSchema = result.nodeTypes.find((entry) => entry.type === 'graph');
|
|
729
|
+
if (graphSchema) {
|
|
730
|
+
output(filterNodeSchemaView(graphSchema, flags));
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
output(flags.summary ? result.graph : { ...result.graph });
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const nodeType = result.nodeTypes.find((entry) => entry.type === requested);
|
|
737
|
+
if (nodeType) {
|
|
738
|
+
output(filterNodeSchemaView(nodeType, flags));
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
die(`Unknown schema type: ${requested}`, 'Run: pmx-canvas node schema');
|
|
742
|
+
},
|
|
743
|
+
);
|
|
744
|
+
|
|
745
|
+
// ── node list ────────────────────────────────────────────────
|
|
746
|
+
cmd(
|
|
747
|
+
'node list',
|
|
748
|
+
'List all nodes on the canvas',
|
|
749
|
+
[
|
|
750
|
+
'pmx-canvas node list',
|
|
751
|
+
'pmx-canvas node list --type markdown',
|
|
752
|
+
'pmx-canvas node list --type mcp-app',
|
|
753
|
+
'pmx-canvas node list --ids',
|
|
754
|
+
],
|
|
755
|
+
async (args) => {
|
|
756
|
+
const { flags } = parseFlags(args);
|
|
757
|
+
if (flags.help || flags.h) return showCommandHelp('node list');
|
|
758
|
+
|
|
759
|
+
const layout = (await invokeOperation('layout.get', {})) as { nodes: Array<Record<string, unknown>> };
|
|
760
|
+
let nodes = layout.nodes;
|
|
761
|
+
|
|
762
|
+
if (flags.type && flags.type !== true) {
|
|
763
|
+
nodes = nodes.filter((n) => n.type === flags.type || n.kind === flags.type);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (flags.ids) {
|
|
767
|
+
output(nodes.map((n) => n.id));
|
|
768
|
+
} else {
|
|
769
|
+
const shouldSummarize = flags.summary === true || flags.compact === true || flags.type === 'mcp-app';
|
|
770
|
+
output(shouldSummarize ? nodes.map((node) => summarizeNodeResult(node)) : nodes);
|
|
771
|
+
}
|
|
772
|
+
},
|
|
773
|
+
);
|
|
774
|
+
|
|
775
|
+
// ── node get ─────────────────────────────────────────────────
|
|
776
|
+
cmd(
|
|
777
|
+
'node get',
|
|
778
|
+
'Get a node by ID',
|
|
779
|
+
[
|
|
780
|
+
'pmx-canvas node get <node-id>',
|
|
781
|
+
'pmx-canvas node get node-abc123',
|
|
782
|
+
'pmx-canvas node get node-abc123 --summary',
|
|
783
|
+
'pmx-canvas node get node-abc123 --field title --field graphConfig',
|
|
784
|
+
],
|
|
785
|
+
async (args) => {
|
|
786
|
+
const { positional, flags } = parseFlags(args);
|
|
787
|
+
if (flags.help || flags.h) return showCommandHelp('node get');
|
|
788
|
+
|
|
789
|
+
const id = positional[0];
|
|
790
|
+
if (!id) die('Missing node ID', 'pmx-canvas node get <node-id>');
|
|
791
|
+
|
|
792
|
+
const result = (await invokeOperation('node.get', { id })) as Record<string, unknown>;
|
|
793
|
+
const requestedFields = collectRequestedFields(args, flags);
|
|
794
|
+
if (requestedFields.length > 0) {
|
|
795
|
+
const picked = Object.fromEntries(requestedFields.map((field) => [field, resolveNodeFieldValue(result, field)]));
|
|
796
|
+
const missing = requestedFields.filter((field) => picked[field] === undefined);
|
|
797
|
+
if (missing.length > 0) {
|
|
798
|
+
die(
|
|
799
|
+
`Unknown node field${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}`,
|
|
800
|
+
`Available fields: ${listAvailableNodeFields(result).join(', ')}`,
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
output({
|
|
804
|
+
id: result.id ?? id,
|
|
805
|
+
fields: picked,
|
|
806
|
+
});
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (flags.summary || flags.compact) {
|
|
811
|
+
output(summarizeNodeResult(result));
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
output(result);
|
|
815
|
+
},
|
|
816
|
+
);
|
|
817
|
+
|
|
818
|
+
// ── node update ──────────────────────────────────────────────
|
|
819
|
+
cmd(
|
|
820
|
+
'node update',
|
|
821
|
+
'Update a node by ID',
|
|
822
|
+
[
|
|
823
|
+
'pmx-canvas node update <node-id> --title "New Title"',
|
|
824
|
+
'pmx-canvas node update <node-id> --content "Updated content"',
|
|
825
|
+
'pmx-canvas node update <node-id> --title "Moved" --x 500 --y 300',
|
|
826
|
+
'pmx-canvas node update <node-id> --width 840 --height 620',
|
|
827
|
+
'pmx-canvas node update <node-id> --spec-file ./dashboard.json',
|
|
828
|
+
'pmx-canvas node update <graph-id> --data-file ./metrics.json --chart-height 420',
|
|
829
|
+
'pmx-canvas node update <node-id> --pinned true',
|
|
830
|
+
'pmx-canvas node update <node-id> --dock-position right',
|
|
831
|
+
'pmx-canvas node update <node-id> --dock-position none # undock back to the canvas',
|
|
832
|
+
'pmx-canvas node update <node-id> --lock-arrange',
|
|
833
|
+
],
|
|
834
|
+
async (args) => {
|
|
835
|
+
const { positional, flags } = parseFlags(args);
|
|
836
|
+
if (flags.help || flags.h) return showCommandHelp('node update');
|
|
837
|
+
|
|
838
|
+
const id = positional[0];
|
|
839
|
+
if (!id) die('Missing node ID', 'pmx-canvas node update <node-id> --title "New Title"');
|
|
840
|
+
|
|
841
|
+
const body: Record<string, unknown> = {};
|
|
842
|
+
await applyStructuredNodeUpdateFlags(body, flags);
|
|
843
|
+
if (flags.title && flags.title !== true) body.title = flags.title;
|
|
844
|
+
if (flags.content && flags.content !== true) body.content = flags.content;
|
|
845
|
+
if (flags.stdin) body.content = await readStdin();
|
|
846
|
+
|
|
847
|
+
const x = optionalFiniteFlag(flags, 'x', 'Use a finite number, e.g. --x 500');
|
|
848
|
+
const y = optionalFiniteFlag(flags, 'y', 'Use a finite number, e.g. --y 300');
|
|
849
|
+
const width = optionalPositiveFiniteFlag(flags, 'width', 'Use a positive number, e.g. --width 840');
|
|
850
|
+
const height = optionalPositiveFiniteFlag(flags, 'height', 'Use a positive number, e.g. --height 620');
|
|
851
|
+
const nodeHeight = optionalPositiveFiniteFlagWithAliases(
|
|
852
|
+
flags,
|
|
853
|
+
'Use a positive number, e.g. --node-height 620',
|
|
854
|
+
'node-height',
|
|
855
|
+
'nodeHeight',
|
|
856
|
+
);
|
|
857
|
+
if (height !== undefined && nodeHeight !== undefined) {
|
|
858
|
+
die('Use either --height/--node-height, not both.');
|
|
859
|
+
}
|
|
860
|
+
const frameHeight = height ?? nodeHeight;
|
|
861
|
+
const pinned = optionalBooleanFlag(flags, 'pinned', 'Use --pinned true or --pinned false');
|
|
862
|
+
if (flags['lock-arrange'] && flags['unlock-arrange']) {
|
|
863
|
+
die('Use either --lock-arrange or --unlock-arrange, not both.');
|
|
864
|
+
}
|
|
865
|
+
const arrangeLocked = flags['lock-arrange'] ? true : flags['unlock-arrange'] ? false : undefined;
|
|
866
|
+
|
|
867
|
+
applyStrictSizeFlags(body, flags);
|
|
868
|
+
|
|
869
|
+
for (const field of TRACE_NODE_FIELDS) {
|
|
870
|
+
const value = getStringFlag(
|
|
871
|
+
flags,
|
|
872
|
+
field,
|
|
873
|
+
field.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`),
|
|
874
|
+
);
|
|
875
|
+
if (value !== undefined) body[field] = value;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
if (
|
|
879
|
+
x !== undefined ||
|
|
880
|
+
y !== undefined ||
|
|
881
|
+
width !== undefined ||
|
|
882
|
+
frameHeight !== undefined ||
|
|
883
|
+
arrangeLocked !== undefined
|
|
884
|
+
) {
|
|
885
|
+
const existing = (await invokeOperation('node.get', { id })) as {
|
|
886
|
+
position: { x: number; y: number };
|
|
887
|
+
size: { width: number; height: number };
|
|
888
|
+
data: Record<string, unknown>;
|
|
889
|
+
};
|
|
890
|
+
|
|
891
|
+
if (x !== undefined || y !== undefined) {
|
|
892
|
+
body.position = {
|
|
893
|
+
x: x ?? existing.position.x,
|
|
894
|
+
y: y ?? existing.position.y,
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
if (width !== undefined || frameHeight !== undefined) {
|
|
899
|
+
body.size = {
|
|
900
|
+
width: width ?? existing.size.width,
|
|
901
|
+
height: frameHeight ?? existing.size.height,
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
if (arrangeLocked !== undefined) {
|
|
906
|
+
body.arrangeLocked = arrangeLocked;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if (pinned !== undefined) body.pinned = pinned;
|
|
911
|
+
|
|
912
|
+
// --dock-position left|right|none : dock a node into the top HUD or undock it.
|
|
913
|
+
// `none`/`null`/empty map to JS null (undock). Assigned with a !== undefined
|
|
914
|
+
// guard so the null survives JSON.stringify to the server (which accepts a
|
|
915
|
+
// top-level dockPosition: null). HTTP PATCH already supports this; this is the
|
|
916
|
+
// CLI path the report (#40) found missing.
|
|
917
|
+
const dockRaw = getStringFlag(flags, 'dock-position', 'dockPosition');
|
|
918
|
+
let dockPosition: 'left' | 'right' | null | undefined;
|
|
919
|
+
if (dockRaw !== undefined) {
|
|
920
|
+
const v = dockRaw.trim().toLowerCase();
|
|
921
|
+
if (v === 'left' || v === 'right') dockPosition = v;
|
|
922
|
+
else if (v === 'none' || v === 'null' || v === '') dockPosition = null;
|
|
923
|
+
else die(`Invalid --dock-position "${dockRaw}".`, 'Use left, right, or none (to undock).');
|
|
924
|
+
}
|
|
925
|
+
if (dockPosition !== undefined) body.dockPosition = dockPosition;
|
|
926
|
+
|
|
927
|
+
if (Object.keys(body).length === 0) {
|
|
928
|
+
die(
|
|
929
|
+
'No updates specified',
|
|
930
|
+
'Use --title, --content, --x, --y, --width, --height, --strict-size, --pinned, --dock-position, trace fields, --lock-arrange, --unlock-arrange, or --stdin',
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const result = await invokeOperation('node.update', { id, ...body });
|
|
935
|
+
output(result);
|
|
936
|
+
},
|
|
937
|
+
);
|
|
938
|
+
|
|
939
|
+
// ── node remove ──────────────────────────────────────────────
|
|
940
|
+
cmd(
|
|
941
|
+
'node remove',
|
|
942
|
+
'Remove a node from the canvas',
|
|
943
|
+
['pmx-canvas node remove <node-id>', 'pmx-canvas node remove node-abc123'],
|
|
944
|
+
async (args) => {
|
|
945
|
+
const { positional, flags } = parseFlags(args);
|
|
946
|
+
if (flags.help || flags.h) return showCommandHelp('node remove');
|
|
947
|
+
|
|
948
|
+
const id = positional[0];
|
|
949
|
+
if (!id) die('Missing node ID', 'pmx-canvas node remove <node-id>');
|
|
950
|
+
|
|
951
|
+
const result = await invokeOperation('node.remove', { id });
|
|
952
|
+
output(result);
|
|
953
|
+
},
|
|
954
|
+
);
|