pmx-canvas 0.2.7 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +148 -1
- package/Readme.md +13 -11
- package/dist/canvas/index.js +2 -2
- package/dist/json-render/index.css +1 -1
- package/dist/types/cli/daemon.d.ts +74 -0
- package/dist/types/cli/watch.d.ts +2 -2
- package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
- package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
- package/dist/types/client/canvas/Minimap.d.ts +1 -1
- package/dist/types/client/nodes/FileNode.d.ts +1 -1
- package/dist/types/client/nodes/ImageNode.d.ts +1 -1
- package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
- package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
- package/dist/types/client/nodes/PromptNode.d.ts +1 -1
- package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
- package/dist/types/mcp/canvas-access.d.ts +0 -7
- package/dist/types/server/ax-state-manager.d.ts +3 -2
- package/dist/types/server/bundled-skills.d.ts +3 -3
- package/dist/types/server/canvas-schema.d.ts +1 -1
- package/dist/types/server/canvas-state.d.ts +2 -0
- package/dist/types/server/ext-app-lookup.d.ts +1 -3
- package/dist/types/server/html-primitives.d.ts +1 -1
- package/dist/types/server/index.d.ts +4 -4
- package/dist/types/server/operations/composites.d.ts +29 -23
- package/dist/types/server/operations/http.d.ts +2 -1
- package/dist/types/server/operations/index.d.ts +1 -1
- package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
- package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
- package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
- package/dist/types/shared/ax-intent.d.ts +1 -1
- package/docs/RELEASE.md +23 -8
- package/docs/ax-host-adapter-contract.md +7 -7
- package/docs/ax-state-contract.md +7 -6
- package/docs/bun-webview-integration.md +5 -5
- package/docs/cli.md +1 -1
- package/docs/http-api.md +35 -3
- package/docs/mcp.md +121 -100
- package/docs/node-types.md +45 -35
- package/docs/plans/plan-006-mcp-tool-consolidation.md +3 -1
- package/docs/plans/plan-008-registry-finish.md +2 -0
- package/docs/plans/plan-009-tech-debt-backlog.md +51 -0
- package/docs/screenshot.png +0 -0
- package/docs/tech-debt-assessment-2026-06.md +1 -1
- package/docs/tech-debt-assessment-2026-07.md +135 -0
- package/package.json +5 -2
- package/skills/data-analysis/SKILL.md +3 -3
- package/skills/frontend-design/SKILL.md +3 -2
- package/skills/json-render-mcp/SKILL.md +4 -3
- package/skills/playwright-cli/SKILL.md +1 -1
- package/skills/pmx-canvas/SKILL.md +12 -7
- package/skills/pmx-canvas/evals/evals.json +2 -2
- package/skills/pmx-canvas/references/ax-html-control-surface.md +3 -1
- package/skills/pmx-canvas/references/excalidraw-diagram-authoring.md +2 -2
- package/skills/pmx-canvas/references/full-reference.md +58 -56
- package/skills/pmx-canvas/references/html-primitives.md +3 -3
- package/skills/tufte-viz/SKILL.md +5 -4
- package/skills/web-artifacts-builder/SKILL.md +4 -4
- package/src/cli/agent.ts +1861 -1548
- package/src/cli/daemon.ts +460 -0
- package/src/cli/index.ts +63 -326
- package/src/cli/watch.ts +2 -10
- package/src/client/App.tsx +48 -46
- package/src/client/canvas/AttentionHistory.tsx +11 -1
- package/src/client/canvas/CanvasNode.tsx +41 -29
- package/src/client/canvas/CanvasViewport.tsx +101 -66
- package/src/client/canvas/CommandPalette.tsx +61 -27
- package/src/client/canvas/ContextMenu.tsx +13 -20
- package/src/client/canvas/ContextPinBar.tsx +1 -5
- package/src/client/canvas/ContextPinHud.tsx +1 -6
- package/src/client/canvas/DockedNode.tsx +4 -4
- package/src/client/canvas/EdgeLayer.tsx +37 -36
- package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
- package/src/client/canvas/FocusFieldLayer.tsx +20 -22
- package/src/client/canvas/IntentLayer.tsx +31 -14
- package/src/client/canvas/Minimap.tsx +11 -16
- package/src/client/canvas/SelectionBar.tsx +4 -11
- package/src/client/canvas/ShortcutOverlay.tsx +3 -1
- package/src/client/canvas/SnapshotPanel.tsx +77 -95
- package/src/client/canvas/auto-fit.ts +15 -14
- package/src/client/canvas/snap-guides.ts +12 -12
- package/src/client/canvas/use-node-resize.ts +1 -5
- package/src/client/canvas/use-pan-zoom.ts +25 -26
- package/src/client/ext-app/bridge.ts +3 -12
- package/src/client/icons.tsx +63 -20
- package/src/client/nodes/ContextNode.tsx +14 -25
- package/src/client/nodes/ExtAppFrame.tsx +60 -39
- package/src/client/nodes/FileNode.tsx +74 -62
- package/src/client/nodes/GroupNode.tsx +4 -6
- package/src/client/nodes/HtmlNode.tsx +76 -46
- package/src/client/nodes/ImageNode.tsx +18 -27
- package/src/client/nodes/InlineFormatBar.tsx +4 -21
- package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
- package/src/client/nodes/LedgerNode.tsx +10 -4
- package/src/client/nodes/MarkdownNode.tsx +3 -10
- package/src/client/nodes/McpAppNode.tsx +26 -22
- package/src/client/nodes/MdFormatBar.tsx +10 -7
- package/src/client/nodes/PromptNode.tsx +23 -51
- package/src/client/nodes/ResponseNode.tsx +3 -13
- package/src/client/nodes/StatusNode.tsx +5 -9
- package/src/client/nodes/StatusSummary.tsx +2 -8
- package/src/client/nodes/WebpageNode.tsx +20 -14
- package/src/client/nodes/iframe-document-url.ts +25 -16
- package/src/client/nodes/image-warnings.ts +1 -7
- package/src/client/nodes/md-format.ts +20 -5
- package/src/client/state/attention-bridge.ts +4 -9
- package/src/client/state/attention-store.ts +1 -7
- package/src/client/state/canvas-store.ts +52 -36
- package/src/client/state/intent-bridge.ts +176 -112
- package/src/client/state/intent-store.ts +4 -1
- package/src/client/state/sse-bridge.ts +53 -70
- package/src/json-render/catalog.ts +12 -16
- package/src/json-render/charts/components.tsx +16 -20
- package/src/json-render/charts/extra-components.tsx +8 -16
- package/src/json-render/charts/extra-definitions.ts +1 -2
- package/src/json-render/charts/tufte-components.tsx +37 -20
- package/src/json-render/charts/tufte-definitions.ts +8 -2
- package/src/json-render/renderer/index.tsx +42 -22
- package/src/json-render/schema.ts +6 -3
- package/src/json-render/server.ts +33 -39
- package/src/mcp/canvas-access.ts +40 -121
- package/src/mcp/server.ts +206 -271
- package/src/server/agent-context.ts +63 -36
- package/src/server/ax-context.ts +7 -5
- package/src/server/ax-interaction.ts +176 -43
- package/src/server/ax-state-manager.ts +185 -41
- package/src/server/ax-state.ts +142 -47
- package/src/server/bundled-skills.ts +3 -3
- package/src/server/canvas-db.ts +213 -95
- package/src/server/canvas-operations.ts +177 -120
- package/src/server/canvas-provenance.ts +1 -4
- package/src/server/canvas-schema.ts +500 -102
- package/src/server/canvas-serialization.ts +27 -35
- package/src/server/canvas-state.ts +155 -58
- package/src/server/chart-template.ts +4 -5
- package/src/server/code-graph.ts +20 -7
- package/src/server/diagram-presets.ts +28 -29
- package/src/server/ext-app-lookup.ts +4 -20
- package/src/server/html-node-summary.ts +19 -10
- package/src/server/html-primitives.ts +326 -97
- package/src/server/html-surface.ts +6 -9
- package/src/server/image-source.ts +6 -4
- package/src/server/index.ts +321 -218
- package/src/server/intent-registry.ts +3 -6
- package/src/server/mcp-app-candidate.ts +5 -10
- package/src/server/mcp-app-host.ts +14 -38
- package/src/server/mcp-app-runtime.ts +12 -20
- package/src/server/mutation-history.ts +15 -5
- package/src/server/operations/composites.ts +38 -40
- package/src/server/operations/http.ts +5 -5
- package/src/server/operations/index.ts +7 -1
- package/src/server/operations/invoker.ts +4 -3
- package/src/server/operations/mcp.ts +29 -36
- package/src/server/operations/ops/annotation.ts +122 -10
- package/src/server/operations/ops/app.ts +102 -77
- package/src/server/operations/ops/ax-await.ts +17 -10
- package/src/server/operations/ops/ax-read.ts +347 -0
- package/src/server/operations/ops/ax-shared.ts +2 -7
- package/src/server/operations/ops/ax-state.ts +32 -14
- package/src/server/operations/ops/ax-timeline.ts +32 -19
- package/src/server/operations/ops/ax-work.ts +54 -37
- package/src/server/operations/ops/batch.ts +39 -14
- package/src/server/operations/ops/canvas-wire.ts +91 -0
- package/src/server/operations/ops/edges.ts +37 -25
- package/src/server/operations/ops/ext-app.ts +346 -0
- package/src/server/operations/ops/groups.ts +49 -20
- package/src/server/operations/ops/intent.ts +18 -12
- package/src/server/operations/ops/json-render.ts +239 -98
- package/src/server/operations/ops/nodes.ts +300 -111
- package/src/server/operations/ops/query.ts +62 -33
- package/src/server/operations/ops/snapshots.ts +35 -26
- package/src/server/operations/ops/validate.ts +2 -1
- package/src/server/operations/ops/viewport.ts +60 -16
- package/src/server/operations/ops/webview.ts +44 -18
- package/src/server/operations/registry.ts +2 -3
- package/src/server/operations/types.ts +7 -5
- package/src/server/operations/webview-runner.ts +1 -3
- package/src/server/placement.ts +8 -18
- package/src/server/server.ts +121 -1012
- package/src/server/spatial-analysis.ts +39 -25
- package/src/server/trace-manager.ts +3 -8
- package/src/server/web-artifacts.ts +23 -27
- package/src/server/webpage-node.ts +5 -13
- package/src/shared/auto-arrange.ts +12 -5
- package/src/shared/ax-intent.ts +1 -1
- package/src/shared/content-height-reporter.ts +8 -6
- package/src/shared/ext-app-tool-result.ts +2 -6
- package/src/shared/placement.ts +1 -4
- package/src/shared/semantic-attention.ts +39 -37
- package/src/shared/surface.ts +8 -4
package/src/cli/agent.ts
CHANGED
|
@@ -72,9 +72,7 @@ interface CanvasSchemaResponse {
|
|
|
72
72
|
components: JsonRenderComponentSchema[];
|
|
73
73
|
};
|
|
74
74
|
graph: {
|
|
75
|
-
graphTypes: Array<
|
|
76
|
-
'line' | 'bar' | 'pie' | 'area' | 'scatter' | 'radar' | 'stacked-bar' | 'composed'
|
|
77
|
-
>;
|
|
75
|
+
graphTypes: Array<'line' | 'bar' | 'pie' | 'area' | 'scatter' | 'radar' | 'stacked-bar' | 'composed'>;
|
|
78
76
|
};
|
|
79
77
|
htmlPrimitives?: Array<{
|
|
80
78
|
kind: string;
|
|
@@ -151,10 +149,7 @@ async function api(
|
|
|
151
149
|
if (!res.ok) {
|
|
152
150
|
if (options?.allowErrorJson) return json;
|
|
153
151
|
const err = json as Record<string, unknown>;
|
|
154
|
-
die(
|
|
155
|
-
err.error ? String(err.error) : `HTTP ${res.status}`,
|
|
156
|
-
typeof err.hint === 'string' ? err.hint : undefined,
|
|
157
|
-
);
|
|
152
|
+
die(err.error ? String(err.error) : `HTTP ${res.status}`, typeof err.hint === 'string' ? err.hint : undefined);
|
|
158
153
|
}
|
|
159
154
|
return json;
|
|
160
155
|
}
|
|
@@ -186,10 +181,38 @@ function parseFlags(args: string[]): { positional: string[]; flags: Record<strin
|
|
|
186
181
|
const flags: Record<string, string | true> = {};
|
|
187
182
|
// Boolean-only flags (never take a value argument)
|
|
188
183
|
const BOOL_FLAGS = new Set([
|
|
189
|
-
'help',
|
|
190
|
-
'
|
|
191
|
-
'
|
|
192
|
-
'
|
|
184
|
+
'help',
|
|
185
|
+
'h',
|
|
186
|
+
'ids',
|
|
187
|
+
'stdin',
|
|
188
|
+
'yes',
|
|
189
|
+
'list',
|
|
190
|
+
'clear',
|
|
191
|
+
'set',
|
|
192
|
+
'animated',
|
|
193
|
+
'dry-run',
|
|
194
|
+
'all',
|
|
195
|
+
'no-open-in-canvas',
|
|
196
|
+
'lock-arrange',
|
|
197
|
+
'unlock-arrange',
|
|
198
|
+
'json',
|
|
199
|
+
'compact',
|
|
200
|
+
'verbose',
|
|
201
|
+
'include-logs',
|
|
202
|
+
'no-pan',
|
|
203
|
+
'schema',
|
|
204
|
+
'example',
|
|
205
|
+
'examples',
|
|
206
|
+
'strict-size',
|
|
207
|
+
'scroll-overflow',
|
|
208
|
+
'report',
|
|
209
|
+
'canvas',
|
|
210
|
+
'hooks',
|
|
211
|
+
'tools',
|
|
212
|
+
'session-messaging',
|
|
213
|
+
'permissions',
|
|
214
|
+
'files',
|
|
215
|
+
'ui-prompts',
|
|
193
216
|
]);
|
|
194
217
|
for (let i = 0; i < args.length; i++) {
|
|
195
218
|
const arg = args[i];
|
|
@@ -228,10 +251,7 @@ function requireFlag(flags: Record<string, string | true>, name: string, hint: s
|
|
|
228
251
|
return val;
|
|
229
252
|
}
|
|
230
253
|
|
|
231
|
-
function getStringFlag(
|
|
232
|
-
flags: Record<string, string | true>,
|
|
233
|
-
...names: string[]
|
|
234
|
-
): string | undefined {
|
|
254
|
+
function getStringFlag(flags: Record<string, string | true>, ...names: string[]): string | undefined {
|
|
235
255
|
for (const name of names) {
|
|
236
256
|
const value = flags[name];
|
|
237
257
|
if (typeof value === 'string' && value.length > 0) return value;
|
|
@@ -269,7 +289,11 @@ function optionalFiniteFlag(flags: Record<string, string | true>, name: string,
|
|
|
269
289
|
return parsed;
|
|
270
290
|
}
|
|
271
291
|
|
|
272
|
-
function optionalPositiveFiniteFlag(
|
|
292
|
+
function optionalPositiveFiniteFlag(
|
|
293
|
+
flags: Record<string, string | true>,
|
|
294
|
+
name: string,
|
|
295
|
+
hint: string,
|
|
296
|
+
): number | undefined {
|
|
273
297
|
const parsed = optionalFiniteFlag(flags, name, hint);
|
|
274
298
|
if (parsed === undefined) return undefined;
|
|
275
299
|
if (parsed <= 0) {
|
|
@@ -411,14 +435,14 @@ function collectFlagValues(args: string[], name: string): string[] {
|
|
|
411
435
|
return values;
|
|
412
436
|
}
|
|
413
437
|
|
|
414
|
-
function collectRequestedFields(
|
|
415
|
-
args: string[],
|
|
416
|
-
flags: Record<string, string | true>,
|
|
417
|
-
): string[] {
|
|
438
|
+
function collectRequestedFields(args: string[], flags: Record<string, string | true>): string[] {
|
|
418
439
|
const requested = [
|
|
419
440
|
...collectFlagValues(args, 'field'),
|
|
420
|
-
...(
|
|
421
|
-
? flags.fields
|
|
441
|
+
...(typeof flags.fields === 'string'
|
|
442
|
+
? flags.fields
|
|
443
|
+
.split(',')
|
|
444
|
+
.map((value) => value.trim())
|
|
445
|
+
.filter(Boolean)
|
|
422
446
|
: []),
|
|
423
447
|
];
|
|
424
448
|
return Array.from(new Set(requested));
|
|
@@ -453,9 +477,7 @@ function listAvailableNodeFields(node: Record<string, unknown>): string[] {
|
|
|
453
477
|
}
|
|
454
478
|
|
|
455
479
|
function summarizeHistoryResult(result: Record<string, unknown>): Record<string, unknown> {
|
|
456
|
-
const entries = Array.isArray(result.entries)
|
|
457
|
-
? result.entries.filter(isRecord)
|
|
458
|
-
: [];
|
|
480
|
+
const entries = Array.isArray(result.entries) ? result.entries.filter(isRecord) : [];
|
|
459
481
|
const countsByOperation: Record<string, number> = {};
|
|
460
482
|
let currentIndex = 0;
|
|
461
483
|
|
|
@@ -483,9 +505,7 @@ function summarizeHistoryResult(result: Record<string, unknown>): Record<string,
|
|
|
483
505
|
}
|
|
484
506
|
|
|
485
507
|
function compactHistoryResult(result: Record<string, unknown>): Record<string, unknown> {
|
|
486
|
-
const entries = Array.isArray(result.entries)
|
|
487
|
-
? result.entries.filter(isRecord)
|
|
488
|
-
: [];
|
|
508
|
+
const entries = Array.isArray(result.entries) ? result.entries.filter(isRecord) : [];
|
|
489
509
|
return {
|
|
490
510
|
totalMutations: entries.length,
|
|
491
511
|
canUndo: result.canUndo === true,
|
|
@@ -504,10 +524,7 @@ function parseRecordArrayJson(raw: string, hint: string): Array<Record<string, u
|
|
|
504
524
|
try {
|
|
505
525
|
parsed = JSON.parse(raw);
|
|
506
526
|
} catch (error) {
|
|
507
|
-
die(
|
|
508
|
-
`Invalid JSON dataset: ${error instanceof Error ? error.message : String(error)}`,
|
|
509
|
-
hint,
|
|
510
|
-
);
|
|
527
|
+
die(`Invalid JSON dataset: ${error instanceof Error ? error.message : String(error)}`, hint);
|
|
511
528
|
}
|
|
512
529
|
|
|
513
530
|
if (!Array.isArray(parsed) || parsed.some((item) => !isRecord(item))) {
|
|
@@ -521,10 +538,7 @@ function parseJsonValue(raw: string, label: string, hint: string): unknown {
|
|
|
521
538
|
try {
|
|
522
539
|
return JSON.parse(raw);
|
|
523
540
|
} catch (error) {
|
|
524
|
-
die(
|
|
525
|
-
`Invalid ${label}: ${error instanceof Error ? error.message : String(error)}`,
|
|
526
|
-
hint,
|
|
527
|
-
);
|
|
541
|
+
die(`Invalid ${label}: ${error instanceof Error ? error.message : String(error)}`, hint);
|
|
528
542
|
}
|
|
529
543
|
}
|
|
530
544
|
|
|
@@ -553,10 +567,7 @@ async function readTextInput(
|
|
|
553
567
|
try {
|
|
554
568
|
return readFileSync(path, 'utf-8');
|
|
555
569
|
} catch (error) {
|
|
556
|
-
die(
|
|
557
|
-
`Unable to read --${name}: ${error instanceof Error ? error.message : String(error)}`,
|
|
558
|
-
options.hint,
|
|
559
|
-
);
|
|
570
|
+
die(`Unable to read --${name}: ${error instanceof Error ? error.message : String(error)}`, options.hint);
|
|
560
571
|
}
|
|
561
572
|
}
|
|
562
573
|
|
|
@@ -588,10 +599,7 @@ async function readOptionalTextInput(
|
|
|
588
599
|
try {
|
|
589
600
|
return readFileSync(path, 'utf-8');
|
|
590
601
|
} catch (error) {
|
|
591
|
-
die(
|
|
592
|
-
`Unable to read --${name}: ${error instanceof Error ? error.message : String(error)}`,
|
|
593
|
-
options.hint,
|
|
594
|
-
);
|
|
602
|
+
die(`Unable to read --${name}: ${error instanceof Error ? error.message : String(error)}`, options.hint);
|
|
595
603
|
}
|
|
596
604
|
}
|
|
597
605
|
|
|
@@ -634,7 +642,11 @@ async function applyStructuredNodeUpdateFlags(
|
|
|
634
642
|
hint: 'Use: pmx-canvas node update <node-id> --spec-file ./new-spec.json',
|
|
635
643
|
});
|
|
636
644
|
if (specRaw !== undefined) {
|
|
637
|
-
body.spec = parseJsonValue(
|
|
645
|
+
body.spec = parseJsonValue(
|
|
646
|
+
specRaw,
|
|
647
|
+
'JSON spec',
|
|
648
|
+
'Use: pmx-canvas node update <node-id> --spec-file ./new-spec.json',
|
|
649
|
+
);
|
|
638
650
|
}
|
|
639
651
|
|
|
640
652
|
const graphPatch = await buildGraphRequestBody(flags, { requireData: false, allowStdin: false });
|
|
@@ -643,11 +655,8 @@ async function applyStructuredNodeUpdateFlags(
|
|
|
643
655
|
}
|
|
644
656
|
}
|
|
645
657
|
|
|
646
|
-
async function buildJsonRenderRequestBody(
|
|
647
|
-
|
|
648
|
-
): Promise<Record<string, unknown>> {
|
|
649
|
-
const hint =
|
|
650
|
-
'Use: pmx-canvas node add --type json-render --spec-file ./dashboard.json --title "Ops Dashboard"';
|
|
658
|
+
async function buildJsonRenderRequestBody(flags: Record<string, string | true>): Promise<Record<string, unknown>> {
|
|
659
|
+
const hint = 'Use: pmx-canvas node add --type json-render --spec-file ./dashboard.json --title "Ops Dashboard"';
|
|
651
660
|
const title = typeof flags.title === 'string' ? flags.title.trim() : '';
|
|
652
661
|
|
|
653
662
|
const rawSpec = await readTextInput(flags, {
|
|
@@ -671,9 +680,7 @@ async function buildJsonRenderRequestBody(
|
|
|
671
680
|
return body;
|
|
672
681
|
}
|
|
673
682
|
|
|
674
|
-
async function buildHtmlPrimitiveRequestBody(
|
|
675
|
-
flags: Record<string, string | true>,
|
|
676
|
-
): Promise<Record<string, unknown>> {
|
|
683
|
+
async function buildHtmlPrimitiveRequestBody(flags: Record<string, string | true>): Promise<Record<string, unknown>> {
|
|
677
684
|
const hint = 'Use: pmx-canvas html primitive add --kind choice-grid --data-file ./primitive.json --title "Options"';
|
|
678
685
|
const kind = getStringFlag(flags, 'kind', 'primitive');
|
|
679
686
|
if (!kind) die('HTML primitives require --kind.', hint);
|
|
@@ -760,7 +767,11 @@ async function buildGraphRequestBody(
|
|
|
760
767
|
if (showLegend !== undefined) body.showLegend = showLegend;
|
|
761
768
|
if (showLabels !== undefined) body.showLabels = showLabels;
|
|
762
769
|
|
|
763
|
-
const chartHeight = optionalPositiveFiniteFlag(
|
|
770
|
+
const chartHeight = optionalPositiveFiniteFlag(
|
|
771
|
+
flags,
|
|
772
|
+
'chart-height',
|
|
773
|
+
'Use a positive number, e.g. --chart-height 300',
|
|
774
|
+
);
|
|
764
775
|
const x = optionalFiniteFlag(flags, 'x', 'Use a finite number, e.g. --x 500');
|
|
765
776
|
const y = optionalFiniteFlag(flags, 'y', 'Use a finite number, e.g. --y 300');
|
|
766
777
|
const width = optionalPositiveFiniteFlag(flags, 'width', 'Use a positive number, e.g. --width 760');
|
|
@@ -780,9 +791,7 @@ async function buildGraphRequestBody(
|
|
|
780
791
|
return body;
|
|
781
792
|
}
|
|
782
793
|
|
|
783
|
-
async function buildWebArtifactRequestBody(
|
|
784
|
-
flags: Record<string, string | true>,
|
|
785
|
-
): Promise<Record<string, unknown>> {
|
|
794
|
+
async function buildWebArtifactRequestBody(flags: Record<string, string | true>): Promise<Record<string, unknown>> {
|
|
786
795
|
const hint = 'Use: pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx';
|
|
787
796
|
const title = requireFlag(flags, 'title', hint);
|
|
788
797
|
const appTsx = await readTextInput(flags, {
|
|
@@ -904,9 +913,7 @@ function printNodeSchemaHelp(schema: CanvasSchemaType): void {
|
|
|
904
913
|
console.log(`Endpoint: ${schema.endpoint}`);
|
|
905
914
|
console.log('Flags:');
|
|
906
915
|
for (const field of schema.fields) {
|
|
907
|
-
const aliases = field.aliases?.length
|
|
908
|
-
? ` (aliases: ${field.aliases.map((alias) => `--${alias}`).join(', ')})`
|
|
909
|
-
: '';
|
|
916
|
+
const aliases = field.aliases?.length ? ` (aliases: ${field.aliases.map((alias) => `--${alias}`).join(', ')})` : '';
|
|
910
917
|
console.log(
|
|
911
918
|
` --${field.name}${field.required ? ' [required]' : ''} <${field.type}> ${field.description}${aliases}`,
|
|
912
919
|
);
|
|
@@ -936,13 +943,19 @@ async function showNodeAddTypeHelp(flags: Record<string, string | true>): Promis
|
|
|
936
943
|
if (componentName) {
|
|
937
944
|
const component = schema.jsonRender.components.find((entry) => entry.type === componentName);
|
|
938
945
|
if (!component) {
|
|
939
|
-
die(
|
|
946
|
+
die(
|
|
947
|
+
`Unknown json-render component: ${componentName}`,
|
|
948
|
+
'Run: pmx-canvas node schema --type json-render --summary',
|
|
949
|
+
);
|
|
940
950
|
}
|
|
941
951
|
const requestedField = getStringFlag(flags, 'field');
|
|
942
952
|
if (requestedField) {
|
|
943
953
|
const prop = component.props.find((entry) => entry.name === requestedField);
|
|
944
954
|
if (!prop) {
|
|
945
|
-
die(
|
|
955
|
+
die(
|
|
956
|
+
`Unknown json-render prop: ${requestedField}`,
|
|
957
|
+
`Run: pmx-canvas node schema --type json-render --component ${componentName}`,
|
|
958
|
+
);
|
|
946
959
|
}
|
|
947
960
|
payload = {
|
|
948
961
|
command: 'node add',
|
|
@@ -1061,7 +1074,10 @@ function filterJsonRenderSchemaView(
|
|
|
1061
1074
|
if (requestedField) {
|
|
1062
1075
|
const prop = component.props.find((entry) => entry.name === requestedField);
|
|
1063
1076
|
if (!prop) {
|
|
1064
|
-
die(
|
|
1077
|
+
die(
|
|
1078
|
+
`Unknown json-render prop: ${requestedField}`,
|
|
1079
|
+
`Run: pmx-canvas node schema --type json-render --component ${componentName}`,
|
|
1080
|
+
);
|
|
1065
1081
|
}
|
|
1066
1082
|
return {
|
|
1067
1083
|
component: componentName,
|
|
@@ -1072,7 +1088,9 @@ function filterJsonRenderSchemaView(
|
|
|
1072
1088
|
return flags.summary ? summarizeJsonRenderComponent(component) : component;
|
|
1073
1089
|
}
|
|
1074
1090
|
|
|
1075
|
-
function summarizeHtmlPrimitive(
|
|
1091
|
+
function summarizeHtmlPrimitive(
|
|
1092
|
+
primitive: NonNullable<CanvasSchemaResponse['htmlPrimitives']>[number],
|
|
1093
|
+
): Record<string, unknown> {
|
|
1076
1094
|
return {
|
|
1077
1095
|
kind: primitive.kind,
|
|
1078
1096
|
title: primitive.title,
|
|
@@ -1138,18 +1156,11 @@ const RESOURCE_SUBCOMMAND_HINTS: Record<string, Record<string, string>> = {
|
|
|
1138
1156
|
},
|
|
1139
1157
|
};
|
|
1140
1158
|
|
|
1141
|
-
function cmd(
|
|
1142
|
-
name: string,
|
|
1143
|
-
help: string,
|
|
1144
|
-
examples: string[],
|
|
1145
|
-
run: (args: string[]) => Promise<void>,
|
|
1146
|
-
) {
|
|
1159
|
+
function cmd(name: string, help: string, examples: string[], run: (args: string[]) => Promise<void>) {
|
|
1147
1160
|
COMMANDS[name] = { run, help, examples };
|
|
1148
1161
|
}
|
|
1149
1162
|
|
|
1150
|
-
cmd('open', 'Open the current workbench in the browser', [
|
|
1151
|
-
'pmx-canvas open',
|
|
1152
|
-
], async (args) => {
|
|
1163
|
+
cmd('open', 'Open the current workbench in the browser', ['pmx-canvas open'], async (args) => {
|
|
1153
1164
|
const { flags } = parseFlags(args);
|
|
1154
1165
|
if (flags.help || flags.h) return showCommandHelp('open');
|
|
1155
1166
|
|
|
@@ -1174,475 +1185,538 @@ cmd('open', 'Open the current workbench in the browser', [
|
|
|
1174
1185
|
});
|
|
1175
1186
|
|
|
1176
1187
|
// ── node add ─────────────────────────────────────────────────
|
|
1177
|
-
cmd(
|
|
1178
|
-
'
|
|
1179
|
-
'
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
return;
|
|
1205
|
-
}
|
|
1188
|
+
cmd(
|
|
1189
|
+
'node add',
|
|
1190
|
+
'Add a node to the canvas',
|
|
1191
|
+
[
|
|
1192
|
+
'pmx-canvas node add --type markdown --title "Design Doc" --content "# Overview"',
|
|
1193
|
+
'pmx-canvas node add --type status --title "Build" --content "passing"',
|
|
1194
|
+
'pmx-canvas node add --type file --content "src/index.ts"',
|
|
1195
|
+
'pmx-canvas node add --type webpage --url "https://example.com/docs"',
|
|
1196
|
+
'pmx-canvas node add --type html --title "Widget" --content "<main>Hello</main>"',
|
|
1197
|
+
'pmx-canvas node add --type html --title "Showcase" --content ./report.html (a .html path is read from disk; otherwise --content is raw HTML)',
|
|
1198
|
+
'pmx-canvas node add --type html --primitive choice-grid --data-file ./options.json --title "Options"',
|
|
1199
|
+
'pmx-canvas node add --type markdown --title "Note" --x 100 --y 200',
|
|
1200
|
+
'pmx-canvas node add --type json-render --title "Ops Dashboard" --spec-file ./dashboard.json',
|
|
1201
|
+
'pmx-canvas node add --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
|
|
1202
|
+
'pmx-canvas node add --type web-artifact --title "Dashboard" --app-file ./App.tsx',
|
|
1203
|
+
],
|
|
1204
|
+
async (args) => {
|
|
1205
|
+
const { flags } = parseFlags(args);
|
|
1206
|
+
if (flags.help || flags.h) return showNodeAddTypeHelp(flags);
|
|
1207
|
+
|
|
1208
|
+
const type = (flags.type as string) || 'markdown';
|
|
1209
|
+
|
|
1210
|
+
if (type === 'json-render') {
|
|
1211
|
+
const result = await invokeOperation('jsonrender.add', await buildJsonRenderRequestBody(flags));
|
|
1212
|
+
output(result);
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1206
1215
|
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1216
|
+
if (type === 'graph') {
|
|
1217
|
+
const result = await invokeOperation('graph.add', await buildGraphRequestBody(flags));
|
|
1218
|
+
output(result);
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1211
1221
|
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
}
|
|
1222
|
+
if (type === 'web-artifact') {
|
|
1223
|
+
await runWebArtifactBuildCommand(flags);
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1217
1226
|
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1227
|
+
if (type === 'html-primitive') {
|
|
1228
|
+
const result = await invokeOperation('node.add', await buildHtmlPrimitiveRequestBody(flags));
|
|
1229
|
+
output(result);
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1223
1232
|
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
}
|
|
1233
|
+
if (type === 'html' && getStringFlag(flags, 'primitive', 'kind')) {
|
|
1234
|
+
const result = await invokeOperation('node.add', await buildHtmlPrimitiveRequestBody(flags));
|
|
1235
|
+
output(result);
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1230
1238
|
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
body.url = webpageUrl;
|
|
1237
|
-
} else if (type === 'image' && imagePath && !flags.content) {
|
|
1238
|
-
body.content = imagePath;
|
|
1239
|
-
} else if (type === 'html') {
|
|
1240
|
-
const html = getStringFlag(flags, 'html') ?? getStringFlag(flags, 'content');
|
|
1241
|
-
if (html !== undefined) body.html = html;
|
|
1242
|
-
const summary = getStringFlag(flags, 'summary');
|
|
1243
|
-
const agentSummary = getStringFlag(flags, 'agent-summary', 'agentSummary');
|
|
1244
|
-
const description = getStringFlag(flags, 'description');
|
|
1245
|
-
if (summary !== undefined) body.summary = summary;
|
|
1246
|
-
if (agentSummary !== undefined) body.agentSummary = agentSummary;
|
|
1247
|
-
if (description !== undefined) body.description = description;
|
|
1248
|
-
if (optionalBooleanFlag(flags, 'presentation', 'Use --presentation true or --presentation false') === true) body.presentation = true;
|
|
1249
|
-
if (typeof flags['slide-title'] === 'string') body.slideTitles = [flags['slide-title']];
|
|
1250
|
-
if (typeof flags['embedded-node-id'] === 'string') body.embeddedNodeIds = [flags['embedded-node-id']];
|
|
1251
|
-
} else if (flags.content) {
|
|
1252
|
-
body.content = flags.content;
|
|
1253
|
-
}
|
|
1254
|
-
applyCommonGeometryFlags(body, flags, {
|
|
1255
|
-
x: 'Use a finite number, e.g. --x 500',
|
|
1256
|
-
y: 'Use a finite number, e.g. --y 300',
|
|
1257
|
-
width: 'Use a positive number, e.g. --width 500',
|
|
1258
|
-
height: 'Use a positive number, e.g. --height 280',
|
|
1259
|
-
});
|
|
1260
|
-
applyStrictSizeFlags(body, flags);
|
|
1261
|
-
if (type === 'trace') {
|
|
1262
|
-
for (const field of TRACE_NODE_FIELDS) {
|
|
1263
|
-
const value = getStringFlag(flags, field, field.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`));
|
|
1264
|
-
if (value !== undefined) body[field] = value;
|
|
1239
|
+
if (type === 'mcp-app') {
|
|
1240
|
+
die(
|
|
1241
|
+
'mcp-app nodes require tool-backed app metadata and cannot be created with generic node add.',
|
|
1242
|
+
'Use: pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx, or pmx-canvas external-app add --kind excalidraw --title "Diagram"',
|
|
1243
|
+
);
|
|
1265
1244
|
}
|
|
1266
|
-
}
|
|
1267
1245
|
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1246
|
+
const body: Record<string, unknown> = { type };
|
|
1247
|
+
if (flags.title) body.title = flags.title;
|
|
1248
|
+
const webpageUrl = getStringFlag(flags, 'url');
|
|
1249
|
+
const imagePath = getStringFlag(flags, 'path');
|
|
1250
|
+
if (type === 'webpage' && webpageUrl) {
|
|
1251
|
+
body.url = webpageUrl;
|
|
1252
|
+
} else if (type === 'image' && imagePath && !flags.content) {
|
|
1253
|
+
body.content = imagePath;
|
|
1272
1254
|
} else if (type === 'html') {
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1255
|
+
const html = getStringFlag(flags, 'html') ?? getStringFlag(flags, 'content');
|
|
1256
|
+
if (html !== undefined) body.html = html;
|
|
1257
|
+
const summary = getStringFlag(flags, 'summary');
|
|
1258
|
+
const agentSummary = getStringFlag(flags, 'agent-summary', 'agentSummary');
|
|
1259
|
+
const description = getStringFlag(flags, 'description');
|
|
1260
|
+
if (summary !== undefined) body.summary = summary;
|
|
1261
|
+
if (agentSummary !== undefined) body.agentSummary = agentSummary;
|
|
1262
|
+
if (description !== undefined) body.description = description;
|
|
1263
|
+
if (optionalBooleanFlag(flags, 'presentation', 'Use --presentation true or --presentation false') === true)
|
|
1264
|
+
body.presentation = true;
|
|
1265
|
+
if (typeof flags['slide-title'] === 'string') body.slideTitles = [flags['slide-title']];
|
|
1266
|
+
if (typeof flags['embedded-node-id'] === 'string') body.embeddedNodeIds = [flags['embedded-node-id']];
|
|
1267
|
+
} else if (flags.content) {
|
|
1268
|
+
body.content = flags.content;
|
|
1269
|
+
}
|
|
1270
|
+
applyCommonGeometryFlags(body, flags, {
|
|
1271
|
+
x: 'Use a finite number, e.g. --x 500',
|
|
1272
|
+
y: 'Use a finite number, e.g. --y 300',
|
|
1273
|
+
width: 'Use a positive number, e.g. --width 500',
|
|
1274
|
+
height: 'Use a positive number, e.g. --height 280',
|
|
1275
|
+
});
|
|
1276
|
+
applyStrictSizeFlags(body, flags);
|
|
1277
|
+
if (type === 'trace') {
|
|
1278
|
+
for (const field of TRACE_NODE_FIELDS) {
|
|
1279
|
+
const value = getStringFlag(
|
|
1280
|
+
flags,
|
|
1281
|
+
field,
|
|
1282
|
+
field.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`),
|
|
1283
|
+
);
|
|
1284
|
+
if (value !== undefined) body[field] = value;
|
|
1285
|
+
}
|
|
1276
1286
|
}
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
const result = await invokeOperation('node.add', body);
|
|
1280
|
-
output(result);
|
|
1281
|
-
});
|
|
1282
|
-
|
|
1283
|
-
cmd('json-render', 'Show json-render schema and canonical examples', [
|
|
1284
|
-
'pmx-canvas json-render --schema --summary',
|
|
1285
|
-
'pmx-canvas json-render --examples',
|
|
1286
|
-
'pmx-canvas json-render --example --component Table',
|
|
1287
|
-
'pmx-canvas json-render --schema --component Badge --field variant',
|
|
1288
|
-
], async (args) => {
|
|
1289
|
-
const { flags } = parseFlags(args);
|
|
1290
|
-
if (flags.help || flags.h) return showCommandHelp('json-render');
|
|
1291
1287
|
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1288
|
+
// Support --stdin for piping content
|
|
1289
|
+
if (flags.stdin) {
|
|
1290
|
+
if (type === 'webpage') {
|
|
1291
|
+
body.url = await readStdin();
|
|
1292
|
+
} else if (type === 'html') {
|
|
1293
|
+
body.html = await readStdin();
|
|
1294
|
+
} else {
|
|
1295
|
+
body.content = await readStdin();
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1295
1298
|
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1299
|
+
const result = await invokeOperation('node.add', body);
|
|
1300
|
+
output(result);
|
|
1301
|
+
},
|
|
1302
|
+
);
|
|
1303
|
+
|
|
1304
|
+
cmd(
|
|
1305
|
+
'json-render',
|
|
1306
|
+
'Show json-render schema and canonical examples',
|
|
1307
|
+
[
|
|
1308
|
+
'pmx-canvas json-render --schema --summary',
|
|
1309
|
+
'pmx-canvas json-render --examples',
|
|
1310
|
+
'pmx-canvas json-render --example --component Table',
|
|
1311
|
+
'pmx-canvas json-render --schema --component Badge --field variant',
|
|
1312
|
+
],
|
|
1313
|
+
async (args) => {
|
|
1314
|
+
const { flags } = parseFlags(args);
|
|
1315
|
+
if (flags.help || flags.h) return showCommandHelp('json-render');
|
|
1316
|
+
|
|
1317
|
+
const schema = await loadCanvasSchema();
|
|
1318
|
+
const componentName = getStringFlag(flags, 'component');
|
|
1319
|
+
const fieldName = getStringFlag(flags, 'field');
|
|
1320
|
+
|
|
1321
|
+
if (flags.example || flags.examples) {
|
|
1322
|
+
if (fieldName)
|
|
1323
|
+
die(
|
|
1324
|
+
'--field is only supported with --schema.',
|
|
1325
|
+
'Use: pmx-canvas json-render --schema --component Table --field rows',
|
|
1326
|
+
);
|
|
1327
|
+
if (componentName) {
|
|
1328
|
+
const component = schema.jsonRender.components.find((entry) => entry.type === componentName);
|
|
1329
|
+
if (!component)
|
|
1330
|
+
die(`Unknown json-render component: ${componentName}`, 'Run: pmx-canvas json-render --schema --summary');
|
|
1331
|
+
output({ component: component.type, example: component.example });
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
output({
|
|
1335
|
+
rootShape: schema.jsonRender.rootShape,
|
|
1336
|
+
examples: Object.fromEntries(schema.jsonRender.components.map((entry) => [entry.type, entry.example])),
|
|
1337
|
+
});
|
|
1302
1338
|
return;
|
|
1303
1339
|
}
|
|
1304
|
-
output({
|
|
1305
|
-
rootShape: schema.jsonRender.rootShape,
|
|
1306
|
-
examples: Object.fromEntries(schema.jsonRender.components.map((entry) => [entry.type, entry.example])),
|
|
1307
|
-
});
|
|
1308
|
-
return;
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
output(filterJsonRenderSchemaView(schema.jsonRender, flags));
|
|
1312
|
-
});
|
|
1313
|
-
|
|
1314
|
-
cmd('html primitive add', 'Create a reusable sandboxed HTML communication primitive', [
|
|
1315
|
-
'pmx-canvas html primitive add --kind choice-grid --data-file ./options.json --title "Options"',
|
|
1316
|
-
'pmx-canvas html primitive add --kind plan-timeline --data-json \'{"milestones":[{"title":"Ship","detail":"Implement and verify","status":"next"}]}\'',
|
|
1317
|
-
'pmx-canvas html primitive add --kind triage-board --data-file ./tickets.json --strict-size',
|
|
1318
|
-
], async (args) => {
|
|
1319
|
-
const { flags } = parseFlags(args);
|
|
1320
|
-
if (flags.help || flags.h) return showCommandHelp('html primitive add');
|
|
1321
|
-
const result = await api('POST', '/api/canvas/node', await buildHtmlPrimitiveRequestBody(flags));
|
|
1322
|
-
output(result);
|
|
1323
|
-
});
|
|
1324
|
-
|
|
1325
|
-
cmd('html primitive schema', 'Describe reusable HTML communication primitives', [
|
|
1326
|
-
'pmx-canvas html primitive schema --summary',
|
|
1327
|
-
'pmx-canvas html primitive schema --kind choice-grid',
|
|
1328
|
-
'pmx-canvas html primitive schema --kind triage-board --summary',
|
|
1329
|
-
], async (args) => {
|
|
1330
|
-
const { flags } = parseFlags(args);
|
|
1331
|
-
if (flags.help || flags.h) return showCommandHelp('html primitive schema');
|
|
1332
|
-
const schema = await loadCanvasSchema();
|
|
1333
|
-
output(filterHtmlPrimitiveSchemaView(schema, flags));
|
|
1334
|
-
});
|
|
1335
1340
|
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1341
|
+
output(filterJsonRenderSchemaView(schema.jsonRender, flags));
|
|
1342
|
+
},
|
|
1343
|
+
);
|
|
1344
|
+
|
|
1345
|
+
cmd(
|
|
1346
|
+
'html primitive add',
|
|
1347
|
+
'Create a reusable sandboxed HTML communication primitive',
|
|
1348
|
+
[
|
|
1349
|
+
'pmx-canvas html primitive add --kind choice-grid --data-file ./options.json --title "Options"',
|
|
1350
|
+
'pmx-canvas html primitive add --kind plan-timeline --data-json \'{"milestones":[{"title":"Ship","detail":"Implement and verify","status":"next"}]}\'',
|
|
1351
|
+
'pmx-canvas html primitive add --kind triage-board --data-file ./tickets.json --strict-size',
|
|
1352
|
+
],
|
|
1353
|
+
async (args) => {
|
|
1354
|
+
const { flags } = parseFlags(args);
|
|
1355
|
+
if (flags.help || flags.h) return showCommandHelp('html primitive add');
|
|
1356
|
+
const result = await api('POST', '/api/canvas/node', await buildHtmlPrimitiveRequestBody(flags));
|
|
1357
|
+
output(result);
|
|
1358
|
+
},
|
|
1359
|
+
);
|
|
1360
|
+
|
|
1361
|
+
cmd(
|
|
1362
|
+
'html primitive schema',
|
|
1363
|
+
'Describe reusable HTML communication primitives',
|
|
1364
|
+
[
|
|
1365
|
+
'pmx-canvas html primitive schema --summary',
|
|
1366
|
+
'pmx-canvas html primitive schema --kind choice-grid',
|
|
1367
|
+
'pmx-canvas html primitive schema --kind triage-board --summary',
|
|
1368
|
+
],
|
|
1369
|
+
async (args) => {
|
|
1370
|
+
const { flags } = parseFlags(args);
|
|
1371
|
+
if (flags.help || flags.h) return showCommandHelp('html primitive schema');
|
|
1372
|
+
const schema = await loadCanvasSchema();
|
|
1373
|
+
output(filterHtmlPrimitiveSchemaView(schema, flags));
|
|
1374
|
+
},
|
|
1375
|
+
);
|
|
1376
|
+
|
|
1377
|
+
cmd(
|
|
1378
|
+
'graph add',
|
|
1379
|
+
'Add a graph node to the canvas',
|
|
1380
|
+
[
|
|
1381
|
+
'pmx-canvas graph add --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
|
|
1382
|
+
'pmx-canvas graph add --graphType composed --data \'[{"day":"Mon","visits":10,"conversion":0.4}]\' --xKey day --barKey visits --lineKey conversion',
|
|
1383
|
+
'pmx-canvas node add --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
|
|
1384
|
+
],
|
|
1385
|
+
async (args) => {
|
|
1386
|
+
const { flags } = parseFlags(args);
|
|
1387
|
+
if (flags.help || flags.h) return showCommandHelp('graph add');
|
|
1359
1388
|
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1389
|
+
const result = await invokeOperation('graph.add', await buildGraphRequestBody(flags));
|
|
1390
|
+
output(result);
|
|
1391
|
+
},
|
|
1392
|
+
);
|
|
1393
|
+
|
|
1394
|
+
cmd(
|
|
1395
|
+
'node schema',
|
|
1396
|
+
'Describe server-supported node create schemas and canonical examples',
|
|
1397
|
+
[
|
|
1398
|
+
'pmx-canvas node schema',
|
|
1399
|
+
'pmx-canvas node schema --type webpage',
|
|
1400
|
+
'pmx-canvas node schema --type json-render',
|
|
1401
|
+
'pmx-canvas json-render --schema --summary',
|
|
1402
|
+
'pmx-canvas node schema --type json-render --component Table',
|
|
1403
|
+
'pmx-canvas node schema --type webpage --field url',
|
|
1404
|
+
'pmx-canvas node schema --summary',
|
|
1405
|
+
],
|
|
1406
|
+
async (args) => {
|
|
1407
|
+
const { flags } = parseFlags(args);
|
|
1408
|
+
if (flags.help || flags.h) return showCommandHelp('node schema');
|
|
1409
|
+
|
|
1410
|
+
const result = await loadCanvasSchema();
|
|
1411
|
+
if (getStringFlag(flags, 'component') && flags.type !== 'json-render') {
|
|
1412
|
+
die('--component is only supported with --type json-render.');
|
|
1413
|
+
}
|
|
1364
1414
|
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1415
|
+
if (typeof flags.type !== 'string') {
|
|
1416
|
+
if (flags.summary) {
|
|
1417
|
+
output({
|
|
1418
|
+
source: result.source,
|
|
1419
|
+
version: result.version,
|
|
1420
|
+
nodeTypes: result.nodeTypes.map((entry) => summarizeNodeSchema(entry)),
|
|
1421
|
+
jsonRender: {
|
|
1422
|
+
componentCount: result.jsonRender.components.length,
|
|
1423
|
+
rootShape: result.jsonRender.rootShape,
|
|
1424
|
+
},
|
|
1425
|
+
graph: result.graph,
|
|
1426
|
+
htmlPrimitives: result.htmlPrimitives?.map((entry) => summarizeHtmlPrimitive(entry)) ?? [],
|
|
1427
|
+
mcp: result.mcp,
|
|
1428
|
+
});
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
output(result);
|
|
1379
1432
|
return;
|
|
1380
1433
|
}
|
|
1381
|
-
output(result);
|
|
1382
|
-
return;
|
|
1383
|
-
}
|
|
1384
1434
|
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
return;
|
|
1389
|
-
}
|
|
1390
|
-
if (requested === 'graph') {
|
|
1391
|
-
const graphSchema = result.nodeTypes.find((entry) => entry.type === 'graph');
|
|
1392
|
-
if (graphSchema) {
|
|
1393
|
-
output(filterNodeSchemaView(graphSchema, flags));
|
|
1435
|
+
const requested = flags.type;
|
|
1436
|
+
if (requested === 'json-render') {
|
|
1437
|
+
output(filterJsonRenderSchemaView(result.jsonRender, flags));
|
|
1394
1438
|
return;
|
|
1395
1439
|
}
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1440
|
+
if (requested === 'graph') {
|
|
1441
|
+
const graphSchema = result.nodeTypes.find((entry) => entry.type === 'graph');
|
|
1442
|
+
if (graphSchema) {
|
|
1443
|
+
output(filterNodeSchemaView(graphSchema, flags));
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
output(flags.summary ? result.graph : { ...result.graph });
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
const nodeType = result.nodeTypes.find((entry) => entry.type === requested);
|
|
1450
|
+
if (nodeType) {
|
|
1451
|
+
output(filterNodeSchemaView(nodeType, flags));
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
die(`Unknown schema type: ${requested}`, 'Run: pmx-canvas node schema');
|
|
1455
|
+
},
|
|
1456
|
+
);
|
|
1406
1457
|
|
|
1407
1458
|
// ── node list ────────────────────────────────────────────────
|
|
1408
|
-
cmd(
|
|
1409
|
-
'
|
|
1410
|
-
'
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1459
|
+
cmd(
|
|
1460
|
+
'node list',
|
|
1461
|
+
'List all nodes on the canvas',
|
|
1462
|
+
[
|
|
1463
|
+
'pmx-canvas node list',
|
|
1464
|
+
'pmx-canvas node list --type markdown',
|
|
1465
|
+
'pmx-canvas node list --type mcp-app',
|
|
1466
|
+
'pmx-canvas node list --ids',
|
|
1467
|
+
],
|
|
1468
|
+
async (args) => {
|
|
1469
|
+
const { flags } = parseFlags(args);
|
|
1470
|
+
if (flags.help || flags.h) return showCommandHelp('node list');
|
|
1471
|
+
|
|
1472
|
+
const layout = (await invokeOperation('layout.get', {})) as { nodes: Array<Record<string, unknown>> };
|
|
1473
|
+
let nodes = layout.nodes;
|
|
1474
|
+
|
|
1475
|
+
if (flags.type && flags.type !== true) {
|
|
1476
|
+
nodes = nodes.filter((n) => n.type === flags.type || n.kind === flags.type);
|
|
1477
|
+
}
|
|
1423
1478
|
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
}
|
|
1433
|
-
});
|
|
1479
|
+
if (flags.ids) {
|
|
1480
|
+
output(nodes.map((n) => n.id));
|
|
1481
|
+
} else {
|
|
1482
|
+
const shouldSummarize = flags.summary === true || flags.compact === true || flags.type === 'mcp-app';
|
|
1483
|
+
output(shouldSummarize ? nodes.map((node) => summarizeNodeResult(node)) : nodes);
|
|
1484
|
+
}
|
|
1485
|
+
},
|
|
1486
|
+
);
|
|
1434
1487
|
|
|
1435
1488
|
// ── node get ─────────────────────────────────────────────────
|
|
1436
|
-
cmd(
|
|
1437
|
-
'
|
|
1438
|
-
'
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
const
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
)
|
|
1489
|
+
cmd(
|
|
1490
|
+
'node get',
|
|
1491
|
+
'Get a node by ID',
|
|
1492
|
+
[
|
|
1493
|
+
'pmx-canvas node get <node-id>',
|
|
1494
|
+
'pmx-canvas node get node-abc123',
|
|
1495
|
+
'pmx-canvas node get node-abc123 --summary',
|
|
1496
|
+
'pmx-canvas node get node-abc123 --field title --field graphConfig',
|
|
1497
|
+
],
|
|
1498
|
+
async (args) => {
|
|
1499
|
+
const { positional, flags } = parseFlags(args);
|
|
1500
|
+
if (flags.help || flags.h) return showCommandHelp('node get');
|
|
1501
|
+
|
|
1502
|
+
const id = positional[0];
|
|
1503
|
+
if (!id) die('Missing node ID', 'pmx-canvas node get <node-id>');
|
|
1504
|
+
|
|
1505
|
+
const result = (await invokeOperation('node.get', { id })) as Record<string, unknown>;
|
|
1506
|
+
const requestedFields = collectRequestedFields(args, flags);
|
|
1507
|
+
if (requestedFields.length > 0) {
|
|
1508
|
+
const picked = Object.fromEntries(requestedFields.map((field) => [field, resolveNodeFieldValue(result, field)]));
|
|
1509
|
+
const missing = requestedFields.filter((field) => picked[field] === undefined);
|
|
1510
|
+
if (missing.length > 0) {
|
|
1511
|
+
die(
|
|
1512
|
+
`Unknown node field${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}`,
|
|
1513
|
+
`Available fields: ${listAvailableNodeFields(result).join(', ')}`,
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
output({
|
|
1517
|
+
id: result.id ?? id,
|
|
1518
|
+
fields: picked,
|
|
1519
|
+
});
|
|
1520
|
+
return;
|
|
1458
1521
|
}
|
|
1459
|
-
output({
|
|
1460
|
-
id: result.id ?? id,
|
|
1461
|
-
fields: picked,
|
|
1462
|
-
});
|
|
1463
|
-
return;
|
|
1464
|
-
}
|
|
1465
1522
|
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
}
|
|
1523
|
+
if (flags.summary || flags.compact) {
|
|
1524
|
+
output(summarizeNodeResult(result));
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
output(result);
|
|
1528
|
+
},
|
|
1529
|
+
);
|
|
1472
1530
|
|
|
1473
1531
|
// ── node update ──────────────────────────────────────────────
|
|
1474
|
-
cmd(
|
|
1475
|
-
'
|
|
1476
|
-
'
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1532
|
+
cmd(
|
|
1533
|
+
'node update',
|
|
1534
|
+
'Update a node by ID',
|
|
1535
|
+
[
|
|
1536
|
+
'pmx-canvas node update <node-id> --title "New Title"',
|
|
1537
|
+
'pmx-canvas node update <node-id> --content "Updated content"',
|
|
1538
|
+
'pmx-canvas node update <node-id> --title "Moved" --x 500 --y 300',
|
|
1539
|
+
'pmx-canvas node update <node-id> --width 840 --height 620',
|
|
1540
|
+
'pmx-canvas node update <node-id> --spec-file ./dashboard.json',
|
|
1541
|
+
'pmx-canvas node update <graph-id> --data-file ./metrics.json --chart-height 420',
|
|
1542
|
+
'pmx-canvas node update <node-id> --pinned true',
|
|
1543
|
+
'pmx-canvas node update <node-id> --dock-position right',
|
|
1544
|
+
'pmx-canvas node update <node-id> --dock-position none # undock back to the canvas',
|
|
1545
|
+
'pmx-canvas node update <node-id> --lock-arrange',
|
|
1546
|
+
],
|
|
1547
|
+
async (args) => {
|
|
1548
|
+
const { positional, flags } = parseFlags(args);
|
|
1549
|
+
if (flags.help || flags.h) return showCommandHelp('node update');
|
|
1550
|
+
|
|
1551
|
+
const id = positional[0];
|
|
1552
|
+
if (!id) die('Missing node ID', 'pmx-canvas node update <node-id> --title "New Title"');
|
|
1553
|
+
|
|
1554
|
+
const body: Record<string, unknown> = {};
|
|
1555
|
+
await applyStructuredNodeUpdateFlags(body, flags);
|
|
1556
|
+
if (flags.title && flags.title !== true) body.title = flags.title;
|
|
1557
|
+
if (flags.content && flags.content !== true) body.content = flags.content;
|
|
1558
|
+
if (flags.stdin) body.content = await readStdin();
|
|
1559
|
+
|
|
1560
|
+
const x = optionalFiniteFlag(flags, 'x', 'Use a finite number, e.g. --x 500');
|
|
1561
|
+
const y = optionalFiniteFlag(flags, 'y', 'Use a finite number, e.g. --y 300');
|
|
1562
|
+
const width = optionalPositiveFiniteFlag(flags, 'width', 'Use a positive number, e.g. --width 840');
|
|
1563
|
+
const height = optionalPositiveFiniteFlag(flags, 'height', 'Use a positive number, e.g. --height 620');
|
|
1564
|
+
const nodeHeight = optionalPositiveFiniteFlagWithAliases(
|
|
1565
|
+
flags,
|
|
1566
|
+
'Use a positive number, e.g. --node-height 620',
|
|
1567
|
+
'node-height',
|
|
1568
|
+
'nodeHeight',
|
|
1569
|
+
);
|
|
1570
|
+
if (height !== undefined && nodeHeight !== undefined) {
|
|
1571
|
+
die('Use either --height/--node-height, not both.');
|
|
1572
|
+
}
|
|
1573
|
+
const frameHeight = height ?? nodeHeight;
|
|
1574
|
+
const pinned = optionalBooleanFlag(flags, 'pinned', 'Use --pinned true or --pinned false');
|
|
1575
|
+
if (flags['lock-arrange'] && flags['unlock-arrange']) {
|
|
1576
|
+
die('Use either --lock-arrange or --unlock-arrange, not both.');
|
|
1577
|
+
}
|
|
1578
|
+
const arrangeLocked = flags['lock-arrange'] ? true : flags['unlock-arrange'] ? false : undefined;
|
|
1491
1579
|
|
|
1492
|
-
|
|
1493
|
-
await applyStructuredNodeUpdateFlags(body, flags);
|
|
1494
|
-
if (flags.title && flags.title !== true) body.title = flags.title;
|
|
1495
|
-
if (flags.content && flags.content !== true) body.content = flags.content;
|
|
1496
|
-
if (flags.stdin) body.content = await readStdin();
|
|
1580
|
+
applyStrictSizeFlags(body, flags);
|
|
1497
1581
|
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
'nodeHeight',
|
|
1507
|
-
);
|
|
1508
|
-
if (height !== undefined && nodeHeight !== undefined) {
|
|
1509
|
-
die('Use either --height/--node-height, not both.');
|
|
1510
|
-
}
|
|
1511
|
-
const frameHeight = height ?? nodeHeight;
|
|
1512
|
-
const pinned = optionalBooleanFlag(flags, 'pinned', 'Use --pinned true or --pinned false');
|
|
1513
|
-
if (flags['lock-arrange'] && flags['unlock-arrange']) {
|
|
1514
|
-
die('Use either --lock-arrange or --unlock-arrange, not both.');
|
|
1515
|
-
}
|
|
1516
|
-
const arrangeLocked = flags['lock-arrange']
|
|
1517
|
-
? true
|
|
1518
|
-
: flags['unlock-arrange']
|
|
1519
|
-
? false
|
|
1520
|
-
: undefined;
|
|
1582
|
+
for (const field of TRACE_NODE_FIELDS) {
|
|
1583
|
+
const value = getStringFlag(
|
|
1584
|
+
flags,
|
|
1585
|
+
field,
|
|
1586
|
+
field.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`),
|
|
1587
|
+
);
|
|
1588
|
+
if (value !== undefined) body[field] = value;
|
|
1589
|
+
}
|
|
1521
1590
|
|
|
1522
|
-
|
|
1591
|
+
if (
|
|
1592
|
+
x !== undefined ||
|
|
1593
|
+
y !== undefined ||
|
|
1594
|
+
width !== undefined ||
|
|
1595
|
+
frameHeight !== undefined ||
|
|
1596
|
+
arrangeLocked !== undefined
|
|
1597
|
+
) {
|
|
1598
|
+
const existing = (await invokeOperation('node.get', { id })) as {
|
|
1599
|
+
position: { x: number; y: number };
|
|
1600
|
+
size: { width: number; height: number };
|
|
1601
|
+
data: Record<string, unknown>;
|
|
1602
|
+
};
|
|
1523
1603
|
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1604
|
+
if (x !== undefined || y !== undefined) {
|
|
1605
|
+
body.position = {
|
|
1606
|
+
x: x ?? existing.position.x,
|
|
1607
|
+
y: y ?? existing.position.y,
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1528
1610
|
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1611
|
+
if (width !== undefined || frameHeight !== undefined) {
|
|
1612
|
+
body.size = {
|
|
1613
|
+
width: width ?? existing.size.width,
|
|
1614
|
+
height: frameHeight ?? existing.size.height,
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1535
1617
|
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
y: y ?? existing.position.y,
|
|
1540
|
-
};
|
|
1618
|
+
if (arrangeLocked !== undefined) {
|
|
1619
|
+
body.arrangeLocked = arrangeLocked;
|
|
1620
|
+
}
|
|
1541
1621
|
}
|
|
1542
1622
|
|
|
1543
|
-
if (
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1623
|
+
if (pinned !== undefined) body.pinned = pinned;
|
|
1624
|
+
|
|
1625
|
+
// --dock-position left|right|none : dock a node into the top HUD or undock it.
|
|
1626
|
+
// `none`/`null`/empty map to JS null (undock). Assigned with a !== undefined
|
|
1627
|
+
// guard so the null survives JSON.stringify to the server (which accepts a
|
|
1628
|
+
// top-level dockPosition: null). HTTP PATCH already supports this; this is the
|
|
1629
|
+
// CLI path the report (#40) found missing.
|
|
1630
|
+
const dockRaw = getStringFlag(flags, 'dock-position', 'dockPosition');
|
|
1631
|
+
let dockPosition: 'left' | 'right' | null | undefined;
|
|
1632
|
+
if (dockRaw !== undefined) {
|
|
1633
|
+
const v = dockRaw.trim().toLowerCase();
|
|
1634
|
+
if (v === 'left' || v === 'right') dockPosition = v;
|
|
1635
|
+
else if (v === 'none' || v === 'null' || v === '') dockPosition = null;
|
|
1636
|
+
else die(`Invalid --dock-position "${dockRaw}".`, 'Use left, right, or none (to undock).');
|
|
1548
1637
|
}
|
|
1638
|
+
if (dockPosition !== undefined) body.dockPosition = dockPosition;
|
|
1549
1639
|
|
|
1550
|
-
if (
|
|
1551
|
-
|
|
1640
|
+
if (Object.keys(body).length === 0) {
|
|
1641
|
+
die(
|
|
1642
|
+
'No updates specified',
|
|
1643
|
+
'Use --title, --content, --x, --y, --width, --height, --strict-size, --pinned, --dock-position, trace fields, --lock-arrange, --unlock-arrange, or --stdin',
|
|
1644
|
+
);
|
|
1552
1645
|
}
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
|
-
if (pinned !== undefined) body.pinned = pinned;
|
|
1556
|
-
|
|
1557
|
-
// --dock-position left|right|none : dock a node into the top HUD or undock it.
|
|
1558
|
-
// `none`/`null`/empty map to JS null (undock). Assigned with a !== undefined
|
|
1559
|
-
// guard so the null survives JSON.stringify to the server (which accepts a
|
|
1560
|
-
// top-level dockPosition: null). HTTP PATCH already supports this; this is the
|
|
1561
|
-
// CLI path the report (#40) found missing.
|
|
1562
|
-
const dockRaw = getStringFlag(flags, 'dock-position', 'dockPosition');
|
|
1563
|
-
let dockPosition: 'left' | 'right' | null | undefined;
|
|
1564
|
-
if (dockRaw !== undefined) {
|
|
1565
|
-
const v = dockRaw.trim().toLowerCase();
|
|
1566
|
-
if (v === 'left' || v === 'right') dockPosition = v;
|
|
1567
|
-
else if (v === 'none' || v === 'null' || v === '') dockPosition = null;
|
|
1568
|
-
else die(`Invalid --dock-position "${dockRaw}".`, 'Use left, right, or none (to undock).');
|
|
1569
|
-
}
|
|
1570
|
-
if (dockPosition !== undefined) body.dockPosition = dockPosition;
|
|
1571
|
-
|
|
1572
|
-
if (Object.keys(body).length === 0) {
|
|
1573
|
-
die(
|
|
1574
|
-
'No updates specified',
|
|
1575
|
-
'Use --title, --content, --x, --y, --width, --height, --strict-size, --pinned, --dock-position, trace fields, --lock-arrange, --unlock-arrange, or --stdin',
|
|
1576
|
-
);
|
|
1577
|
-
}
|
|
1578
1646
|
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
}
|
|
1647
|
+
const result = await invokeOperation('node.update', { id, ...body });
|
|
1648
|
+
output(result);
|
|
1649
|
+
},
|
|
1650
|
+
);
|
|
1582
1651
|
|
|
1583
1652
|
// ── node remove ──────────────────────────────────────────────
|
|
1584
|
-
cmd(
|
|
1585
|
-
'
|
|
1586
|
-
'
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1653
|
+
cmd(
|
|
1654
|
+
'node remove',
|
|
1655
|
+
'Remove a node from the canvas',
|
|
1656
|
+
['pmx-canvas node remove <node-id>', 'pmx-canvas node remove node-abc123'],
|
|
1657
|
+
async (args) => {
|
|
1658
|
+
const { positional, flags } = parseFlags(args);
|
|
1659
|
+
if (flags.help || flags.h) return showCommandHelp('node remove');
|
|
1660
|
+
|
|
1661
|
+
const id = positional[0];
|
|
1662
|
+
if (!id) die('Missing node ID', 'pmx-canvas node remove <node-id>');
|
|
1663
|
+
|
|
1664
|
+
const result = await invokeOperation('node.remove', { id });
|
|
1665
|
+
output(result);
|
|
1666
|
+
},
|
|
1667
|
+
);
|
|
1597
1668
|
|
|
1598
1669
|
// ── edge add ─────────────────────────────────────────────────
|
|
1599
|
-
cmd(
|
|
1600
|
-
'
|
|
1601
|
-
'
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
)
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
)
|
|
1625
|
-
|
|
1670
|
+
cmd(
|
|
1671
|
+
'edge add',
|
|
1672
|
+
'Add an edge between two nodes',
|
|
1673
|
+
[
|
|
1674
|
+
'pmx-canvas edge add --from <node-id> --to <node-id> --type flow',
|
|
1675
|
+
'pmx-canvas edge add --from-search "DVT O3 — GitOps" --to-search "deep work trend" --type relation',
|
|
1676
|
+
'pmx-canvas edge add --from n1 --to n2 --type depends-on --label "imports"',
|
|
1677
|
+
'pmx-canvas edge add --from n1 --to n2 --type references --style dashed --animated',
|
|
1678
|
+
],
|
|
1679
|
+
async (args) => {
|
|
1680
|
+
const { flags } = parseFlags(args);
|
|
1681
|
+
if (flags.help || flags.h) return showCommandHelp('edge add');
|
|
1682
|
+
|
|
1683
|
+
const type = (flags.type as string) || 'flow';
|
|
1684
|
+
const from = typeof flags.from === 'string' ? flags.from : undefined;
|
|
1685
|
+
const to = typeof flags.to === 'string' ? flags.to : undefined;
|
|
1686
|
+
const fromSearch = typeof flags['from-search'] === 'string' ? flags['from-search'] : undefined;
|
|
1687
|
+
const toSearch = typeof flags['to-search'] === 'string' ? flags['to-search'] : undefined;
|
|
1688
|
+
|
|
1689
|
+
if (!from && !fromSearch) {
|
|
1690
|
+
die(
|
|
1691
|
+
'Missing source selector',
|
|
1692
|
+
'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',
|
|
1693
|
+
);
|
|
1694
|
+
}
|
|
1695
|
+
if (!to && !toSearch) {
|
|
1696
|
+
die(
|
|
1697
|
+
'Missing target selector',
|
|
1698
|
+
'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',
|
|
1699
|
+
);
|
|
1700
|
+
}
|
|
1626
1701
|
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1702
|
+
const body: Record<string, unknown> = {
|
|
1703
|
+
type,
|
|
1704
|
+
...(from ? { from } : {}),
|
|
1705
|
+
...(to ? { to } : {}),
|
|
1706
|
+
...(fromSearch ? { fromSearch } : {}),
|
|
1707
|
+
...(toSearch ? { toSearch } : {}),
|
|
1708
|
+
};
|
|
1709
|
+
if (flags.label && flags.label !== true) body.label = flags.label;
|
|
1710
|
+
if (typeof flags.style === 'string') body.style = flags.style;
|
|
1711
|
+
if (flags.animated) body.animated = true;
|
|
1637
1712
|
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
}
|
|
1713
|
+
const result = await invokeOperation('edge.add', body);
|
|
1714
|
+
output(result);
|
|
1715
|
+
},
|
|
1716
|
+
);
|
|
1641
1717
|
|
|
1642
1718
|
// ── edge list ────────────────────────────────────────────────
|
|
1643
|
-
cmd('edge list', 'List all edges on the canvas', [
|
|
1644
|
-
'pmx-canvas edge list',
|
|
1645
|
-
], async (args) => {
|
|
1719
|
+
cmd('edge list', 'List all edges on the canvas', ['pmx-canvas edge list'], async (args) => {
|
|
1646
1720
|
const { flags } = parseFlags(args);
|
|
1647
1721
|
if (flags.help || flags.h) return showCommandHelp('edge list');
|
|
1648
1722
|
|
|
@@ -1651,9 +1725,7 @@ cmd('edge list', 'List all edges on the canvas', [
|
|
|
1651
1725
|
});
|
|
1652
1726
|
|
|
1653
1727
|
// ── edge remove ──────────────────────────────────────────────
|
|
1654
|
-
cmd('edge remove', 'Remove an edge by ID', [
|
|
1655
|
-
'pmx-canvas edge remove <edge-id>',
|
|
1656
|
-
], async (args) => {
|
|
1728
|
+
cmd('edge remove', 'Remove an edge by ID', ['pmx-canvas edge remove <edge-id>'], async (args) => {
|
|
1657
1729
|
const { positional, flags } = parseFlags(args);
|
|
1658
1730
|
if (flags.help || flags.h) return showCommandHelp('edge remove');
|
|
1659
1731
|
|
|
@@ -1665,40 +1737,42 @@ cmd('edge remove', 'Remove an edge by ID', [
|
|
|
1665
1737
|
});
|
|
1666
1738
|
|
|
1667
1739
|
// ── search ───────────────────────────────────────────────────
|
|
1668
|
-
cmd(
|
|
1669
|
-
'
|
|
1670
|
-
'
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1740
|
+
cmd(
|
|
1741
|
+
'search',
|
|
1742
|
+
'Search nodes by title or content',
|
|
1743
|
+
['pmx-canvas search "design doc"', 'pmx-canvas search --query "TODO"'],
|
|
1744
|
+
async (args) => {
|
|
1745
|
+
const { positional, flags } = parseFlags(args);
|
|
1746
|
+
if (flags.help || flags.h) return showCommandHelp('search');
|
|
1747
|
+
|
|
1748
|
+
const query = positional[0] || (typeof flags.query === 'string' ? flags.query : '');
|
|
1749
|
+
if (!query) die('Missing search query', 'pmx-canvas search "query"');
|
|
1750
|
+
|
|
1751
|
+
const result = await invokeOperation('search', { q: query });
|
|
1752
|
+
output(result);
|
|
1753
|
+
},
|
|
1754
|
+
);
|
|
1681
1755
|
|
|
1682
1756
|
// ── layout ───────────────────────────────────────────────────
|
|
1683
|
-
cmd(
|
|
1684
|
-
'
|
|
1685
|
-
'
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1757
|
+
cmd(
|
|
1758
|
+
'layout',
|
|
1759
|
+
'Get the full canvas layout (nodes, edges, viewport)',
|
|
1760
|
+
['pmx-canvas layout', 'pmx-canvas layout --summary'],
|
|
1761
|
+
async (args) => {
|
|
1762
|
+
const { flags } = parseFlags(args);
|
|
1763
|
+
if (flags.help || flags.h) return showCommandHelp('layout');
|
|
1764
|
+
|
|
1765
|
+
if (flags.summary || flags.compact) {
|
|
1766
|
+
output(await api('GET', '/api/canvas/summary'));
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
const result = await api('GET', '/api/canvas/state');
|
|
1770
|
+
output(result);
|
|
1771
|
+
},
|
|
1772
|
+
);
|
|
1697
1773
|
|
|
1698
1774
|
// ── status ───────────────────────────────────────────────────
|
|
1699
|
-
cmd('status', 'Quick canvas summary', [
|
|
1700
|
-
'pmx-canvas status',
|
|
1701
|
-
], async (args) => {
|
|
1775
|
+
cmd('status', 'Quick canvas summary', ['pmx-canvas status'], async (args) => {
|
|
1702
1776
|
const { flags } = parseFlags(args);
|
|
1703
1777
|
if (flags.help || flags.h) return showCommandHelp('status');
|
|
1704
1778
|
|
|
@@ -1712,11 +1786,12 @@ cmd('status', 'Quick canvas summary', [
|
|
|
1712
1786
|
const typeCounts: Record<string, number> = {};
|
|
1713
1787
|
for (const n of layout.nodes) {
|
|
1714
1788
|
const data = isRecord(n.data) ? n.data : {};
|
|
1715
|
-
const t =
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1789
|
+
const t =
|
|
1790
|
+
typeof n.kind === 'string'
|
|
1791
|
+
? n.kind
|
|
1792
|
+
: n.type === 'mcp-app' && data.hostMode === 'hosted' && typeof data.path === 'string'
|
|
1793
|
+
? 'web-artifact'
|
|
1794
|
+
: (n.type as string);
|
|
1720
1795
|
typeCounts[t] = (typeCounts[t] || 0) + 1;
|
|
1721
1796
|
}
|
|
1722
1797
|
|
|
@@ -1730,25 +1805,24 @@ cmd('status', 'Quick canvas summary', [
|
|
|
1730
1805
|
});
|
|
1731
1806
|
|
|
1732
1807
|
// ── arrange ──────────────────────────────────────────────────
|
|
1733
|
-
cmd(
|
|
1734
|
-
'
|
|
1735
|
-
'
|
|
1736
|
-
'pmx-canvas arrange --layout flow',
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
}
|
|
1808
|
+
cmd(
|
|
1809
|
+
'arrange',
|
|
1810
|
+
'Auto-arrange nodes on the canvas',
|
|
1811
|
+
['pmx-canvas arrange', 'pmx-canvas arrange --layout column', 'pmx-canvas arrange --layout flow'],
|
|
1812
|
+
async (args) => {
|
|
1813
|
+
const { flags } = parseFlags(args);
|
|
1814
|
+
if (flags.help || flags.h) return showCommandHelp('arrange');
|
|
1815
|
+
|
|
1816
|
+
const body: Record<string, unknown> = {};
|
|
1817
|
+
if (flags.layout && flags.layout !== true) body.layout = flags.layout;
|
|
1818
|
+
|
|
1819
|
+
const result = await api('POST', '/api/canvas/arrange', body);
|
|
1820
|
+
output(result);
|
|
1821
|
+
},
|
|
1822
|
+
);
|
|
1747
1823
|
|
|
1748
1824
|
// ── focus ────────────────────────────────────────────────────
|
|
1749
|
-
cmd('focus', 'Pan viewport to center on a node', [
|
|
1750
|
-
'pmx-canvas focus <node-id>',
|
|
1751
|
-
], async (args) => {
|
|
1825
|
+
cmd('focus', 'Pan viewport to center on a node', ['pmx-canvas focus <node-id>'], async (args) => {
|
|
1752
1826
|
const { positional, flags } = parseFlags(args);
|
|
1753
1827
|
if (flags.help || flags.h) return showCommandHelp('focus');
|
|
1754
1828
|
|
|
@@ -1759,269 +1833,347 @@ cmd('focus', 'Pan viewport to center on a node', [
|
|
|
1759
1833
|
output(result);
|
|
1760
1834
|
});
|
|
1761
1835
|
|
|
1762
|
-
cmd(
|
|
1763
|
-
'
|
|
1764
|
-
'
|
|
1765
|
-
'pmx-canvas fit node-a node-b',
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
}
|
|
1836
|
+
cmd(
|
|
1837
|
+
'fit',
|
|
1838
|
+
'Fit the viewport to all nodes or a selected subset',
|
|
1839
|
+
['pmx-canvas fit', 'pmx-canvas fit --width 1440 --height 900 --padding 80', 'pmx-canvas fit node-a node-b'],
|
|
1840
|
+
async (args) => {
|
|
1841
|
+
const { positional, flags } = parseFlags(args);
|
|
1842
|
+
if (flags.help || flags.h) return showCommandHelp('fit');
|
|
1843
|
+
|
|
1844
|
+
const body: Record<string, unknown> = {};
|
|
1845
|
+
const width = optionalPositiveFiniteFlag(flags, 'width', 'Use a positive number, e.g. --width 1440');
|
|
1846
|
+
const height = optionalPositiveFiniteFlag(flags, 'height', 'Use a positive number, e.g. --height 900');
|
|
1847
|
+
const padding = optionalPositiveFiniteFlag(flags, 'padding', 'Use a positive number, e.g. --padding 80');
|
|
1848
|
+
const maxScale = optionalPositiveFiniteFlag(flags, 'max-scale', 'Use a positive number, e.g. --max-scale 1');
|
|
1849
|
+
if (width !== undefined) body.width = width;
|
|
1850
|
+
if (height !== undefined) body.height = height;
|
|
1851
|
+
if (padding !== undefined) body.padding = padding;
|
|
1852
|
+
if (maxScale !== undefined) body.maxScale = maxScale;
|
|
1853
|
+
if (positional.length > 0) body.nodeIds = positional;
|
|
1854
|
+
|
|
1855
|
+
const result = await api('POST', '/api/canvas/fit', body);
|
|
1856
|
+
output(result);
|
|
1857
|
+
},
|
|
1858
|
+
);
|
|
1784
1859
|
|
|
1785
1860
|
// ── external-app add ─────────────────────────────────────────
|
|
1786
|
-
cmd(
|
|
1787
|
-
'
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
title: typeof flags.title === 'string' ? flags.title : 'Excalidraw Diagram',
|
|
1799
|
-
elements: DEFAULT_EXCALIDRAW_ELEMENTS,
|
|
1800
|
-
};
|
|
1801
|
-
const nodeId = getStringFlag(flags, 'node-id', 'nodeId', 'id');
|
|
1802
|
-
if (nodeId) body.nodeId = nodeId;
|
|
1803
|
-
const elementsJson = getStringFlag(flags, 'elements-json', 'elements');
|
|
1804
|
-
if (elementsJson !== undefined) body.elements = parseJsonValue(elementsJson, 'Excalidraw elements', 'Use --elements-json \'[{"type":"rectangle","id":"r1","x":0,"y":0,"width":120,"height":80}]\'');
|
|
1805
|
-
const elementsFile = getStringFlag(flags, 'elements-file', 'initial-file');
|
|
1806
|
-
if (elementsFile) body.elements = parseJsonValue(readFileSync(elementsFile, 'utf-8'), 'Excalidraw elements file', 'Use --elements-file ./scene.excalidraw');
|
|
1807
|
-
applyCommonGeometryFlags(body, flags, {
|
|
1808
|
-
x: 'Use a finite number, e.g. --x 500',
|
|
1809
|
-
y: 'Use a finite number, e.g. --y 300',
|
|
1810
|
-
width: 'Use a positive number, e.g. --width 960',
|
|
1811
|
-
height: 'Use a positive number, e.g. --height 720',
|
|
1812
|
-
});
|
|
1813
|
-
const timeoutMs = optionalPositiveFiniteFlag(flags, 'timeout-ms', 'Use a positive number, e.g. --timeout-ms 120000');
|
|
1814
|
-
if (timeoutMs !== undefined) body.timeoutMs = timeoutMs;
|
|
1861
|
+
cmd(
|
|
1862
|
+
'external-app add',
|
|
1863
|
+
'Create a hosted external app node',
|
|
1864
|
+
['pmx-canvas external-app add --kind excalidraw --title "Diagram"'],
|
|
1865
|
+
async (args) => {
|
|
1866
|
+
const { flags } = parseFlags(args);
|
|
1867
|
+
if (flags.help || flags.h) return showCommandHelp('external-app add');
|
|
1868
|
+
|
|
1869
|
+
const kind = typeof flags.kind === 'string' ? flags.kind.trim() : '';
|
|
1870
|
+
if (kind !== 'excalidraw') {
|
|
1871
|
+
die('Unsupported external app kind.', 'Use: pmx-canvas external-app add --kind excalidraw --title "Diagram"');
|
|
1872
|
+
}
|
|
1815
1873
|
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1874
|
+
const body: Record<string, unknown> = {
|
|
1875
|
+
title: typeof flags.title === 'string' ? flags.title : 'Excalidraw Diagram',
|
|
1876
|
+
elements: DEFAULT_EXCALIDRAW_ELEMENTS,
|
|
1877
|
+
};
|
|
1878
|
+
const nodeId = getStringFlag(flags, 'node-id', 'nodeId', 'id');
|
|
1879
|
+
if (nodeId) body.nodeId = nodeId;
|
|
1880
|
+
const elementsJson = getStringFlag(flags, 'elements-json', 'elements');
|
|
1881
|
+
if (elementsJson !== undefined)
|
|
1882
|
+
body.elements = parseJsonValue(
|
|
1883
|
+
elementsJson,
|
|
1884
|
+
'Excalidraw elements',
|
|
1885
|
+
'Use --elements-json \'[{"type":"rectangle","id":"r1","x":0,"y":0,"width":120,"height":80}]\'',
|
|
1886
|
+
);
|
|
1887
|
+
const elementsFile = getStringFlag(flags, 'elements-file', 'initial-file');
|
|
1888
|
+
if (elementsFile)
|
|
1889
|
+
body.elements = parseJsonValue(
|
|
1890
|
+
readFileSync(elementsFile, 'utf-8'),
|
|
1891
|
+
'Excalidraw elements file',
|
|
1892
|
+
'Use --elements-file ./scene.excalidraw',
|
|
1893
|
+
);
|
|
1894
|
+
applyCommonGeometryFlags(body, flags, {
|
|
1895
|
+
x: 'Use a finite number, e.g. --x 500',
|
|
1896
|
+
y: 'Use a finite number, e.g. --y 300',
|
|
1897
|
+
width: 'Use a positive number, e.g. --width 960',
|
|
1898
|
+
height: 'Use a positive number, e.g. --height 720',
|
|
1899
|
+
});
|
|
1900
|
+
const timeoutMs = optionalPositiveFiniteFlag(
|
|
1901
|
+
flags,
|
|
1902
|
+
'timeout-ms',
|
|
1903
|
+
'Use a positive number, e.g. --timeout-ms 120000',
|
|
1904
|
+
);
|
|
1905
|
+
if (timeoutMs !== undefined) body.timeoutMs = timeoutMs;
|
|
1821
1906
|
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1907
|
+
const result = await api('POST', '/api/canvas/diagram', body);
|
|
1908
|
+
output(
|
|
1909
|
+
result && typeof result === 'object' && !Array.isArray(result) && 'nodeId' in result && !('id' in result)
|
|
1910
|
+
? { id: (result as { nodeId?: unknown }).nodeId, ...result }
|
|
1911
|
+
: result,
|
|
1912
|
+
);
|
|
1913
|
+
},
|
|
1914
|
+
);
|
|
1915
|
+
|
|
1916
|
+
cmd(
|
|
1917
|
+
'diagram add',
|
|
1918
|
+
'Create an Excalidraw diagram node',
|
|
1919
|
+
[
|
|
1920
|
+
'pmx-canvas diagram add --title "Architecture"',
|
|
1921
|
+
'pmx-canvas diagram add --title "Architecture" --elements \'[{"type":"rectangle","id":"r1","x":0,"y":0,"width":120,"height":80}]\'',
|
|
1922
|
+
],
|
|
1923
|
+
async (args) => {
|
|
1924
|
+
const { flags } = parseFlags(args);
|
|
1925
|
+
if (flags.help || flags.h) return showCommandHelp('diagram add');
|
|
1926
|
+
const externalAppAdd = COMMANDS['external-app add'];
|
|
1927
|
+
await externalAppAdd.run([...args, '--kind', 'excalidraw']);
|
|
1928
|
+
},
|
|
1929
|
+
);
|
|
1831
1930
|
|
|
1832
1931
|
// ── pin ──────────────────────────────────────────────────────
|
|
1833
|
-
cmd(
|
|
1834
|
-
'
|
|
1835
|
-
'
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1932
|
+
cmd(
|
|
1933
|
+
'pin',
|
|
1934
|
+
'Manage context pins',
|
|
1935
|
+
[
|
|
1936
|
+
'pmx-canvas pin node1 node2 node3',
|
|
1937
|
+
'pmx-canvas pin --set node1 node2 node3',
|
|
1938
|
+
'pmx-canvas pin --list',
|
|
1939
|
+
'pmx-canvas pin --clear',
|
|
1940
|
+
],
|
|
1941
|
+
async (args) => {
|
|
1942
|
+
const { positional, flags } = parseFlags(args);
|
|
1943
|
+
if (flags.help || flags.h) return showCommandHelp('pin');
|
|
1944
|
+
|
|
1945
|
+
if (flags.list) {
|
|
1946
|
+
const result = await api('GET', '/api/canvas/pinned-context');
|
|
1947
|
+
output(result);
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1841
1950
|
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1951
|
+
if (flags.clear) {
|
|
1952
|
+
const result = await invokeOperation('pin.set', { nodeIds: [] });
|
|
1953
|
+
output(result);
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1847
1956
|
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1957
|
+
// --set: positional args are node IDs
|
|
1958
|
+
if (positional.length > 0 || flags.set) {
|
|
1959
|
+
const result = await invokeOperation('pin.set', { nodeIds: positional });
|
|
1960
|
+
output(result);
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1853
1963
|
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
const result = await invokeOperation('pin.set', { nodeIds: positional });
|
|
1964
|
+
// Default: list
|
|
1965
|
+
const result = await api('GET', '/api/canvas/pinned-context');
|
|
1857
1966
|
output(result);
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
// Default: list
|
|
1862
|
-
const result = await api('GET', '/api/canvas/pinned-context');
|
|
1863
|
-
output(result);
|
|
1864
|
-
});
|
|
1967
|
+
},
|
|
1968
|
+
);
|
|
1865
1969
|
|
|
1866
1970
|
// ── AX ────────────────────────────────────────────────────────
|
|
1867
|
-
cmd('ax status', 'Read host-agnostic PMX AX state', [
|
|
1868
|
-
'pmx-canvas ax status',
|
|
1869
|
-
], async (args) => {
|
|
1971
|
+
cmd('ax status', 'Read host-agnostic PMX AX state', ['pmx-canvas ax status'], async (args) => {
|
|
1870
1972
|
const { flags } = parseFlags(args);
|
|
1871
1973
|
if (flags.help || flags.h) return showCommandHelp('ax status');
|
|
1872
1974
|
|
|
1873
1975
|
output(await api('GET', '/api/canvas/ax'));
|
|
1874
1976
|
});
|
|
1875
1977
|
|
|
1876
|
-
cmd('ax context', 'Read agent-ready PMX AX context', [
|
|
1877
|
-
'pmx-canvas ax context',
|
|
1878
|
-
], async (args) => {
|
|
1978
|
+
cmd('ax context', 'Read agent-ready PMX AX context', ['pmx-canvas ax context'], async (args) => {
|
|
1879
1979
|
const { flags } = parseFlags(args);
|
|
1880
1980
|
if (flags.help || flags.h) return showCommandHelp('ax context');
|
|
1881
1981
|
|
|
1882
1982
|
output(await api('GET', '/api/canvas/ax/context'));
|
|
1883
1983
|
});
|
|
1884
1984
|
|
|
1885
|
-
cmd(
|
|
1886
|
-
'
|
|
1887
|
-
'
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
const nodeIds = flags.clear ? [] : positional;
|
|
1893
|
-
if (!flags.clear && nodeIds.length === 0) {
|
|
1894
|
-
die('Missing node ID', 'pmx-canvas ax focus <node-id> [more-node-ids]');
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
|
-
output(await api('POST', '/api/canvas/ax/focus', { nodeIds, source: resolveAxSource(flags) }));
|
|
1898
|
-
});
|
|
1899
|
-
|
|
1900
|
-
cmd('ax event add', 'Record a normalized AX timeline event', [
|
|
1901
|
-
'pmx-canvas ax event add --kind tool-start --summary "ran tests"',
|
|
1902
|
-
'pmx-canvas ax event add --kind failure --summary "build broke" --detail "..." node1 node2',
|
|
1903
|
-
], async (args) => {
|
|
1904
|
-
const { positional, flags } = parseFlags(args);
|
|
1905
|
-
if (flags.help || flags.h) return showCommandHelp('ax event add');
|
|
1906
|
-
|
|
1907
|
-
const kind = requireFlag(flags, 'kind', 'pmx-canvas ax event add --kind <kind> --summary <text>');
|
|
1908
|
-
const summary = requireFlag(flags, 'summary', 'pmx-canvas ax event add --kind <kind> --summary <text>');
|
|
1909
|
-
const detail = getStringFlag(flags, 'detail');
|
|
1910
|
-
|
|
1911
|
-
output(await api('POST', '/api/canvas/ax/event', {
|
|
1912
|
-
kind,
|
|
1913
|
-
summary,
|
|
1914
|
-
...(detail ? { detail } : {}),
|
|
1915
|
-
...(positional.length > 0 ? { nodeIds: positional } : {}),
|
|
1916
|
-
source: resolveAxSource(flags),
|
|
1917
|
-
}));
|
|
1918
|
-
});
|
|
1919
|
-
|
|
1920
|
-
cmd('ax steer', 'Send a steering message to the active agent session', [
|
|
1921
|
-
'pmx-canvas ax steer "focus on the failing test first"',
|
|
1922
|
-
'pmx-canvas ax steer --message "stop and re-plan"',
|
|
1923
|
-
], async (args) => {
|
|
1924
|
-
const { positional, flags } = parseFlags(args);
|
|
1925
|
-
if (flags.help || flags.h) return showCommandHelp('ax steer');
|
|
1926
|
-
|
|
1927
|
-
const message = getStringFlag(flags, 'message') ?? positional.join(' ').trim();
|
|
1928
|
-
if (!message) {
|
|
1929
|
-
die('Missing steering message', 'pmx-canvas ax steer <message>');
|
|
1930
|
-
}
|
|
1931
|
-
|
|
1932
|
-
output(await api('POST', '/api/canvas/ax/steer', { message, source: resolveAxSource(flags) }));
|
|
1933
|
-
});
|
|
1934
|
-
|
|
1935
|
-
cmd('ax interaction', 'Submit a node-originated AX interaction (capability-gated)', [
|
|
1936
|
-
'pmx-canvas ax interaction --type ax.work.create --node node-1 --payload \'{"title":"Wire auth"}\'',
|
|
1937
|
-
'pmx-canvas ax interaction --type ax.focus.set --node node-2',
|
|
1938
|
-
], async (args) => {
|
|
1939
|
-
const { flags } = parseFlags(args);
|
|
1940
|
-
if (flags.help || flags.h) return showCommandHelp('ax interaction');
|
|
1941
|
-
|
|
1942
|
-
const type = getStringFlag(flags, 'type');
|
|
1943
|
-
if (!type) die('Missing --type', 'pmx-canvas ax interaction --type <ax.*> --node <id> [--payload <json>]');
|
|
1944
|
-
const sourceNodeId = getStringFlag(flags, 'node');
|
|
1945
|
-
if (!sourceNodeId) die('Missing --node', 'pmx-canvas ax interaction --type <ax.*> --node <id>');
|
|
1985
|
+
cmd(
|
|
1986
|
+
'ax focus',
|
|
1987
|
+
'Set or clear PMX AX focus without moving the viewport',
|
|
1988
|
+
['pmx-canvas ax focus node1 node2', 'pmx-canvas ax focus --clear'],
|
|
1989
|
+
async (args) => {
|
|
1990
|
+
const { positional, flags } = parseFlags(args);
|
|
1991
|
+
if (flags.help || flags.h) return showCommandHelp('ax focus');
|
|
1946
1992
|
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
try {
|
|
1951
|
-
payload = JSON.parse(payloadRaw);
|
|
1952
|
-
} catch {
|
|
1953
|
-
die('Invalid --payload JSON', 'pmx-canvas ax interaction --payload \'{"title":"..."}\'');
|
|
1993
|
+
const nodeIds = flags.clear ? [] : positional;
|
|
1994
|
+
if (!flags.clear && nodeIds.length === 0) {
|
|
1995
|
+
die('Missing node ID', 'pmx-canvas ax focus <node-id> [more-node-ids]');
|
|
1954
1996
|
}
|
|
1955
|
-
}
|
|
1956
|
-
|
|
1957
|
-
output(await api('POST', '/api/canvas/ax/interaction', {
|
|
1958
|
-
type,
|
|
1959
|
-
sourceNodeId,
|
|
1960
|
-
...(payload !== undefined ? { payload } : {}),
|
|
1961
|
-
source: resolveAxSource(flags),
|
|
1962
|
-
}));
|
|
1963
|
-
});
|
|
1964
|
-
|
|
1965
|
-
cmd('ax delivery list', 'List pending AX steering for a consumer (loop-safe)', [
|
|
1966
|
-
'pmx-canvas ax delivery list',
|
|
1967
|
-
'pmx-canvas ax delivery list --consumer copilot --limit 20',
|
|
1968
|
-
'pmx-canvas ax delivery list --order newest # latest browser steering first (#68)',
|
|
1969
|
-
], async (args) => {
|
|
1970
|
-
const { flags } = parseFlags(args);
|
|
1971
|
-
if (flags.help || flags.h) return showCommandHelp('ax delivery list');
|
|
1972
|
-
const consumer = getStringFlag(flags, 'consumer');
|
|
1973
|
-
const limit = optionalNumberFlag(flags, 'limit', 'pmx-canvas ax delivery list --limit <n>');
|
|
1974
|
-
const order = getStringFlag(flags, 'order');
|
|
1975
|
-
if (order !== undefined && order !== 'newest' && order !== 'oldest') {
|
|
1976
|
-
die('Invalid --order', 'pmx-canvas ax delivery list --order newest|oldest');
|
|
1977
|
-
}
|
|
1978
|
-
const params = new URLSearchParams();
|
|
1979
|
-
if (consumer) params.set('consumer', consumer);
|
|
1980
|
-
if (limit) params.set('limit', String(limit));
|
|
1981
|
-
if (order) params.set('order', order);
|
|
1982
|
-
const qs = params.toString();
|
|
1983
|
-
output(await api('GET', `/api/canvas/ax/delivery/pending${qs ? `?${qs}` : ''}`));
|
|
1984
|
-
});
|
|
1985
1997
|
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1998
|
+
output(await api('POST', '/api/canvas/ax/focus', { nodeIds, source: resolveAxSource(flags) }));
|
|
1999
|
+
},
|
|
2000
|
+
);
|
|
2001
|
+
|
|
2002
|
+
cmd(
|
|
2003
|
+
'ax event add',
|
|
2004
|
+
'Record a normalized AX timeline event',
|
|
2005
|
+
[
|
|
2006
|
+
'pmx-canvas ax event add --kind tool-start --summary "ran tests"',
|
|
2007
|
+
'pmx-canvas ax event add --kind failure --summary "build broke" --detail "..." node1 node2',
|
|
2008
|
+
],
|
|
2009
|
+
async (args) => {
|
|
2010
|
+
const { positional, flags } = parseFlags(args);
|
|
2011
|
+
if (flags.help || flags.h) return showCommandHelp('ax event add');
|
|
2012
|
+
|
|
2013
|
+
const kind = requireFlag(flags, 'kind', 'pmx-canvas ax event add --kind <kind> --summary <text>');
|
|
2014
|
+
const summary = requireFlag(flags, 'summary', 'pmx-canvas ax event add --kind <kind> --summary <text>');
|
|
2015
|
+
const detail = getStringFlag(flags, 'detail');
|
|
2016
|
+
|
|
2017
|
+
output(
|
|
2018
|
+
await api('POST', '/api/canvas/ax/event', {
|
|
2019
|
+
kind,
|
|
2020
|
+
summary,
|
|
2021
|
+
...(detail ? { detail } : {}),
|
|
2022
|
+
...(positional.length > 0 ? { nodeIds: positional } : {}),
|
|
2023
|
+
source: resolveAxSource(flags),
|
|
2024
|
+
}),
|
|
2025
|
+
);
|
|
2026
|
+
},
|
|
2027
|
+
);
|
|
2028
|
+
|
|
2029
|
+
cmd(
|
|
2030
|
+
'ax steer',
|
|
2031
|
+
'Send a steering message to the active agent session',
|
|
2032
|
+
['pmx-canvas ax steer "focus on the failing test first"', 'pmx-canvas ax steer --message "stop and re-plan"'],
|
|
2033
|
+
async (args) => {
|
|
2034
|
+
const { positional, flags } = parseFlags(args);
|
|
2035
|
+
if (flags.help || flags.h) return showCommandHelp('ax steer');
|
|
2036
|
+
|
|
2037
|
+
const message = getStringFlag(flags, 'message') ?? positional.join(' ').trim();
|
|
2038
|
+
if (!message) {
|
|
2039
|
+
die('Missing steering message', 'pmx-canvas ax steer <message>');
|
|
2040
|
+
}
|
|
1995
2041
|
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2042
|
+
output(await api('POST', '/api/canvas/ax/steer', { message, source: resolveAxSource(flags) }));
|
|
2043
|
+
},
|
|
2044
|
+
);
|
|
2045
|
+
|
|
2046
|
+
cmd(
|
|
2047
|
+
'ax interaction',
|
|
2048
|
+
'Submit a node-originated AX interaction (capability-gated)',
|
|
2049
|
+
[
|
|
2050
|
+
'pmx-canvas ax interaction --type ax.work.create --node node-1 --payload \'{"title":"Wire auth"}\'',
|
|
2051
|
+
'pmx-canvas ax interaction --type ax.focus.set --node node-2',
|
|
2052
|
+
],
|
|
2053
|
+
async (args) => {
|
|
2054
|
+
const { flags } = parseFlags(args);
|
|
2055
|
+
if (flags.help || flags.h) return showCommandHelp('ax interaction');
|
|
2056
|
+
|
|
2057
|
+
const type = getStringFlag(flags, 'type');
|
|
2058
|
+
if (!type) die('Missing --type', 'pmx-canvas ax interaction --type <ax.*> --node <id> [--payload <json>]');
|
|
2059
|
+
const sourceNodeId = getStringFlag(flags, 'node');
|
|
2060
|
+
if (!sourceNodeId) die('Missing --node', 'pmx-canvas ax interaction --type <ax.*> --node <id>');
|
|
2061
|
+
|
|
2062
|
+
let payload: unknown;
|
|
2063
|
+
const payloadRaw = getStringFlag(flags, 'payload');
|
|
2064
|
+
if (payloadRaw) {
|
|
2065
|
+
try {
|
|
2066
|
+
payload = JSON.parse(payloadRaw);
|
|
2067
|
+
} catch {
|
|
2068
|
+
die('Invalid --payload JSON', 'pmx-canvas ax interaction --payload \'{"title":"..."}\'');
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2010
2071
|
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2072
|
+
output(
|
|
2073
|
+
await api('POST', '/api/canvas/ax/interaction', {
|
|
2074
|
+
type,
|
|
2075
|
+
sourceNodeId,
|
|
2076
|
+
...(payload !== undefined ? { payload } : {}),
|
|
2077
|
+
source: resolveAxSource(flags),
|
|
2078
|
+
}),
|
|
2079
|
+
);
|
|
2080
|
+
},
|
|
2081
|
+
);
|
|
2082
|
+
|
|
2083
|
+
cmd(
|
|
2084
|
+
'ax delivery list',
|
|
2085
|
+
'List pending AX steering for a consumer (loop-safe)',
|
|
2086
|
+
[
|
|
2087
|
+
'pmx-canvas ax delivery list',
|
|
2088
|
+
'pmx-canvas ax delivery list --consumer copilot --limit 20',
|
|
2089
|
+
'pmx-canvas ax delivery list --order newest # latest browser steering first (#68)',
|
|
2090
|
+
],
|
|
2091
|
+
async (args) => {
|
|
2092
|
+
const { flags } = parseFlags(args);
|
|
2093
|
+
if (flags.help || flags.h) return showCommandHelp('ax delivery list');
|
|
2094
|
+
const consumer = getStringFlag(flags, 'consumer');
|
|
2095
|
+
const limit = optionalNumberFlag(flags, 'limit', 'pmx-canvas ax delivery list --limit <n>');
|
|
2096
|
+
const order = getStringFlag(flags, 'order');
|
|
2097
|
+
if (order !== undefined && order !== 'newest' && order !== 'oldest') {
|
|
2098
|
+
die('Invalid --order', 'pmx-canvas ax delivery list --order newest|oldest');
|
|
2099
|
+
}
|
|
2100
|
+
const params = new URLSearchParams();
|
|
2101
|
+
if (consumer) params.set('consumer', consumer);
|
|
2102
|
+
if (limit) params.set('limit', String(limit));
|
|
2103
|
+
if (order) params.set('order', order);
|
|
2104
|
+
const qs = params.toString();
|
|
2105
|
+
output(await api('GET', `/api/canvas/ax/delivery/pending${qs ? `?${qs}` : ''}`));
|
|
2106
|
+
},
|
|
2107
|
+
);
|
|
2108
|
+
|
|
2109
|
+
cmd(
|
|
2110
|
+
'ax delivery mark',
|
|
2111
|
+
'Mark an AX steering message as delivered',
|
|
2112
|
+
['pmx-canvas ax delivery mark <steering-id>'],
|
|
2113
|
+
async (args) => {
|
|
2114
|
+
const { positional, flags } = parseFlags(args);
|
|
2115
|
+
if (flags.help || flags.h) return showCommandHelp('ax delivery mark');
|
|
2116
|
+
const id = getStringFlag(flags, 'id') ?? positional[0];
|
|
2117
|
+
if (!id) die('Missing steering id', 'pmx-canvas ax delivery mark <steering-id>');
|
|
2118
|
+
output(await api('POST', `/api/canvas/ax/delivery/${encodeURIComponent(id)}/mark`, {}));
|
|
2119
|
+
},
|
|
2120
|
+
);
|
|
2121
|
+
|
|
2122
|
+
cmd(
|
|
2123
|
+
'ax elicitation request',
|
|
2124
|
+
'Request structured human input',
|
|
2125
|
+
[
|
|
2126
|
+
'pmx-canvas ax elicitation request --prompt "Who owns this migration?"',
|
|
2127
|
+
'pmx-canvas ax elicitation request --prompt "Pick a region" --fields region,owner',
|
|
2128
|
+
],
|
|
2129
|
+
async (args) => {
|
|
2130
|
+
const { flags } = parseFlags(args);
|
|
2131
|
+
if (flags.help || flags.h) return showCommandHelp('ax elicitation request');
|
|
2132
|
+
const prompt = requireFlag(flags, 'prompt', 'pmx-canvas ax elicitation request --prompt <text>');
|
|
2133
|
+
const fields = getStringFlag(flags, 'fields');
|
|
2134
|
+
output(
|
|
2135
|
+
await api('POST', '/api/canvas/ax/elicitation', {
|
|
2136
|
+
prompt,
|
|
2137
|
+
...(fields
|
|
2138
|
+
? {
|
|
2139
|
+
fields: fields
|
|
2140
|
+
.split(',')
|
|
2141
|
+
.map((f) => f.trim())
|
|
2142
|
+
.filter(Boolean),
|
|
2143
|
+
}
|
|
2144
|
+
: {}),
|
|
2145
|
+
source: resolveAxSource(flags),
|
|
2146
|
+
}),
|
|
2147
|
+
);
|
|
2148
|
+
},
|
|
2149
|
+
);
|
|
2150
|
+
|
|
2151
|
+
cmd(
|
|
2152
|
+
'ax elicitation respond',
|
|
2153
|
+
'Answer a pending elicitation',
|
|
2154
|
+
['pmx-canvas ax elicitation respond <id> --response \'{"owner":"alice"}\''],
|
|
2155
|
+
async (args) => {
|
|
2156
|
+
const { positional, flags } = parseFlags(args);
|
|
2157
|
+
if (flags.help || flags.h) return showCommandHelp('ax elicitation respond');
|
|
2158
|
+
const id = getStringFlag(flags, 'id') ?? positional[0];
|
|
2159
|
+
if (!id) die('Missing elicitation id', 'pmx-canvas ax elicitation respond <id> --response <json>');
|
|
2160
|
+
let response: unknown = {};
|
|
2161
|
+
const raw = getStringFlag(flags, 'response');
|
|
2162
|
+
if (raw) {
|
|
2163
|
+
try {
|
|
2164
|
+
response = JSON.parse(raw);
|
|
2165
|
+
} catch {
|
|
2166
|
+
die('Invalid --response JSON', '--response \'{"k":"v"}\'');
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
output(
|
|
2170
|
+
await api('POST', `/api/canvas/ax/elicitation/${encodeURIComponent(id)}/respond`, {
|
|
2171
|
+
response,
|
|
2172
|
+
source: resolveAxSource(flags),
|
|
2173
|
+
}),
|
|
2174
|
+
);
|
|
2175
|
+
},
|
|
2176
|
+
);
|
|
2025
2177
|
|
|
2026
2178
|
cmd('ax elicitation list', 'List elicitations', ['pmx-canvas ax elicitation list'], async (args) => {
|
|
2027
2179
|
const { flags } = parseFlags(args);
|
|
@@ -2029,32 +2181,42 @@ cmd('ax elicitation list', 'List elicitations', ['pmx-canvas ax elicitation list
|
|
|
2029
2181
|
output(await api('GET', '/api/canvas/ax/elicitation'));
|
|
2030
2182
|
});
|
|
2031
2183
|
|
|
2032
|
-
cmd(
|
|
2033
|
-
'
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2184
|
+
cmd(
|
|
2185
|
+
'ax mode request',
|
|
2186
|
+
'Request a workflow mode transition (plan/execute/autonomous)',
|
|
2187
|
+
['pmx-canvas ax mode request --mode execute --reason "plan approved"'],
|
|
2188
|
+
async (args) => {
|
|
2189
|
+
const { flags } = parseFlags(args);
|
|
2190
|
+
if (flags.help || flags.h) return showCommandHelp('ax mode request');
|
|
2191
|
+
const mode = requireFlag(flags, 'mode', 'pmx-canvas ax mode request --mode plan|execute|autonomous');
|
|
2192
|
+
const reason = getStringFlag(flags, 'reason');
|
|
2193
|
+
output(
|
|
2194
|
+
await api('POST', '/api/canvas/ax/mode', { mode, ...(reason ? { reason } : {}), source: resolveAxSource(flags) }),
|
|
2195
|
+
);
|
|
2196
|
+
},
|
|
2197
|
+
);
|
|
2198
|
+
|
|
2199
|
+
cmd(
|
|
2200
|
+
'ax mode resolve',
|
|
2201
|
+
'Resolve a pending mode request',
|
|
2202
|
+
['pmx-canvas ax mode resolve <id> --decision approved'],
|
|
2203
|
+
async (args) => {
|
|
2204
|
+
const { positional, flags } = parseFlags(args);
|
|
2205
|
+
if (flags.help || flags.h) return showCommandHelp('ax mode resolve');
|
|
2206
|
+
const id = getStringFlag(flags, 'id') ?? positional[0];
|
|
2207
|
+
if (!id) die('Missing mode request id', 'pmx-canvas ax mode resolve <id> --decision approved|rejected');
|
|
2208
|
+
const decision = getStringFlag(flags, 'decision');
|
|
2209
|
+
if (decision !== 'approved' && decision !== 'rejected') die('Invalid --decision', '--decision approved|rejected');
|
|
2210
|
+
const resolution = getStringFlag(flags, 'resolution');
|
|
2211
|
+
output(
|
|
2212
|
+
await api('POST', `/api/canvas/ax/mode/${encodeURIComponent(id)}/resolve`, {
|
|
2213
|
+
decision,
|
|
2214
|
+
...(resolution ? { resolution } : {}),
|
|
2215
|
+
source: resolveAxSource(flags),
|
|
2216
|
+
}),
|
|
2217
|
+
);
|
|
2218
|
+
},
|
|
2219
|
+
);
|
|
2058
2220
|
|
|
2059
2221
|
cmd('ax mode list', 'List mode requests', ['pmx-canvas ax mode list'], async (args) => {
|
|
2060
2222
|
const { flags } = parseFlags(args);
|
|
@@ -2068,21 +2230,36 @@ cmd('ax command list', 'List the PMX command registry', ['pmx-canvas ax command
|
|
|
2068
2230
|
output(await api('GET', '/api/canvas/ax/command'));
|
|
2069
2231
|
});
|
|
2070
2232
|
|
|
2071
|
-
cmd(
|
|
2072
|
-
'
|
|
2073
|
-
'
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2233
|
+
cmd(
|
|
2234
|
+
'ax command invoke',
|
|
2235
|
+
'Invoke a registry-gated PMX command intent',
|
|
2236
|
+
[
|
|
2237
|
+
'pmx-canvas ax command invoke pmx.plan',
|
|
2238
|
+
'pmx-canvas ax command invoke pmx.promote-context --args \'{"nodeIds":["n1"]}\'',
|
|
2239
|
+
],
|
|
2240
|
+
async (args) => {
|
|
2241
|
+
const { positional, flags } = parseFlags(args);
|
|
2242
|
+
if (flags.help || flags.h) return showCommandHelp('ax command invoke');
|
|
2243
|
+
const name = getStringFlag(flags, 'name') ?? positional[0];
|
|
2244
|
+
if (!name) die('Missing command name', 'pmx-canvas ax command invoke <name>');
|
|
2245
|
+
let cmdArgs: unknown;
|
|
2246
|
+
const raw = getStringFlag(flags, 'args');
|
|
2247
|
+
if (raw) {
|
|
2248
|
+
try {
|
|
2249
|
+
cmdArgs = JSON.parse(raw);
|
|
2250
|
+
} catch {
|
|
2251
|
+
die('Invalid --args JSON', '--args \'{"k":"v"}\'');
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
output(
|
|
2255
|
+
await api('POST', '/api/canvas/ax/command', {
|
|
2256
|
+
name,
|
|
2257
|
+
...(cmdArgs !== undefined ? { args: cmdArgs } : {}),
|
|
2258
|
+
source: resolveAxSource(flags),
|
|
2259
|
+
}),
|
|
2260
|
+
);
|
|
2261
|
+
},
|
|
2262
|
+
);
|
|
2086
2263
|
|
|
2087
2264
|
cmd('ax policy get', 'Show the current declarative AX policy', ['pmx-canvas ax policy get'], async (args) => {
|
|
2088
2265
|
const { flags } = parseFlags(args);
|
|
@@ -2090,222 +2267,285 @@ cmd('ax policy get', 'Show the current declarative AX policy', ['pmx-canvas ax p
|
|
|
2090
2267
|
output(await api('GET', '/api/canvas/ax/policy'));
|
|
2091
2268
|
});
|
|
2092
2269
|
|
|
2093
|
-
cmd(
|
|
2094
|
-
'
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
}
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
'
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
})
|
|
2270
|
+
cmd(
|
|
2271
|
+
'ax policy set',
|
|
2272
|
+
'Set the declarative AX policy (stored by PMX, enforced by adapters)',
|
|
2273
|
+
['pmx-canvas ax policy set --excluded-tools shell,write --mode concise'],
|
|
2274
|
+
async (args) => {
|
|
2275
|
+
const { flags } = parseFlags(args);
|
|
2276
|
+
if (flags.help || flags.h) return showCommandHelp('ax policy set');
|
|
2277
|
+
const csv = (v?: string) =>
|
|
2278
|
+
v
|
|
2279
|
+
? v
|
|
2280
|
+
.split(',')
|
|
2281
|
+
.map((s) => s.trim())
|
|
2282
|
+
.filter(Boolean)
|
|
2283
|
+
: undefined;
|
|
2284
|
+
const allowed = csv(getStringFlag(flags, 'allowed-tools'));
|
|
2285
|
+
const excluded = csv(getStringFlag(flags, 'excluded-tools'));
|
|
2286
|
+
const approvalRequired = csv(getStringFlag(flags, 'approval-tools'));
|
|
2287
|
+
const mode = getStringFlag(flags, 'mode');
|
|
2288
|
+
const systemAppend = getStringFlag(flags, 'system-append');
|
|
2289
|
+
const tools =
|
|
2290
|
+
allowed || excluded || approvalRequired
|
|
2291
|
+
? {
|
|
2292
|
+
...(allowed ? { allowed } : {}),
|
|
2293
|
+
...(excluded ? { excluded } : {}),
|
|
2294
|
+
...(approvalRequired ? { approvalRequired } : {}),
|
|
2295
|
+
}
|
|
2296
|
+
: undefined;
|
|
2297
|
+
const prompt =
|
|
2298
|
+
mode || systemAppend ? { ...(mode ? { mode } : {}), ...(systemAppend ? { systemAppend } : {}) } : undefined;
|
|
2299
|
+
output(
|
|
2300
|
+
await api('POST', '/api/canvas/ax/policy', {
|
|
2301
|
+
...(tools ? { tools } : {}),
|
|
2302
|
+
...(prompt ? { prompt } : {}),
|
|
2303
|
+
source: resolveAxSource(flags),
|
|
2304
|
+
}),
|
|
2305
|
+
);
|
|
2306
|
+
},
|
|
2307
|
+
);
|
|
2308
|
+
|
|
2309
|
+
cmd(
|
|
2310
|
+
'ax timeline',
|
|
2311
|
+
'Read the bounded AX timeline (events, evidence, steering)',
|
|
2312
|
+
['pmx-canvas ax timeline', 'pmx-canvas ax timeline --limit 100'],
|
|
2313
|
+
async (args) => {
|
|
2314
|
+
const { flags } = parseFlags(args);
|
|
2315
|
+
if (flags.help || flags.h) return showCommandHelp('ax timeline');
|
|
2316
|
+
|
|
2317
|
+
const limit = optionalNumberFlag(flags, 'limit', 'pmx-canvas ax timeline --limit <n>');
|
|
2318
|
+
output(await api('GET', `/api/canvas/ax/timeline${limit ? `?limit=${limit}` : ''}`));
|
|
2319
|
+
},
|
|
2320
|
+
);
|
|
2321
|
+
|
|
2322
|
+
cmd(
|
|
2323
|
+
'ax work add',
|
|
2324
|
+
'Add a canvas-bound AX work item',
|
|
2325
|
+
[
|
|
2326
|
+
'pmx-canvas ax work add --title "Wire up auth" --status in-progress',
|
|
2327
|
+
'pmx-canvas ax work add --title "Review API" node1 node2',
|
|
2328
|
+
],
|
|
2329
|
+
async (args) => {
|
|
2330
|
+
const { positional, flags } = parseFlags(args);
|
|
2331
|
+
if (flags.help || flags.h) return showCommandHelp('ax work add');
|
|
2332
|
+
|
|
2333
|
+
const title = requireFlag(flags, 'title', 'pmx-canvas ax work add --title <text>');
|
|
2334
|
+
const status = getStringFlag(flags, 'status');
|
|
2335
|
+
const detail = getStringFlag(flags, 'detail');
|
|
2336
|
+
|
|
2337
|
+
output(
|
|
2338
|
+
await api('POST', '/api/canvas/ax/work', {
|
|
2339
|
+
title,
|
|
2340
|
+
...(status ? { status } : {}),
|
|
2341
|
+
...(detail ? { detail } : {}),
|
|
2342
|
+
...(positional.length > 0 ? { nodeIds: positional } : {}),
|
|
2343
|
+
source: resolveAxSource(flags),
|
|
2344
|
+
}),
|
|
2345
|
+
);
|
|
2346
|
+
},
|
|
2347
|
+
);
|
|
2348
|
+
|
|
2349
|
+
cmd(
|
|
2350
|
+
'ax work update',
|
|
2351
|
+
'Update a canvas-bound AX work item by ID',
|
|
2352
|
+
['pmx-canvas ax work update <id> --status done', 'pmx-canvas ax work update <id> --title "New title" --detail "..."'],
|
|
2353
|
+
async (args) => {
|
|
2354
|
+
const { positional, flags } = parseFlags(args);
|
|
2355
|
+
if (flags.help || flags.h) return showCommandHelp('ax work update');
|
|
2356
|
+
|
|
2357
|
+
const id = positional[0];
|
|
2358
|
+
if (!id) die('Missing work item ID', 'pmx-canvas ax work update <id> --status <status>');
|
|
2359
|
+
const title = getStringFlag(flags, 'title');
|
|
2360
|
+
const status = getStringFlag(flags, 'status');
|
|
2361
|
+
const detail = getStringFlag(flags, 'detail');
|
|
2362
|
+
|
|
2363
|
+
output(
|
|
2364
|
+
await api('PATCH', `/api/canvas/ax/work/${encodeURIComponent(id)}`, {
|
|
2365
|
+
...(title ? { title } : {}),
|
|
2366
|
+
...(status ? { status } : {}),
|
|
2367
|
+
...(detail ? { detail } : {}),
|
|
2368
|
+
...(positional.length > 1 ? { nodeIds: positional.slice(1) } : {}),
|
|
2369
|
+
source: resolveAxSource(flags),
|
|
2370
|
+
}),
|
|
2371
|
+
);
|
|
2372
|
+
},
|
|
2373
|
+
);
|
|
2165
2374
|
|
|
2166
|
-
cmd('ax work list', 'List canvas-bound AX work items', [
|
|
2167
|
-
'pmx-canvas ax work list',
|
|
2168
|
-
], async (args) => {
|
|
2375
|
+
cmd('ax work list', 'List canvas-bound AX work items', ['pmx-canvas ax work list'], async (args) => {
|
|
2169
2376
|
const { flags } = parseFlags(args);
|
|
2170
2377
|
if (flags.help || flags.h) return showCommandHelp('ax work list');
|
|
2171
2378
|
|
|
2172
2379
|
output(await api('GET', '/api/canvas/ax/work'));
|
|
2173
2380
|
});
|
|
2174
2381
|
|
|
2175
|
-
cmd(
|
|
2176
|
-
'
|
|
2177
|
-
'
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
})
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2382
|
+
cmd(
|
|
2383
|
+
'ax approval request',
|
|
2384
|
+
'Request a canvas-bound AX approval gate',
|
|
2385
|
+
[
|
|
2386
|
+
'pmx-canvas ax approval request --title "Deploy to prod"',
|
|
2387
|
+
'pmx-canvas ax approval request --title "Drop table" --action db.drop node1',
|
|
2388
|
+
],
|
|
2389
|
+
async (args) => {
|
|
2390
|
+
const { positional, flags } = parseFlags(args);
|
|
2391
|
+
if (flags.help || flags.h) return showCommandHelp('ax approval request');
|
|
2392
|
+
|
|
2393
|
+
const title = requireFlag(flags, 'title', 'pmx-canvas ax approval request --title <text>');
|
|
2394
|
+
const detail = getStringFlag(flags, 'detail');
|
|
2395
|
+
const action = getStringFlag(flags, 'action');
|
|
2396
|
+
|
|
2397
|
+
output(
|
|
2398
|
+
await api('POST', '/api/canvas/ax/approval', {
|
|
2399
|
+
title,
|
|
2400
|
+
...(detail ? { detail } : {}),
|
|
2401
|
+
...(action ? { action } : {}),
|
|
2402
|
+
...(positional.length > 0 ? { nodeIds: positional } : {}),
|
|
2403
|
+
source: resolveAxSource(flags),
|
|
2404
|
+
}),
|
|
2405
|
+
);
|
|
2406
|
+
},
|
|
2407
|
+
);
|
|
2408
|
+
|
|
2409
|
+
cmd(
|
|
2410
|
+
'ax approval resolve',
|
|
2411
|
+
'Resolve a pending AX approval gate by ID',
|
|
2412
|
+
[
|
|
2413
|
+
'pmx-canvas ax approval resolve <id> --decision approved',
|
|
2414
|
+
'pmx-canvas ax approval resolve <id> --decision rejected --resolution "too risky"',
|
|
2415
|
+
],
|
|
2416
|
+
async (args) => {
|
|
2417
|
+
const { positional, flags } = parseFlags(args);
|
|
2418
|
+
if (flags.help || flags.h) return showCommandHelp('ax approval resolve');
|
|
2419
|
+
|
|
2420
|
+
const id = positional[0];
|
|
2421
|
+
if (!id) die('Missing approval gate ID', 'pmx-canvas ax approval resolve <id> --decision <approved|rejected>');
|
|
2422
|
+
const decision = requireFlag(
|
|
2423
|
+
flags,
|
|
2424
|
+
'decision',
|
|
2425
|
+
'pmx-canvas ax approval resolve <id> --decision <approved|rejected>',
|
|
2426
|
+
);
|
|
2427
|
+
if (decision !== 'approved' && decision !== 'rejected') {
|
|
2428
|
+
die('Invalid decision', 'pmx-canvas ax approval resolve <id> --decision <approved|rejected>');
|
|
2429
|
+
}
|
|
2430
|
+
const resolution = getStringFlag(flags, 'resolution');
|
|
2431
|
+
|
|
2432
|
+
output(
|
|
2433
|
+
await api('POST', `/api/canvas/ax/approval/${encodeURIComponent(id)}/resolve`, {
|
|
2434
|
+
decision,
|
|
2435
|
+
...(resolution ? { resolution } : {}),
|
|
2436
|
+
source: resolveAxSource(flags),
|
|
2437
|
+
}),
|
|
2438
|
+
);
|
|
2439
|
+
},
|
|
2440
|
+
);
|
|
2216
2441
|
|
|
2217
|
-
cmd('ax approval list', 'List canvas-bound AX approval gates', [
|
|
2218
|
-
'pmx-canvas ax approval list',
|
|
2219
|
-
], async (args) => {
|
|
2442
|
+
cmd('ax approval list', 'List canvas-bound AX approval gates', ['pmx-canvas ax approval list'], async (args) => {
|
|
2220
2443
|
const { flags } = parseFlags(args);
|
|
2221
2444
|
if (flags.help || flags.h) return showCommandHelp('ax approval list');
|
|
2222
2445
|
|
|
2223
2446
|
output(await api('GET', '/api/canvas/ax/approval'));
|
|
2224
2447
|
});
|
|
2225
2448
|
|
|
2226
|
-
cmd(
|
|
2227
|
-
'
|
|
2228
|
-
'
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
})
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2449
|
+
cmd(
|
|
2450
|
+
'ax evidence add',
|
|
2451
|
+
'Record an AX evidence item on the timeline',
|
|
2452
|
+
[
|
|
2453
|
+
'pmx-canvas ax evidence add --kind test-output --title "unit pass" --body "..."',
|
|
2454
|
+
'pmx-canvas ax evidence add --kind screenshot --title "before" --ref /tmp/before.png node1',
|
|
2455
|
+
],
|
|
2456
|
+
async (args) => {
|
|
2457
|
+
const { positional, flags } = parseFlags(args);
|
|
2458
|
+
if (flags.help || flags.h) return showCommandHelp('ax evidence add');
|
|
2459
|
+
|
|
2460
|
+
const kind = requireFlag(flags, 'kind', 'pmx-canvas ax evidence add --kind <kind> --title <text>');
|
|
2461
|
+
const title = requireFlag(flags, 'title', 'pmx-canvas ax evidence add --kind <kind> --title <text>');
|
|
2462
|
+
const body = getStringFlag(flags, 'body');
|
|
2463
|
+
const ref = getStringFlag(flags, 'ref');
|
|
2464
|
+
|
|
2465
|
+
output(
|
|
2466
|
+
await api('POST', '/api/canvas/ax/evidence', {
|
|
2467
|
+
kind,
|
|
2468
|
+
title,
|
|
2469
|
+
...(body ? { body } : {}),
|
|
2470
|
+
...(ref ? { ref } : {}),
|
|
2471
|
+
...(positional.length > 0 ? { nodeIds: positional } : {}),
|
|
2472
|
+
source: resolveAxSource(flags),
|
|
2473
|
+
}),
|
|
2474
|
+
);
|
|
2475
|
+
},
|
|
2476
|
+
);
|
|
2477
|
+
|
|
2478
|
+
cmd(
|
|
2479
|
+
'ax review add',
|
|
2480
|
+
'Add a canvas-bound AX review annotation',
|
|
2481
|
+
[
|
|
2482
|
+
'pmx-canvas ax review add --body "needs a test" --node node1',
|
|
2483
|
+
'pmx-canvas ax review add --body "off-by-one" --kind finding --severity error --file src/x.ts',
|
|
2484
|
+
],
|
|
2485
|
+
async (args) => {
|
|
2486
|
+
const { flags } = parseFlags(args);
|
|
2487
|
+
if (flags.help || flags.h) return showCommandHelp('ax review add');
|
|
2488
|
+
|
|
2489
|
+
const body = requireFlag(flags, 'body', 'pmx-canvas ax review add --body <text>');
|
|
2490
|
+
const kind = getStringFlag(flags, 'kind');
|
|
2491
|
+
const severity = getStringFlag(flags, 'severity');
|
|
2492
|
+
const anchorType = getStringFlag(flags, 'anchor');
|
|
2493
|
+
const nodeId = getStringFlag(flags, 'node');
|
|
2494
|
+
const file = getStringFlag(flags, 'file');
|
|
2495
|
+
const author = getStringFlag(flags, 'author');
|
|
2496
|
+
|
|
2497
|
+
output(
|
|
2498
|
+
await api('POST', '/api/canvas/ax/review', {
|
|
2499
|
+
body,
|
|
2500
|
+
...(kind ? { kind } : {}),
|
|
2501
|
+
...(severity ? { severity } : {}),
|
|
2502
|
+
...(anchorType ? { anchorType } : {}),
|
|
2503
|
+
...(nodeId ? { nodeId } : {}),
|
|
2504
|
+
...(file ? { file } : {}),
|
|
2505
|
+
...(author ? { author } : {}),
|
|
2506
|
+
source: resolveAxSource(flags),
|
|
2507
|
+
}),
|
|
2508
|
+
);
|
|
2509
|
+
},
|
|
2510
|
+
);
|
|
2274
2511
|
|
|
2275
|
-
cmd('ax review list', 'List canvas-bound AX review annotations', [
|
|
2276
|
-
'pmx-canvas ax review list',
|
|
2277
|
-
], async (args) => {
|
|
2512
|
+
cmd('ax review list', 'List canvas-bound AX review annotations', ['pmx-canvas ax review list'], async (args) => {
|
|
2278
2513
|
const { flags } = parseFlags(args);
|
|
2279
2514
|
if (flags.help || flags.h) return showCommandHelp('ax review list');
|
|
2280
2515
|
|
|
2281
2516
|
output(await api('GET', '/api/canvas/ax/review'));
|
|
2282
2517
|
});
|
|
2283
2518
|
|
|
2284
|
-
cmd(
|
|
2285
|
-
'
|
|
2286
|
-
'
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2519
|
+
cmd(
|
|
2520
|
+
'ax host report',
|
|
2521
|
+
'Report host/session capability to the canvas',
|
|
2522
|
+
[
|
|
2523
|
+
'pmx-canvas ax host report --host copilot --canvas --tools --session-messaging',
|
|
2524
|
+
'pmx-canvas ax host report --host codex --canvas --files',
|
|
2525
|
+
],
|
|
2526
|
+
async (args) => {
|
|
2527
|
+
const { flags } = parseFlags(args);
|
|
2528
|
+
if (flags.help || flags.h) return showCommandHelp('ax host report');
|
|
2529
|
+
|
|
2530
|
+
const host = getStringFlag(flags, 'host');
|
|
2531
|
+
|
|
2532
|
+
output(
|
|
2533
|
+
await api('PUT', '/api/canvas/ax/host-capability', {
|
|
2534
|
+
...(host ? { host } : {}),
|
|
2535
|
+
canvas: flags.canvas === true,
|
|
2536
|
+
hooks: flags.hooks === true,
|
|
2537
|
+
tools: flags.tools === true,
|
|
2538
|
+
sessionMessaging: flags['session-messaging'] === true,
|
|
2539
|
+
permissions: flags.permissions === true,
|
|
2540
|
+
files: flags.files === true,
|
|
2541
|
+
uiPrompts: flags['ui-prompts'] === true,
|
|
2542
|
+
source: resolveAxSource(flags),
|
|
2543
|
+
}),
|
|
2544
|
+
);
|
|
2545
|
+
},
|
|
2546
|
+
);
|
|
2305
2547
|
|
|
2306
|
-
cmd('ax host status', 'Read the reported host/session capability', [
|
|
2307
|
-
'pmx-canvas ax host status',
|
|
2308
|
-
], async (args) => {
|
|
2548
|
+
cmd('ax host status', 'Read the reported host/session capability', ['pmx-canvas ax host status'], async (args) => {
|
|
2309
2549
|
const { flags } = parseFlags(args);
|
|
2310
2550
|
if (flags.help || flags.h) return showCommandHelp('ax host status');
|
|
2311
2551
|
|
|
@@ -2313,41 +2553,44 @@ cmd('ax host status', 'Read the reported host/session capability', [
|
|
|
2313
2553
|
});
|
|
2314
2554
|
|
|
2315
2555
|
// ── copilot install-extension ────────────────────────────────
|
|
2316
|
-
cmd(
|
|
2317
|
-
'
|
|
2318
|
-
'
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2556
|
+
cmd(
|
|
2557
|
+
'copilot install-extension',
|
|
2558
|
+
'Install the bundled GitHub Copilot extension adapter',
|
|
2559
|
+
[
|
|
2560
|
+
'pmx-canvas copilot install-extension --dry-run',
|
|
2561
|
+
'pmx-canvas copilot install-extension --target .github/extensions/pmx-canvas/extension.mjs --yes',
|
|
2562
|
+
],
|
|
2563
|
+
async (args) => {
|
|
2564
|
+
const { flags } = parseFlags(args);
|
|
2565
|
+
if (flags.help || flags.h) return showCommandHelp('copilot install-extension');
|
|
2566
|
+
|
|
2567
|
+
const sourcePath = fileURLToPath(new URL('../../.github/extensions/pmx-canvas/extension.mjs', import.meta.url));
|
|
2568
|
+
if (!existsSync(sourcePath)) {
|
|
2569
|
+
die('Bundled Copilot extension adapter not found.', `Expected at ${sourcePath}`);
|
|
2570
|
+
}
|
|
2327
2571
|
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2572
|
+
const targetPath =
|
|
2573
|
+
getStringFlag(flags, 'target') ?? join(process.cwd(), '.github', 'extensions', 'pmx-canvas', 'extension.mjs');
|
|
2574
|
+
const dryRun = flags['dry-run'] === true;
|
|
2575
|
+
const targetExists = existsSync(targetPath);
|
|
2332
2576
|
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2577
|
+
if (dryRun) {
|
|
2578
|
+
output({ ok: true, dryRun: true, sourcePath, targetPath, targetExists, wrote: false });
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2337
2581
|
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2582
|
+
if (targetExists && flags.yes !== true) {
|
|
2583
|
+
die('Target already exists. Re-run with --yes to overwrite.', `Target: ${targetPath}`);
|
|
2584
|
+
}
|
|
2341
2585
|
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
}
|
|
2586
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
2587
|
+
copyFileSync(sourcePath, targetPath);
|
|
2588
|
+
output({ ok: true, dryRun: false, sourcePath, targetPath, wrote: true });
|
|
2589
|
+
},
|
|
2590
|
+
);
|
|
2346
2591
|
|
|
2347
2592
|
// ── undo ─────────────────────────────────────────────────────
|
|
2348
|
-
cmd('undo', 'Undo the last canvas mutation', [
|
|
2349
|
-
'pmx-canvas undo',
|
|
2350
|
-
], async (args) => {
|
|
2593
|
+
cmd('undo', 'Undo the last canvas mutation', ['pmx-canvas undo'], async (args) => {
|
|
2351
2594
|
const { flags } = parseFlags(args);
|
|
2352
2595
|
if (flags.help || flags.h) return showCommandHelp('undo');
|
|
2353
2596
|
|
|
@@ -2356,9 +2599,7 @@ cmd('undo', 'Undo the last canvas mutation', [
|
|
|
2356
2599
|
});
|
|
2357
2600
|
|
|
2358
2601
|
// ── redo ─────────────────────────────────────────────────────
|
|
2359
|
-
cmd('redo', 'Redo the last undone mutation', [
|
|
2360
|
-
'pmx-canvas redo',
|
|
2361
|
-
], async (args) => {
|
|
2602
|
+
cmd('redo', 'Redo the last undone mutation', ['pmx-canvas redo'], async (args) => {
|
|
2362
2603
|
const { flags } = parseFlags(args);
|
|
2363
2604
|
if (flags.help || flags.h) return showCommandHelp('redo');
|
|
2364
2605
|
|
|
@@ -2367,101 +2608,112 @@ cmd('redo', 'Redo the last undone mutation', [
|
|
|
2367
2608
|
});
|
|
2368
2609
|
|
|
2369
2610
|
// ── history ──────────────────────────────────────────────────
|
|
2370
|
-
cmd(
|
|
2371
|
-
'
|
|
2372
|
-
'
|
|
2373
|
-
'pmx-canvas history --compact',
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
}
|
|
2611
|
+
cmd(
|
|
2612
|
+
'history',
|
|
2613
|
+
'Show canvas mutation history',
|
|
2614
|
+
['pmx-canvas history', 'pmx-canvas history --summary', 'pmx-canvas history --compact'],
|
|
2615
|
+
async (args) => {
|
|
2616
|
+
const { flags } = parseFlags(args);
|
|
2617
|
+
if (flags.help || flags.h) return showCommandHelp('history');
|
|
2618
|
+
|
|
2619
|
+
const result = (await invokeOperation('history.get', {})) as Record<string, unknown>;
|
|
2620
|
+
if (flags.summary) {
|
|
2621
|
+
output(summarizeHistoryResult(result));
|
|
2622
|
+
return;
|
|
2623
|
+
}
|
|
2624
|
+
if (flags.compact) {
|
|
2625
|
+
output(compactHistoryResult(result));
|
|
2626
|
+
return;
|
|
2627
|
+
}
|
|
2628
|
+
output(result);
|
|
2629
|
+
},
|
|
2630
|
+
);
|
|
2389
2631
|
|
|
2390
2632
|
// ── snapshot save ────────────────────────────────────────────
|
|
2391
|
-
cmd(
|
|
2392
|
-
'
|
|
2393
|
-
'
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2633
|
+
cmd(
|
|
2634
|
+
'snapshot save',
|
|
2635
|
+
'Save a named snapshot of the current canvas',
|
|
2636
|
+
['pmx-canvas snapshot save --name "before-refactor"', 'pmx-canvas snapshot save --name checkpoint-1'],
|
|
2637
|
+
async (args) => {
|
|
2638
|
+
const { flags } = parseFlags(args);
|
|
2639
|
+
if (flags.help || flags.h) return showCommandHelp('snapshot save');
|
|
2640
|
+
|
|
2641
|
+
const name = requireFlag(flags, 'name', 'pmx-canvas snapshot save --name "my-snapshot"');
|
|
2642
|
+
const result = await invokeOperation('snapshot.save', { name });
|
|
2643
|
+
output(result);
|
|
2644
|
+
},
|
|
2645
|
+
);
|
|
2402
2646
|
|
|
2403
2647
|
// ── snapshot list ────────────────────────────────────────────
|
|
2404
|
-
cmd(
|
|
2405
|
-
'
|
|
2406
|
-
'
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
})
|
|
2648
|
+
cmd(
|
|
2649
|
+
'snapshot list',
|
|
2650
|
+
'List saved snapshots',
|
|
2651
|
+
[
|
|
2652
|
+
'pmx-canvas snapshot list',
|
|
2653
|
+
'pmx-canvas snapshot list --limit 50 --query baseline',
|
|
2654
|
+
'pmx-canvas snapshot list --after 2026-05-01T00:00:00Z --before 2026-05-05T00:00:00Z',
|
|
2655
|
+
'pmx-canvas snapshot list --all',
|
|
2656
|
+
],
|
|
2657
|
+
async (args) => {
|
|
2658
|
+
const { flags } = parseFlags(args);
|
|
2659
|
+
if (flags.help || flags.h) return showCommandHelp('snapshot list');
|
|
2660
|
+
|
|
2661
|
+
const limit = optionalNumberFlag(flags, 'limit', 'Use a positive integer, e.g. --limit 50');
|
|
2662
|
+
const query = getStringFlag(flags, 'query', 'q');
|
|
2663
|
+
const before = getStringFlag(flags, 'before');
|
|
2664
|
+
const after = getStringFlag(flags, 'after');
|
|
2665
|
+
const result = await invokeOperation('snapshot.list', {
|
|
2666
|
+
...(limit !== undefined ? { limit } : {}),
|
|
2667
|
+
...(query ? { q: query } : {}),
|
|
2668
|
+
...(before ? { before } : {}),
|
|
2669
|
+
...(after ? { after } : {}),
|
|
2670
|
+
...(flags.all ? { all: true } : {}),
|
|
2671
|
+
});
|
|
2672
|
+
output(result);
|
|
2673
|
+
},
|
|
2674
|
+
);
|
|
2426
2675
|
|
|
2427
2676
|
// ── snapshot gc ──────────────────────────────────────────────
|
|
2428
|
-
cmd(
|
|
2429
|
-
'
|
|
2430
|
-
'
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2677
|
+
cmd(
|
|
2678
|
+
'snapshot gc',
|
|
2679
|
+
'Delete old snapshots, keeping the newest N',
|
|
2680
|
+
['pmx-canvas snapshot gc --keep 20 --dry-run', 'pmx-canvas snapshot gc --keep 50 --yes'],
|
|
2681
|
+
async (args) => {
|
|
2682
|
+
const { flags } = parseFlags(args);
|
|
2683
|
+
if (flags.help || flags.h) return showCommandHelp('snapshot gc');
|
|
2684
|
+
|
|
2685
|
+
const keep = optionalNumberFlag(flags, 'keep', 'Use a positive integer, e.g. --keep 20');
|
|
2686
|
+
const dryRun = flags['dry-run'] === true;
|
|
2687
|
+
if (!dryRun && !flags.yes) {
|
|
2688
|
+
die('Destructive operation requires --yes flag', 'Preview with: pmx-canvas snapshot gc --keep 20 --dry-run');
|
|
2689
|
+
}
|
|
2690
|
+
const result = await invokeOperation('snapshot.gc', {
|
|
2691
|
+
...(keep !== undefined ? { keep } : {}),
|
|
2692
|
+
dryRun,
|
|
2693
|
+
});
|
|
2694
|
+
output(result);
|
|
2695
|
+
},
|
|
2696
|
+
);
|
|
2446
2697
|
|
|
2447
2698
|
// ── snapshot restore ─────────────────────────────────────────
|
|
2448
|
-
cmd(
|
|
2449
|
-
'
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
});
|
|
2699
|
+
cmd(
|
|
2700
|
+
'snapshot restore',
|
|
2701
|
+
'Restore canvas from a snapshot',
|
|
2702
|
+
['pmx-canvas snapshot restore <snapshot-id-or-name>'],
|
|
2703
|
+
async (args) => {
|
|
2704
|
+
const { positional, flags } = parseFlags(args);
|
|
2705
|
+
if (flags.help || flags.h) return showCommandHelp('snapshot restore');
|
|
2706
|
+
|
|
2707
|
+
const id = positional[0];
|
|
2708
|
+
if (!id) die('Missing snapshot ID or name', 'pmx-canvas snapshot restore <snapshot-id-or-name>');
|
|
2709
|
+
|
|
2710
|
+
const result = await invokeOperation('snapshot.restore', { id });
|
|
2711
|
+
output(result);
|
|
2712
|
+
},
|
|
2713
|
+
);
|
|
2460
2714
|
|
|
2461
2715
|
// ── snapshot delete ──────────────────────────────────────────
|
|
2462
|
-
cmd('snapshot delete', 'Delete a saved snapshot', [
|
|
2463
|
-
'pmx-canvas snapshot delete <snapshot-id>',
|
|
2464
|
-
], async (args) => {
|
|
2716
|
+
cmd('snapshot delete', 'Delete a saved snapshot', ['pmx-canvas snapshot delete <snapshot-id>'], async (args) => {
|
|
2465
2717
|
const { positional, flags } = parseFlags(args);
|
|
2466
2718
|
if (flags.help || flags.h) return showCommandHelp('snapshot delete');
|
|
2467
2719
|
|
|
@@ -2481,171 +2733,196 @@ async function runSnapshotDiff(args: string[]): Promise<void> {
|
|
|
2481
2733
|
}
|
|
2482
2734
|
|
|
2483
2735
|
// ── snapshot diff ────────────────────────────────────────────
|
|
2484
|
-
cmd(
|
|
2485
|
-
'
|
|
2486
|
-
'
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2736
|
+
cmd(
|
|
2737
|
+
'snapshot diff',
|
|
2738
|
+
'Compare current canvas against a saved snapshot',
|
|
2739
|
+
['pmx-canvas snapshot diff <snapshot-id>', 'pmx-canvas snapshot diff "before-refactor"'],
|
|
2740
|
+
async (args) => {
|
|
2741
|
+
const { flags } = parseFlags(args);
|
|
2742
|
+
if (flags.help || flags.h) return showCommandHelp('snapshot diff');
|
|
2743
|
+
await runSnapshotDiff(args);
|
|
2744
|
+
},
|
|
2745
|
+
);
|
|
2492
2746
|
|
|
2493
2747
|
// ── diff ─────────────────────────────────────────────────────
|
|
2494
|
-
cmd('diff', 'Compare current canvas against a snapshot', [
|
|
2495
|
-
'pmx-canvas diff <snapshot-id>',
|
|
2496
|
-
], async (args) => {
|
|
2748
|
+
cmd('diff', 'Compare current canvas against a snapshot', ['pmx-canvas diff <snapshot-id>'], async (args) => {
|
|
2497
2749
|
const { flags } = parseFlags(args);
|
|
2498
2750
|
if (flags.help || flags.h) return showCommandHelp('diff');
|
|
2499
2751
|
await runSnapshotDiff(args);
|
|
2500
2752
|
});
|
|
2501
2753
|
|
|
2502
2754
|
// ── group create ─────────────────────────────────────────────
|
|
2503
|
-
cmd(
|
|
2504
|
-
'
|
|
2505
|
-
'
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2755
|
+
cmd(
|
|
2756
|
+
'group create',
|
|
2757
|
+
'Create a group node',
|
|
2758
|
+
[
|
|
2759
|
+
'pmx-canvas group create --title "API Layer" node1 node2',
|
|
2760
|
+
'pmx-canvas group create --title "Quarterly board" --x 40 --y 60 --width 1600 --height 900 --child-layout column node1 node2',
|
|
2761
|
+
'pmx-canvas group create --title "Frontend" --color "#ff6b6b"',
|
|
2762
|
+
],
|
|
2763
|
+
async (args) => {
|
|
2764
|
+
const { positional, flags } = parseFlags(args);
|
|
2765
|
+
if (flags.help || flags.h) return showCommandHelp('group create');
|
|
2766
|
+
|
|
2767
|
+
const body: Record<string, unknown> = {};
|
|
2768
|
+
if (flags.title && flags.title !== true) body.title = flags.title;
|
|
2769
|
+
if (flags.color && flags.color !== true) body.color = flags.color;
|
|
2770
|
+
const x = optionalFiniteFlag(flags, 'x', 'Use a finite number, e.g. --x 40');
|
|
2771
|
+
const y = optionalFiniteFlag(flags, 'y', 'Use a finite number, e.g. --y 60');
|
|
2772
|
+
const width = optionalPositiveFiniteFlag(flags, 'width', 'Use a positive number, e.g. --width 1600');
|
|
2773
|
+
const height = optionalPositiveFiniteFlag(flags, 'height', 'Use a positive number, e.g. --height 900');
|
|
2774
|
+
if (x !== undefined) body.x = x;
|
|
2775
|
+
if (y !== undefined) body.y = y;
|
|
2776
|
+
if (width !== undefined) body.width = width;
|
|
2777
|
+
if (height !== undefined) body.height = height;
|
|
2778
|
+
if (typeof flags['child-layout'] === 'string') body.childLayout = flags['child-layout'];
|
|
2779
|
+
if (positional.length > 0) body.childIds = positional;
|
|
2780
|
+
|
|
2781
|
+
const result = await api('POST', '/api/canvas/group', body);
|
|
2782
|
+
output(result);
|
|
2783
|
+
},
|
|
2784
|
+
);
|
|
2528
2785
|
|
|
2529
2786
|
// ── group add ────────────────────────────────────────────────
|
|
2530
|
-
cmd(
|
|
2531
|
-
'
|
|
2532
|
-
'
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
groupId,
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2787
|
+
cmd(
|
|
2788
|
+
'group add',
|
|
2789
|
+
'Add nodes to an existing group',
|
|
2790
|
+
[
|
|
2791
|
+
'pmx-canvas group add --group <group-id> node1 node2',
|
|
2792
|
+
'pmx-canvas group add --group <group-id> --child-layout flow node1 node2',
|
|
2793
|
+
],
|
|
2794
|
+
async (args) => {
|
|
2795
|
+
const { positional, flags } = parseFlags(args);
|
|
2796
|
+
if (flags.help || flags.h) return showCommandHelp('group add');
|
|
2797
|
+
|
|
2798
|
+
const groupId = requireFlag(flags, 'group', 'pmx-canvas group add --group <group-id> node1 node2');
|
|
2799
|
+
if (positional.length === 0) die('No node IDs provided', 'pmx-canvas group add --group <group-id> node1 node2');
|
|
2800
|
+
|
|
2801
|
+
const result = await api('POST', '/api/canvas/group/add', {
|
|
2802
|
+
groupId,
|
|
2803
|
+
childIds: positional,
|
|
2804
|
+
...(typeof flags['child-layout'] === 'string' ? { childLayout: flags['child-layout'] } : {}),
|
|
2805
|
+
});
|
|
2806
|
+
output(result);
|
|
2807
|
+
},
|
|
2808
|
+
);
|
|
2547
2809
|
|
|
2548
2810
|
// ── batch ────────────────────────────────────────────────────
|
|
2549
|
-
cmd(
|
|
2550
|
-
'
|
|
2551
|
-
'
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2811
|
+
cmd(
|
|
2812
|
+
'batch',
|
|
2813
|
+
'Run a batch of canvas operations from JSON',
|
|
2814
|
+
[
|
|
2815
|
+
'pmx-canvas batch --file ./canvas-ops.json',
|
|
2816
|
+
'pmx-canvas batch --json \'[{"op":"node.add","assign":"a","args":{"type":"markdown","title":"A"}}]\'',
|
|
2817
|
+
'pmx-canvas batch --json \'[{"op":"graph.add","assign":"g","args":{"graphType":"bar","data":[{"label":"Docs","value":5}],"xKey":"label","yKey":"value"}}]\'',
|
|
2818
|
+
'cat ops.json | pmx-canvas batch --stdin',
|
|
2819
|
+
],
|
|
2820
|
+
async (args) => {
|
|
2821
|
+
const { flags } = parseFlags(args);
|
|
2822
|
+
if (flags.help || flags.h) return showCommandHelp('batch');
|
|
2823
|
+
|
|
2824
|
+
let raw = '';
|
|
2825
|
+
if (typeof flags.file === 'string') {
|
|
2826
|
+
try {
|
|
2827
|
+
raw = readFileSync(flags.file, 'utf-8');
|
|
2828
|
+
} catch (error) {
|
|
2829
|
+
die(
|
|
2830
|
+
`Unable to read --file: ${error instanceof Error ? error.message : String(error)}`,
|
|
2831
|
+
'Use: pmx-canvas batch --file ./canvas-ops.json',
|
|
2832
|
+
);
|
|
2833
|
+
}
|
|
2834
|
+
} else if (typeof flags.json === 'string') {
|
|
2835
|
+
raw = flags.json;
|
|
2836
|
+
} else if (flags.stdin) {
|
|
2837
|
+
raw = await readStdin();
|
|
2838
|
+
} else {
|
|
2839
|
+
die('Batch operations require --file, --json, or --stdin.', 'Use: pmx-canvas batch --file ./canvas-ops.json');
|
|
2840
|
+
}
|
|
2557
2841
|
|
|
2558
|
-
|
|
2559
|
-
if (typeof flags.file === 'string') {
|
|
2842
|
+
let parsed: unknown;
|
|
2560
2843
|
try {
|
|
2561
|
-
|
|
2844
|
+
parsed = JSON.parse(raw);
|
|
2562
2845
|
} catch (error) {
|
|
2563
2846
|
die(
|
|
2564
|
-
`
|
|
2565
|
-
'Use
|
|
2847
|
+
`Invalid batch JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
2848
|
+
'Use a JSON array of operations or an object with an "operations" array.',
|
|
2566
2849
|
);
|
|
2567
2850
|
}
|
|
2568
|
-
} else if (typeof flags.json === 'string') {
|
|
2569
|
-
raw = flags.json;
|
|
2570
|
-
} else if (flags.stdin) {
|
|
2571
|
-
raw = await readStdin();
|
|
2572
|
-
} else {
|
|
2573
|
-
die(
|
|
2574
|
-
'Batch operations require --file, --json, or --stdin.',
|
|
2575
|
-
'Use: pmx-canvas batch --file ./canvas-ops.json',
|
|
2576
|
-
);
|
|
2577
|
-
}
|
|
2578
2851
|
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
die(
|
|
2584
|
-
`Invalid batch JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
2585
|
-
'Use a JSON array of operations or an object with an "operations" array.',
|
|
2852
|
+
const result = await api(
|
|
2853
|
+
'POST',
|
|
2854
|
+
'/api/canvas/batch',
|
|
2855
|
+
Array.isArray(parsed) ? { operations: parsed } : (parsed as Record<string, unknown>),
|
|
2586
2856
|
);
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
output(result);
|
|
2591
|
-
});
|
|
2857
|
+
output(result);
|
|
2858
|
+
},
|
|
2859
|
+
);
|
|
2592
2860
|
|
|
2593
2861
|
// ── validate ─────────────────────────────────────────────────
|
|
2594
|
-
cmd(
|
|
2595
|
-
'
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
'
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2862
|
+
cmd(
|
|
2863
|
+
'validate',
|
|
2864
|
+
'Validate the current layout for collisions and missing edge endpoints',
|
|
2865
|
+
['pmx-canvas validate'],
|
|
2866
|
+
async (args) => {
|
|
2867
|
+
const { flags } = parseFlags(args);
|
|
2868
|
+
if (flags.help || flags.h) return showCommandHelp('validate');
|
|
2869
|
+
|
|
2870
|
+
const result = await api('GET', '/api/canvas/validate');
|
|
2871
|
+
output(result);
|
|
2872
|
+
},
|
|
2873
|
+
);
|
|
2874
|
+
|
|
2875
|
+
cmd(
|
|
2876
|
+
'validate spec',
|
|
2877
|
+
'Validate a json-render spec or graph payload without creating a node',
|
|
2878
|
+
[
|
|
2879
|
+
'pmx-canvas validate spec --type json-render --spec-file ./dashboard.json',
|
|
2880
|
+
'pmx-canvas validate spec --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
|
|
2881
|
+
'pmx-canvas validate spec --type html-primitive --kind choice-grid --data-file ./options.json',
|
|
2882
|
+
'pmx-canvas validate spec --type json-render --spec-file ./dashboard.json --summary',
|
|
2883
|
+
],
|
|
2884
|
+
async (args) => {
|
|
2885
|
+
const { flags } = parseFlags(args);
|
|
2886
|
+
if (flags.help || flags.h) return showCommandHelp('validate spec');
|
|
2887
|
+
|
|
2888
|
+
const type = getStringFlag(flags, 'type');
|
|
2889
|
+
if (type !== 'json-render' && type !== 'graph' && type !== 'html-primitive') {
|
|
2890
|
+
die('validate spec requires --type json-render, --type graph, or --type html-primitive.');
|
|
2891
|
+
}
|
|
2617
2892
|
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2893
|
+
let body: Record<string, unknown>;
|
|
2894
|
+
if (type === 'json-render') {
|
|
2895
|
+
body = {
|
|
2896
|
+
type,
|
|
2897
|
+
spec: (await buildJsonRenderRequestBody({ ...flags, title: String(flags.title ?? 'Validation') })).spec,
|
|
2898
|
+
};
|
|
2899
|
+
} else if (type === 'html-primitive') {
|
|
2900
|
+
const primitiveBody = await buildHtmlPrimitiveRequestBody(flags);
|
|
2901
|
+
body = {
|
|
2902
|
+
type,
|
|
2903
|
+
kind: primitiveBody.primitive,
|
|
2904
|
+
...(typeof primitiveBody.title === 'string' ? { title: primitiveBody.title } : {}),
|
|
2905
|
+
...(isRecord(primitiveBody.data) ? { data: primitiveBody.data } : {}),
|
|
2906
|
+
};
|
|
2907
|
+
} else {
|
|
2908
|
+
body = { type, ...(await buildGraphRequestBody(flags)) };
|
|
2909
|
+
}
|
|
2632
2910
|
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
}
|
|
2911
|
+
const result = (await api('POST', '/api/canvas/schema/validate', body)) as Record<string, unknown>;
|
|
2912
|
+
if (flags.summary) {
|
|
2913
|
+
output({
|
|
2914
|
+
ok: result.ok,
|
|
2915
|
+
type: result.type,
|
|
2916
|
+
summary: result.summary,
|
|
2917
|
+
});
|
|
2918
|
+
return;
|
|
2919
|
+
}
|
|
2920
|
+
output(result);
|
|
2921
|
+
},
|
|
2922
|
+
);
|
|
2644
2923
|
|
|
2645
2924
|
// ── group remove ─────────────────────────────────────────────
|
|
2646
|
-
cmd('group remove', 'Ungroup all children from a group', [
|
|
2647
|
-
'pmx-canvas group remove <group-id>',
|
|
2648
|
-
], async (args) => {
|
|
2925
|
+
cmd('group remove', 'Ungroup all children from a group', ['pmx-canvas group remove <group-id>'], async (args) => {
|
|
2649
2926
|
const { positional, flags } = parseFlags(args);
|
|
2650
2927
|
if (flags.help || flags.h) return showCommandHelp('group remove');
|
|
2651
2928
|
|
|
@@ -2657,46 +2934,51 @@ cmd('group remove', 'Ungroup all children from a group', [
|
|
|
2657
2934
|
});
|
|
2658
2935
|
|
|
2659
2936
|
// ── web-artifact build ───────────────────────────────────────
|
|
2660
|
-
cmd(
|
|
2661
|
-
'
|
|
2662
|
-
'
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2937
|
+
cmd(
|
|
2938
|
+
'web-artifact build',
|
|
2939
|
+
'Build a bundled HTML web artifact and optionally open it on the canvas',
|
|
2940
|
+
[
|
|
2941
|
+
'pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx',
|
|
2942
|
+
'pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx --index-css-file ./index.css',
|
|
2943
|
+
'pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx --include-logs',
|
|
2944
|
+
],
|
|
2945
|
+
async (args) => {
|
|
2946
|
+
const { flags } = parseFlags(args);
|
|
2947
|
+
if (flags.help || flags.h) return showCommandHelp('web-artifact build');
|
|
2948
|
+
await runWebArtifactBuildCommand(flags);
|
|
2949
|
+
},
|
|
2950
|
+
);
|
|
2669
2951
|
|
|
2670
2952
|
// ── clear ────────────────────────────────────────────────────
|
|
2671
|
-
cmd(
|
|
2672
|
-
'
|
|
2673
|
-
'
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2953
|
+
cmd(
|
|
2954
|
+
'clear',
|
|
2955
|
+
'Remove all nodes and edges from the canvas',
|
|
2956
|
+
['pmx-canvas clear --yes', 'pmx-canvas clear --dry-run'],
|
|
2957
|
+
async (args) => {
|
|
2958
|
+
const { flags } = parseFlags(args);
|
|
2959
|
+
if (flags.help || flags.h) return showCommandHelp('clear');
|
|
2960
|
+
|
|
2961
|
+
if (flags['dry-run']) {
|
|
2962
|
+
const layout = (await api('GET', '/api/canvas/state')) as { nodes: unknown[]; edges: unknown[] };
|
|
2963
|
+
output({
|
|
2964
|
+
dry_run: true,
|
|
2965
|
+
would_remove: { nodes: layout.nodes.length, edges: layout.edges.length },
|
|
2966
|
+
message: 'No changes made. Pass --yes to confirm.',
|
|
2967
|
+
});
|
|
2968
|
+
return;
|
|
2969
|
+
}
|
|
2687
2970
|
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2971
|
+
if (!flags.yes) {
|
|
2972
|
+
die('Destructive operation requires --yes flag', 'pmx-canvas clear --yes (or preview with --dry-run)');
|
|
2973
|
+
}
|
|
2691
2974
|
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
}
|
|
2975
|
+
const result = await api('POST', '/api/canvas/clear');
|
|
2976
|
+
output(result);
|
|
2977
|
+
},
|
|
2978
|
+
);
|
|
2695
2979
|
|
|
2696
2980
|
// ── webview status ────────────────────────────────────────────
|
|
2697
|
-
cmd('webview status', 'Show Bun.WebView automation status', [
|
|
2698
|
-
'pmx-canvas webview status',
|
|
2699
|
-
], async (args) => {
|
|
2981
|
+
cmd('webview status', 'Show Bun.WebView automation status', ['pmx-canvas webview status'], async (args) => {
|
|
2700
2982
|
const { flags } = parseFlags(args);
|
|
2701
2983
|
if (flags.help || flags.h) return showCommandHelp('webview status');
|
|
2702
2984
|
|
|
@@ -2705,51 +2987,54 @@ cmd('webview status', 'Show Bun.WebView automation status', [
|
|
|
2705
2987
|
});
|
|
2706
2988
|
|
|
2707
2989
|
// ── webview start ─────────────────────────────────────────────
|
|
2708
|
-
cmd(
|
|
2709
|
-
'
|
|
2710
|
-
'
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2990
|
+
cmd(
|
|
2991
|
+
'webview start',
|
|
2992
|
+
'Start or replace the Bun.WebView automation session',
|
|
2993
|
+
[
|
|
2994
|
+
'pmx-canvas webview start',
|
|
2995
|
+
'pmx-canvas webview start --backend chrome --width 1440 --height 900',
|
|
2996
|
+
'pmx-canvas webview start --chrome-path /Applications/Google\\ Chrome.app/.../Google\\ Chrome',
|
|
2997
|
+
],
|
|
2998
|
+
async (args) => {
|
|
2999
|
+
const { flags } = parseFlags(args);
|
|
3000
|
+
if (flags.help || flags.h) return showCommandHelp('webview start');
|
|
3001
|
+
|
|
3002
|
+
const backend = flags.backend;
|
|
3003
|
+
if (backend && backend !== true && backend !== 'chrome' && backend !== 'webkit') {
|
|
3004
|
+
die('Invalid value for --backend', 'Use: --backend chrome or --backend webkit');
|
|
3005
|
+
}
|
|
2720
3006
|
|
|
2721
|
-
|
|
2722
|
-
|
|
3007
|
+
const body: Record<string, unknown> = {};
|
|
3008
|
+
if (backend && backend !== true) body.backend = backend;
|
|
2723
3009
|
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
3010
|
+
const width = optionalNumberFlag(flags, 'width', 'Use a positive integer width, e.g. --width 1440');
|
|
3011
|
+
const height = optionalNumberFlag(flags, 'height', 'Use a positive integer height, e.g. --height 900');
|
|
3012
|
+
if (width !== undefined) body.width = width;
|
|
3013
|
+
if (height !== undefined) body.height = height;
|
|
2728
3014
|
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
3015
|
+
if (flags['chrome-path'] && flags['chrome-path'] !== true) {
|
|
3016
|
+
body.chromePath = flags['chrome-path'];
|
|
3017
|
+
}
|
|
2732
3018
|
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
3019
|
+
if (flags['data-dir'] && flags['data-dir'] !== true) {
|
|
3020
|
+
body.dataStoreDir = flags['data-dir'];
|
|
3021
|
+
}
|
|
2736
3022
|
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
3023
|
+
if (flags['chrome-argv'] && flags['chrome-argv'] !== true) {
|
|
3024
|
+
const chromeArgv = String(flags['chrome-argv'])
|
|
3025
|
+
.split(',')
|
|
3026
|
+
.map((value) => value.trim())
|
|
3027
|
+
.filter((value) => value.length > 0);
|
|
3028
|
+
if (chromeArgv.length > 0) body.chromeArgv = chromeArgv;
|
|
3029
|
+
}
|
|
2744
3030
|
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
}
|
|
3031
|
+
const result = await api('POST', '/api/workbench/webview/start', body, { allowErrorJson: true });
|
|
3032
|
+
output(result);
|
|
3033
|
+
},
|
|
3034
|
+
);
|
|
2748
3035
|
|
|
2749
3036
|
// ── webview stop ──────────────────────────────────────────────
|
|
2750
|
-
cmd('webview stop', 'Stop the active Bun.WebView automation session', [
|
|
2751
|
-
'pmx-canvas webview stop',
|
|
2752
|
-
], async (args) => {
|
|
3037
|
+
cmd('webview stop', 'Stop the active Bun.WebView automation session', ['pmx-canvas webview stop'], async (args) => {
|
|
2753
3038
|
const { flags } = parseFlags(args);
|
|
2754
3039
|
if (flags.help || flags.h) return showCommandHelp('webview stop');
|
|
2755
3040
|
|
|
@@ -2758,137 +3043,148 @@ cmd('webview stop', 'Stop the active Bun.WebView automation session', [
|
|
|
2758
3043
|
});
|
|
2759
3044
|
|
|
2760
3045
|
// ── webview evaluate ──────────────────────────────────────────
|
|
2761
|
-
cmd(
|
|
2762
|
-
'
|
|
2763
|
-
'
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
if (typeof flags.file === 'string') {
|
|
2776
|
-
let script = '';
|
|
2777
|
-
try {
|
|
2778
|
-
script = readFileSync(flags.file, 'utf-8');
|
|
2779
|
-
} catch (error) {
|
|
3046
|
+
cmd(
|
|
3047
|
+
'webview evaluate',
|
|
3048
|
+
'Evaluate JavaScript in the active Bun.WebView automation session',
|
|
3049
|
+
[
|
|
3050
|
+
'pmx-canvas webview evaluate --expression "document.title"',
|
|
3051
|
+
'pmx-canvas webview evaluate --script "const title = document.title; return title.toUpperCase()"',
|
|
3052
|
+
'pmx-canvas webview evaluate --file ./probe.js',
|
|
3053
|
+
],
|
|
3054
|
+
async (args) => {
|
|
3055
|
+
const { flags } = parseFlags(args);
|
|
3056
|
+
if (flags.help || flags.h) return showCommandHelp('webview evaluate');
|
|
3057
|
+
|
|
3058
|
+
const sourceCount = [flags.expression, flags.script, flags.file].filter(Boolean).length;
|
|
3059
|
+
if (sourceCount > 1) {
|
|
2780
3060
|
die(
|
|
2781
|
-
|
|
2782
|
-
'pmx-canvas webview evaluate --
|
|
3061
|
+
'Use only one of --expression, --script, or --file.',
|
|
3062
|
+
'pmx-canvas webview evaluate --expression "document.title"',
|
|
2783
3063
|
);
|
|
2784
3064
|
}
|
|
2785
|
-
expression = wrapCanvasAutomationScript(script);
|
|
2786
|
-
} else if (typeof flags.script === 'string') {
|
|
2787
|
-
expression = wrapCanvasAutomationScript(flags.script);
|
|
2788
|
-
} else {
|
|
2789
|
-
expression = requireFlag(
|
|
2790
|
-
flags,
|
|
2791
|
-
'expression',
|
|
2792
|
-
'pmx-canvas webview evaluate --expression "document.title"',
|
|
2793
|
-
);
|
|
2794
|
-
}
|
|
2795
3065
|
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
3066
|
+
let expression = '';
|
|
3067
|
+
if (typeof flags.file === 'string') {
|
|
3068
|
+
let script = '';
|
|
3069
|
+
try {
|
|
3070
|
+
script = readFileSync(flags.file, 'utf-8');
|
|
3071
|
+
} catch (error) {
|
|
3072
|
+
die(
|
|
3073
|
+
`Unable to read --file: ${error instanceof Error ? error.message : String(error)}`,
|
|
3074
|
+
'pmx-canvas webview evaluate --file ./probe.js',
|
|
3075
|
+
);
|
|
3076
|
+
}
|
|
3077
|
+
expression = wrapCanvasAutomationScript(script);
|
|
3078
|
+
} else if (typeof flags.script === 'string') {
|
|
3079
|
+
expression = wrapCanvasAutomationScript(flags.script);
|
|
3080
|
+
} else {
|
|
3081
|
+
expression = requireFlag(flags, 'expression', 'pmx-canvas webview evaluate --expression "document.title"');
|
|
3082
|
+
}
|
|
2799
3083
|
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
const { flags } = parseFlags(args);
|
|
2805
|
-
if (flags.help || flags.h) return showCommandHelp('webview resize');
|
|
3084
|
+
const result = await api('POST', '/api/workbench/webview/evaluate', { expression });
|
|
3085
|
+
output(result);
|
|
3086
|
+
},
|
|
3087
|
+
);
|
|
2806
3088
|
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
3089
|
+
// ── webview resize ────────────────────────────────────────────
|
|
3090
|
+
cmd(
|
|
3091
|
+
'webview resize',
|
|
3092
|
+
'Resize the active Bun.WebView automation session viewport',
|
|
3093
|
+
['pmx-canvas webview resize --width 1280 --height 800'],
|
|
3094
|
+
async (args) => {
|
|
3095
|
+
const { flags } = parseFlags(args);
|
|
3096
|
+
if (flags.help || flags.h) return showCommandHelp('webview resize');
|
|
3097
|
+
|
|
3098
|
+
const width = optionalNumberFlag(flags, 'width', 'Use: pmx-canvas webview resize --width 1280 --height 800');
|
|
3099
|
+
const height = optionalNumberFlag(flags, 'height', 'Use: pmx-canvas webview resize --width 1280 --height 800');
|
|
3100
|
+
if (width === undefined || height === undefined) {
|
|
3101
|
+
die('Missing required flags: --width, --height', 'Use: pmx-canvas webview resize --width 1280 --height 800');
|
|
3102
|
+
}
|
|
2812
3103
|
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
}
|
|
3104
|
+
const result = await api('POST', '/api/workbench/webview/resize', { width, height });
|
|
3105
|
+
output(result);
|
|
3106
|
+
},
|
|
3107
|
+
);
|
|
2816
3108
|
|
|
2817
3109
|
// ── webview screenshot ────────────────────────────────────────
|
|
2818
|
-
cmd(
|
|
2819
|
-
'
|
|
2820
|
-
'
|
|
2821
|
-
|
|
2822
|
-
const { flags } = parseFlags(args);
|
|
2823
|
-
if (flags.help || flags.h) return showCommandHelp('webview screenshot');
|
|
2824
|
-
|
|
2825
|
-
const outputPath = requireFlag(
|
|
2826
|
-
flags,
|
|
2827
|
-
'output',
|
|
3110
|
+
cmd(
|
|
3111
|
+
'webview screenshot',
|
|
3112
|
+
'Capture a screenshot from the active Bun.WebView automation session',
|
|
3113
|
+
[
|
|
2828
3114
|
'pmx-canvas webview screenshot --output ./canvas.png',
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
3115
|
+
'pmx-canvas webview screenshot --output ./canvas.webp --format webp --quality 80',
|
|
3116
|
+
],
|
|
3117
|
+
async (args) => {
|
|
3118
|
+
const { flags } = parseFlags(args);
|
|
3119
|
+
if (flags.help || flags.h) return showCommandHelp('webview screenshot');
|
|
3120
|
+
|
|
3121
|
+
const outputPath = requireFlag(flags, 'output', 'pmx-canvas webview screenshot --output ./canvas.png');
|
|
3122
|
+
|
|
3123
|
+
const body: Record<string, unknown> = {};
|
|
3124
|
+
if (flags.format && flags.format !== true) {
|
|
3125
|
+
const format = String(flags.format);
|
|
3126
|
+
if (format !== 'png' && format !== 'jpeg' && format !== 'webp') {
|
|
3127
|
+
die('Invalid value for --format', 'Use: --format png, jpeg, or webp');
|
|
3128
|
+
}
|
|
3129
|
+
body.format = format;
|
|
2836
3130
|
}
|
|
2837
|
-
body.format = format;
|
|
2838
|
-
}
|
|
2839
3131
|
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
3132
|
+
if (flags.quality && flags.quality !== true) {
|
|
3133
|
+
const quality = Number(flags.quality);
|
|
3134
|
+
if (!Number.isFinite(quality)) {
|
|
3135
|
+
die(`Invalid value for --quality: ${String(flags.quality)}`, 'Use a numeric quality, e.g. --quality 80');
|
|
3136
|
+
}
|
|
3137
|
+
body.quality = quality;
|
|
2844
3138
|
}
|
|
2845
|
-
body.quality = quality;
|
|
2846
|
-
}
|
|
2847
3139
|
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
3140
|
+
const base = getBaseUrl();
|
|
3141
|
+
const response = await fetch(`${base}/api/workbench/webview/screenshot`, {
|
|
3142
|
+
method: 'POST',
|
|
3143
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3144
|
+
body: JSON.stringify(body),
|
|
3145
|
+
});
|
|
2854
3146
|
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
3147
|
+
if (!response.ok) {
|
|
3148
|
+
const text = await response.text();
|
|
3149
|
+
try {
|
|
3150
|
+
const json = JSON.parse(text) as Record<string, unknown>;
|
|
3151
|
+
die(
|
|
3152
|
+
json.error ? String(json.error) : `HTTP ${response.status}`,
|
|
3153
|
+
typeof json.hint === 'string' ? json.hint : undefined,
|
|
3154
|
+
);
|
|
3155
|
+
} catch {
|
|
3156
|
+
die(`HTTP ${response.status}: ${text}`);
|
|
3157
|
+
}
|
|
2865
3158
|
}
|
|
2866
|
-
}
|
|
2867
3159
|
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
}
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
'
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
3160
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
3161
|
+
writeFileSync(outputPath, bytes);
|
|
3162
|
+
output({
|
|
3163
|
+
ok: true,
|
|
3164
|
+
output: outputPath,
|
|
3165
|
+
bytes: bytes.byteLength,
|
|
3166
|
+
mimeType: response.headers.get('Content-Type') ?? 'application/octet-stream',
|
|
3167
|
+
});
|
|
3168
|
+
},
|
|
3169
|
+
);
|
|
3170
|
+
|
|
3171
|
+
cmd(
|
|
3172
|
+
'screenshot',
|
|
3173
|
+
'Capture a screenshot from the active Bun.WebView automation session',
|
|
3174
|
+
[
|
|
3175
|
+
'pmx-canvas screenshot --output ./canvas.png',
|
|
3176
|
+
'pmx-canvas screenshot --output ./canvas.webp --format webp --quality 80',
|
|
3177
|
+
],
|
|
3178
|
+
async (args) => {
|
|
3179
|
+
if (args.includes('--help') || args.includes('-h')) return showCommandHelp('screenshot');
|
|
3180
|
+
const screenshotCommand = COMMANDS['webview screenshot'];
|
|
3181
|
+
if (!screenshotCommand) die('Internal error: webview screenshot command is unavailable.');
|
|
3182
|
+
await screenshotCommand.run(args);
|
|
3183
|
+
},
|
|
3184
|
+
);
|
|
2887
3185
|
|
|
2888
3186
|
// ── code-graph ───────────────────────────────────────────────
|
|
2889
|
-
cmd('code-graph', 'Show auto-detected file dependency graph', [
|
|
2890
|
-
'pmx-canvas code-graph',
|
|
2891
|
-
], async (args) => {
|
|
3187
|
+
cmd('code-graph', 'Show auto-detected file dependency graph', ['pmx-canvas code-graph'], async (args) => {
|
|
2892
3188
|
const { flags } = parseFlags(args);
|
|
2893
3189
|
if (flags.help || flags.h) return showCommandHelp('code-graph');
|
|
2894
3190
|
|
|
@@ -2897,9 +3193,7 @@ cmd('code-graph', 'Show auto-detected file dependency graph', [
|
|
|
2897
3193
|
});
|
|
2898
3194
|
|
|
2899
3195
|
// ── spatial ──────────────────────────────────────────────────
|
|
2900
|
-
cmd('spatial', 'Spatial analysis: clusters, reading order, neighborhoods', [
|
|
2901
|
-
'pmx-canvas spatial',
|
|
2902
|
-
], async (args) => {
|
|
3196
|
+
cmd('spatial', 'Spatial analysis: clusters, reading order, neighborhoods', ['pmx-canvas spatial'], async (args) => {
|
|
2903
3197
|
const { flags } = parseFlags(args);
|
|
2904
3198
|
if (flags.help || flags.h) return showCommandHelp('spatial');
|
|
2905
3199
|
|
|
@@ -2908,102 +3202,119 @@ cmd('spatial', 'Spatial analysis: clusters, reading order, neighborhoods', [
|
|
|
2908
3202
|
});
|
|
2909
3203
|
|
|
2910
3204
|
// ── watch ────────────────────────────────────────────────────
|
|
2911
|
-
cmd(
|
|
2912
|
-
'
|
|
2913
|
-
'
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
3205
|
+
cmd(
|
|
3206
|
+
'watch',
|
|
3207
|
+
'Watch low-token semantic canvas changes over the existing SSE stream',
|
|
3208
|
+
[
|
|
3209
|
+
'pmx-canvas watch',
|
|
3210
|
+
'pmx-canvas watch --json',
|
|
3211
|
+
'pmx-canvas watch --events context-pin,move-end',
|
|
3212
|
+
'pmx-canvas watch --json --events connect --max-events 1',
|
|
3213
|
+
],
|
|
3214
|
+
async (args) => {
|
|
3215
|
+
const { flags } = parseFlags(args);
|
|
3216
|
+
if (flags.help || flags.h) return showCommandHelp('watch');
|
|
3217
|
+
|
|
3218
|
+
if (flags.json && flags.compact) {
|
|
3219
|
+
die('Use either --json or --compact, not both.');
|
|
3220
|
+
}
|
|
2923
3221
|
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
die(
|
|
2938
|
-
`Invalid value for --events: ${filtersRaw}`,
|
|
2939
|
-
'Use a comma-separated subset of: context-pin,move-end,group,connect,remove',
|
|
3222
|
+
const filtersRaw = typeof flags.events === 'string' ? flags.events : undefined;
|
|
3223
|
+
const requestedFilters = filtersRaw
|
|
3224
|
+
? Array.from(
|
|
3225
|
+
new Set(
|
|
3226
|
+
filtersRaw
|
|
3227
|
+
.split(',')
|
|
3228
|
+
.map((value) => value.trim())
|
|
3229
|
+
.filter((value) => value.length > 0),
|
|
3230
|
+
),
|
|
3231
|
+
)
|
|
3232
|
+
: [];
|
|
3233
|
+
const invalidFilter = requestedFilters.find(
|
|
3234
|
+
(value) => !ALL_SEMANTIC_WATCH_EVENT_TYPES.includes(value as (typeof ALL_SEMANTIC_WATCH_EVENT_TYPES)[number]),
|
|
2940
3235
|
);
|
|
2941
|
-
|
|
3236
|
+
if (invalidFilter) {
|
|
3237
|
+
die(
|
|
3238
|
+
`Invalid value in --events: ${invalidFilter}`,
|
|
3239
|
+
'Use a comma-separated subset of: context-pin,move-end,group,connect,remove',
|
|
3240
|
+
);
|
|
3241
|
+
}
|
|
3242
|
+
const filters = parseSemanticEventFilter(filtersRaw);
|
|
3243
|
+
if (filtersRaw && filters.size === 0) {
|
|
3244
|
+
die(
|
|
3245
|
+
`Invalid value for --events: ${filtersRaw}`,
|
|
3246
|
+
'Use a comma-separated subset of: context-pin,move-end,group,connect,remove',
|
|
3247
|
+
);
|
|
3248
|
+
}
|
|
2942
3249
|
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
3250
|
+
const maxEvents = optionalNumberFlag(flags, 'max-events', 'Use a positive integer, e.g. --max-events 1');
|
|
3251
|
+
const jsonMode = Boolean(flags.json);
|
|
3252
|
+
const reducer = new SemanticWatchReducer();
|
|
3253
|
+
const pinned = (await api('GET', '/api/canvas/pinned-context')) as { nodeIds?: string[] };
|
|
3254
|
+
reducer.setInitialPins(Array.isArray(pinned.nodeIds) ? pinned.nodeIds : []);
|
|
2948
3255
|
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
3256
|
+
const base = getBaseUrl();
|
|
3257
|
+
const controller = new AbortController();
|
|
3258
|
+
let response: Response;
|
|
3259
|
+
try {
|
|
3260
|
+
response = await fetch(`${base}/api/workbench/events`, {
|
|
3261
|
+
method: 'GET',
|
|
3262
|
+
headers: { Accept: 'text/event-stream' },
|
|
3263
|
+
signal: controller.signal,
|
|
3264
|
+
});
|
|
3265
|
+
} catch (error) {
|
|
3266
|
+
die(
|
|
3267
|
+
`Cannot connect to pmx-canvas event stream at ${base}: ${error instanceof Error ? error.message : String(error)}`,
|
|
3268
|
+
'Start the server first: pmx-canvas --no-open',
|
|
3269
|
+
);
|
|
3270
|
+
}
|
|
2964
3271
|
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
3272
|
+
if (!response.ok) {
|
|
3273
|
+
const text = await response.text();
|
|
3274
|
+
die(`Failed to open event stream: HTTP ${response.status}`, text);
|
|
3275
|
+
}
|
|
3276
|
+
if (!response.body) {
|
|
3277
|
+
die('Workbench event stream did not return a readable body.');
|
|
3278
|
+
}
|
|
2972
3279
|
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
return;
|
|
3280
|
+
let emitted = 0;
|
|
3281
|
+
try {
|
|
3282
|
+
for await (const message of parseSseStream(response.body)) {
|
|
3283
|
+
const semanticEvents = reducer.handleMessage(message).filter((event) => filters.has(event.type));
|
|
3284
|
+
|
|
3285
|
+
for (const event of semanticEvents) {
|
|
3286
|
+
console.log(jsonMode ? JSON.stringify(event) : formatCompactWatchEvent(event));
|
|
3287
|
+
emitted++;
|
|
3288
|
+
if (maxEvents !== undefined && emitted >= maxEvents) {
|
|
3289
|
+
controller.abort();
|
|
3290
|
+
return;
|
|
3291
|
+
}
|
|
2986
3292
|
}
|
|
2987
3293
|
}
|
|
3294
|
+
} catch (error) {
|
|
3295
|
+
if (controller.signal.aborted) return;
|
|
3296
|
+
die(
|
|
3297
|
+
`Watch stream failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
3298
|
+
'Ensure the server is still running and reachable.',
|
|
3299
|
+
);
|
|
2988
3300
|
}
|
|
2989
|
-
}
|
|
2990
|
-
|
|
2991
|
-
die(
|
|
2992
|
-
`Watch stream failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2993
|
-
'Ensure the server is still running and reachable.',
|
|
2994
|
-
);
|
|
2995
|
-
}
|
|
2996
|
-
});
|
|
3301
|
+
},
|
|
3302
|
+
);
|
|
2997
3303
|
|
|
2998
3304
|
// ── serve (delegates back to original CLI) ───────────────────
|
|
2999
|
-
cmd(
|
|
3000
|
-
'
|
|
3001
|
-
'
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3305
|
+
cmd(
|
|
3306
|
+
'serve',
|
|
3307
|
+
'Start the canvas server',
|
|
3308
|
+
[
|
|
3309
|
+
'pmx-canvas serve',
|
|
3310
|
+
'pmx-canvas serve --port=8080 --no-open',
|
|
3311
|
+
'pmx-canvas serve --demo --theme=light',
|
|
3312
|
+
'pmx-canvas --no-open --webview-automation',
|
|
3313
|
+
],
|
|
3314
|
+
async (_args) => {
|
|
3315
|
+
console.log('Use: pmx-canvas [--port=PORT] [--demo] [--no-open] [--theme=THEME] [--webview-automation]');
|
|
3316
|
+
},
|
|
3317
|
+
);
|
|
3007
3318
|
|
|
3008
3319
|
// ── Help ─────────────────────────────────────────────────────
|
|
3009
3320
|
|
|
@@ -3049,8 +3360,12 @@ function showCommandHelp(name: string): void {
|
|
|
3049
3360
|
}
|
|
3050
3361
|
if (name === 'node add' || name === 'graph add' || name === 'validate spec') {
|
|
3051
3362
|
console.log('\nGraph flags:');
|
|
3052
|
-
console.log(
|
|
3053
|
-
|
|
3363
|
+
console.log(
|
|
3364
|
+
' Graph fields accept kebab-case CLI flags and camelCase schema names, e.g. --graph-type/--graphType and --x-key/--xKey',
|
|
3365
|
+
);
|
|
3366
|
+
console.log(
|
|
3367
|
+
' Use --node-height/--nodeHeight for canvas frame height; use --chart-height for chart content height. --height is kept as a frame-height alias for compatibility.',
|
|
3368
|
+
);
|
|
3054
3369
|
console.log(' Pass --show-legend false to hide legends in compact node layouts.');
|
|
3055
3370
|
}
|
|
3056
3371
|
if (name === 'validate spec') {
|
|
@@ -3326,9 +3641,7 @@ export async function runAgentCli(args: string[]): Promise<void> {
|
|
|
3326
3641
|
`Available subcommands: ${available}`,
|
|
3327
3642
|
].filter((hint): hint is string => typeof hint === 'string');
|
|
3328
3643
|
die(
|
|
3329
|
-
subcommand
|
|
3330
|
-
? `Unknown ${oneWord} subcommand: "${subcommand}".`
|
|
3331
|
-
: `Missing ${oneWord} subcommand.`,
|
|
3644
|
+
subcommand ? `Unknown ${oneWord} subcommand: "${subcommand}".` : `Missing ${oneWord} subcommand.`,
|
|
3332
3645
|
hints.join(' '),
|
|
3333
3646
|
);
|
|
3334
3647
|
}
|