@lushly-dev/fast-element 3.0.1-preview.0 → 3.0.1-preview.2
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 +12 -0
- package/dist/command/command.api.json +7857 -0
- package/dist/context/context.api.json +1084 -0
- package/dist/declarative/declarative.api.json +8130 -0
- package/dist/definition-schema/definition-schema.api.json +2689 -0
- package/dist/di/di.api.json +4945 -0
- package/dist/dts/arrays.d.ts +2 -0
- package/dist/dts/binding/binding.d.ts +59 -0
- package/dist/dts/binding/exports.d.ts +9 -0
- package/dist/dts/binding/normalize.d.ts +9 -0
- package/dist/dts/binding/one-time.d.ts +11 -0
- package/dist/dts/binding/one-way.d.ts +20 -0
- package/dist/dts/binding/signal.d.ts +36 -0
- package/dist/dts/binding/two-way.d.ts +44 -0
- package/dist/dts/command/behavior.d.ts +94 -0
- package/dist/dts/command/behaviors/confirm-behavior.d.ts +139 -0
- package/dist/dts/command/behaviors/undo-behavior.d.ts +178 -0
- package/dist/dts/command/command.d.ts +79 -0
- package/dist/dts/command/component-registry.d.ts +50 -0
- package/dist/dts/command/components/confirm-dialog.d.ts +117 -0
- package/dist/dts/command/index.d.ts +21 -0
- package/dist/dts/command/registry.d.ts +201 -0
- package/dist/dts/command/schema-validation.d.ts +45 -0
- package/dist/dts/command/types.d.ts +332 -0
- package/dist/dts/command/validation.d.ts +116 -0
- package/dist/dts/command/weak-ref.d.ts +11 -0
- package/dist/dts/components/attributes.d.ts +153 -0
- package/dist/dts/components/definition-schema-transforms.d.ts +9 -0
- package/dist/dts/components/element-controller.d.ts +349 -0
- package/dist/dts/components/element-hydration.d.ts +2 -0
- package/dist/dts/components/enable-hydration.d.ts +66 -0
- package/dist/dts/components/fast-definitions.d.ts +211 -0
- package/dist/dts/components/fast-element-registry.d.ts +23 -0
- package/dist/dts/components/fast-element.d.ts +91 -0
- package/dist/dts/components/hydration-tracker.d.ts +110 -0
- package/dist/dts/components/hydration.d.ts +52 -0
- package/dist/dts/components/schema.d.ts +205 -0
- package/dist/dts/context.d.ts +171 -0
- package/dist/dts/debug.d.ts +5 -0
- package/dist/dts/declarative/attribute-map.d.ts +58 -0
- package/dist/dts/declarative/debug.d.ts +4 -0
- package/dist/dts/declarative/index.d.ts +14 -0
- package/dist/dts/declarative/interfaces.d.ts +8 -0
- package/dist/dts/declarative/observer-map-utilities.d.ts +58 -0
- package/dist/dts/declarative/observer-map.d.ts +89 -0
- package/dist/dts/declarative/runtime.d.ts +5 -0
- package/dist/dts/declarative/syntax.d.ts +21 -0
- package/dist/dts/declarative/template-bridge.d.ts +33 -0
- package/dist/dts/declarative/template-parser.d.ts +98 -0
- package/dist/dts/declarative/template.d.ts +9 -0
- package/dist/dts/declarative/utilities.d.ts +358 -0
- package/dist/dts/definition-schema/api.d.ts +42 -0
- package/dist/dts/definition-schema/cem.d.ts +78 -0
- package/dist/dts/definition-schema/extension.d.ts +18 -0
- package/dist/dts/definition-schema/extract.d.ts +38 -0
- package/dist/dts/definition-schema/index.d.ts +9 -0
- package/dist/dts/definition-schema/storage.d.ts +35 -0
- package/dist/dts/definition-schema/types.d.ts +176 -0
- package/dist/dts/dev-mode.d.ts +13 -0
- package/dist/dts/di/di.d.ts +894 -0
- package/dist/dts/dom-policy.d.ts +101 -0
- package/dist/dts/dom.d.ts +88 -0
- package/dist/dts/hydration/diagnostics.d.ts +93 -0
- package/dist/dts/hydration/exports.d.ts +18 -0
- package/dist/dts/hydration/hydration-debugger.d.ts +35 -0
- package/dist/dts/hydration/messages.d.ts +62 -0
- package/dist/dts/hydration/runtime.d.ts +7 -0
- package/dist/dts/hydration/target-builder.d.ts +107 -0
- package/dist/dts/index.d.ts +51 -0
- package/dist/dts/index.debug.d.ts +1 -0
- package/dist/dts/index.rollup.d.ts +1 -0
- package/dist/dts/index.rollup.debug.d.ts +1 -0
- package/dist/dts/interfaces.d.ts +106 -0
- package/dist/dts/metadata.d.ts +26 -0
- package/dist/dts/observation/arrays.d.ts +248 -0
- package/dist/dts/observation/notifier.d.ts +127 -0
- package/dist/dts/observation/observable.d.ts +276 -0
- package/dist/dts/observation/update-queue.d.ts +40 -0
- package/dist/dts/observation/volatile.d.ts +9 -0
- package/dist/dts/platform.d.ts +87 -0
- package/dist/dts/registry.d.ts +2 -0
- package/dist/dts/render.d.ts +7 -0
- package/dist/dts/schema.d.ts +1 -0
- package/dist/dts/scoped/detection.d.ts +55 -0
- package/dist/dts/scoped/index.d.ts +68 -0
- package/dist/dts/scoped/mixin.d.ts +118 -0
- package/dist/dts/scoped/registry.d.ts +76 -0
- package/dist/dts/scoped/types.d.ts +48 -0
- package/dist/dts/state/exports.d.ts +3 -0
- package/dist/dts/state/reactive.d.ts +8 -0
- package/dist/dts/state/state.d.ts +141 -0
- package/dist/dts/state/visitor.d.ts +6 -0
- package/dist/dts/state/watch.d.ts +10 -0
- package/dist/dts/styles/css-directive.d.ts +50 -0
- package/dist/dts/styles/css.d.ts +34 -0
- package/dist/dts/styles/element-styles.d.ts +61 -0
- package/dist/dts/styles/host.d.ts +65 -0
- package/dist/dts/styles/style-strategy.d.ts +42 -0
- package/dist/dts/templating/children.d.ts +62 -0
- package/dist/dts/templating/compiler.d.ts +54 -0
- package/dist/dts/templating/html-binding-directive.d.ts +155 -0
- package/dist/dts/templating/html-directive.d.ts +218 -0
- package/dist/dts/templating/html.d.ts +1 -0
- package/dist/dts/templating/hydration-view.d.ts +130 -0
- package/dist/dts/templating/markup.d.ts +48 -0
- package/dist/dts/templating/node-observation.d.ts +99 -0
- package/dist/dts/templating/ref.d.ts +23 -0
- package/dist/dts/templating/render.d.ts +318 -0
- package/dist/dts/templating/repeat.d.ts +131 -0
- package/dist/dts/templating/slotted.d.ts +38 -0
- package/dist/dts/templating/template.d.ts +221 -0
- package/dist/dts/templating/view.d.ts +268 -0
- package/dist/dts/templating/when.d.ts +12 -0
- package/dist/dts/testing/exports.d.ts +3 -0
- package/dist/dts/testing/fakes.d.ts +16 -0
- package/dist/dts/testing/fixture.d.ts +84 -0
- package/dist/dts/testing/models.d.ts +20 -0
- package/dist/dts/testing/timeout.d.ts +7 -0
- package/dist/dts/trust/index.d.ts +8 -0
- package/dist/dts/trust/types.d.ts +48 -0
- package/dist/dts/trust/utils.d.ts +26 -0
- package/dist/dts/tsdoc-metadata.json +11 -0
- package/dist/dts/utilities.d.ts +76 -0
- package/dist/esm/arrays.js +1 -0
- package/dist/esm/binding/binding.js +18 -0
- package/dist/esm/binding/exports.js +6 -0
- package/dist/esm/binding/normalize.js +17 -0
- package/dist/esm/binding/one-time.js +21 -0
- package/dist/esm/binding/one-way.js +30 -0
- package/dist/esm/binding/signal.js +99 -0
- package/dist/esm/binding/two-way.js +109 -0
- package/dist/esm/command/behavior.js +155 -0
- package/dist/esm/command/behaviors/confirm-behavior.js +225 -0
- package/dist/esm/command/behaviors/undo-behavior.js +313 -0
- package/dist/esm/command/command.js +104 -0
- package/dist/esm/command/component-registry.js +107 -0
- package/dist/esm/command/components/confirm-dialog.js +370 -0
- package/dist/esm/command/index.js +22 -0
- package/dist/esm/command/registry.js +755 -0
- package/dist/esm/command/schema-validation.js +194 -0
- package/dist/esm/command/types.js +86 -0
- package/dist/esm/command/validation.js +508 -0
- package/dist/esm/command/weak-ref.js +5 -0
- package/dist/esm/components/attributes.js +219 -0
- package/dist/esm/components/definition-schema-transforms.js +23 -0
- package/dist/esm/components/element-controller.js +840 -0
- package/dist/esm/components/element-hydration.js +2 -0
- package/dist/esm/components/enable-hydration.js +123 -0
- package/dist/esm/components/fast-definitions.js +294 -0
- package/dist/esm/components/fast-element-registry.js +67 -0
- package/dist/esm/components/fast-element.js +82 -0
- package/dist/esm/components/hydration-tracker.js +211 -0
- package/dist/esm/components/hydration.js +187 -0
- package/dist/esm/components/schema.js +253 -0
- package/dist/esm/context.js +185 -0
- package/dist/esm/debug.js +71 -0
- package/dist/esm/declarative/attribute-map.js +122 -0
- package/dist/esm/declarative/debug.js +4 -0
- package/dist/esm/declarative/index.js +10 -0
- package/dist/esm/declarative/interfaces.js +9 -0
- package/dist/esm/declarative/observer-map-utilities.js +565 -0
- package/dist/esm/declarative/observer-map.js +216 -0
- package/dist/esm/declarative/runtime.js +14 -0
- package/dist/esm/declarative/syntax.js +36 -0
- package/dist/esm/declarative/template-bridge.js +160 -0
- package/dist/esm/declarative/template-parser.js +306 -0
- package/dist/esm/declarative/template.js +116 -0
- package/dist/esm/declarative/utilities.js +1069 -0
- package/dist/esm/definition-schema/api.js +129 -0
- package/dist/esm/definition-schema/cem.js +89 -0
- package/dist/esm/definition-schema/extension.js +53 -0
- package/dist/esm/definition-schema/extract.js +167 -0
- package/dist/esm/definition-schema/index.js +8 -0
- package/dist/esm/definition-schema/storage.js +58 -0
- package/dist/esm/definition-schema/types.js +8 -0
- package/dist/esm/dev-mode.js +22 -0
- package/dist/esm/di/di.js +1441 -0
- package/dist/esm/dom-policy.js +374 -0
- package/dist/esm/dom.js +101 -0
- package/dist/esm/hydration/diagnostics.js +50 -0
- package/dist/esm/hydration/exports.js +6 -0
- package/dist/esm/hydration/hydration-debugger.js +112 -0
- package/dist/esm/hydration/messages.js +84 -0
- package/dist/esm/hydration/runtime.js +33 -0
- package/dist/esm/hydration/target-builder.js +264 -0
- package/dist/esm/index.debug.js +3 -0
- package/dist/esm/index.js +49 -0
- package/dist/esm/index.rollup.debug.js +6 -0
- package/dist/esm/index.rollup.js +4 -0
- package/dist/esm/interfaces.js +55 -0
- package/dist/esm/metadata.js +57 -0
- package/dist/esm/observation/arrays.js +661 -0
- package/dist/esm/observation/notifier.js +162 -0
- package/dist/esm/observation/observable.js +324 -0
- package/dist/esm/observation/update-queue.js +56 -0
- package/dist/esm/observation/volatile.js +16 -0
- package/dist/esm/platform.js +98 -0
- package/dist/esm/registry.js +2 -0
- package/dist/esm/render.js +1 -0
- package/dist/esm/schema.js +1 -0
- package/dist/esm/scoped/detection.js +102 -0
- package/dist/esm/scoped/index.js +69 -0
- package/dist/esm/scoped/mixin.js +127 -0
- package/dist/esm/scoped/registry.js +125 -0
- package/dist/esm/scoped/types.js +4 -0
- package/dist/esm/state/exports.js +3 -0
- package/dist/esm/state/reactive.js +34 -0
- package/dist/esm/state/state.js +148 -0
- package/dist/esm/state/visitor.js +28 -0
- package/dist/esm/state/watch.js +36 -0
- package/dist/esm/styles/css-directive.js +36 -0
- package/dist/esm/styles/css.js +56 -0
- package/dist/esm/styles/element-styles.js +144 -0
- package/dist/esm/styles/host.js +1 -0
- package/dist/esm/styles/style-strategy.js +1 -0
- package/dist/esm/templating/children.js +67 -0
- package/dist/esm/templating/compiler.js +359 -0
- package/dist/esm/templating/html-binding-directive.js +288 -0
- package/dist/esm/templating/html-directive.js +113 -0
- package/dist/esm/templating/html.js +1 -0
- package/dist/esm/templating/hydration-view.js +228 -0
- package/dist/esm/templating/markup.js +83 -0
- package/dist/esm/templating/node-observation.js +84 -0
- package/dist/esm/templating/ref.js +21 -0
- package/dist/esm/templating/render.js +468 -0
- package/dist/esm/templating/repeat.js +434 -0
- package/dist/esm/templating/slotted.js +49 -0
- package/dist/esm/templating/template.js +206 -0
- package/dist/esm/templating/view.js +298 -0
- package/dist/esm/templating/when.js +22 -0
- package/dist/esm/testing/exports.js +3 -0
- package/dist/esm/testing/fakes.js +107 -0
- package/dist/esm/testing/fixture.js +86 -0
- package/dist/esm/testing/models.js +58 -0
- package/dist/esm/testing/timeout.js +24 -0
- package/dist/esm/trust/index.js +7 -0
- package/dist/esm/trust/types.js +6 -0
- package/dist/esm/trust/utils.js +126 -0
- package/dist/esm/utilities.js +140 -0
- package/dist/fast-element.api.json +26798 -0
- package/dist/fast-element.d.ts +3985 -0
- package/dist/fast-element.debug.js +7604 -0
- package/dist/fast-element.debug.min.js +3 -0
- package/dist/fast-element.js +7603 -0
- package/dist/fast-element.min.js +3 -0
- package/dist/fast-element.untrimmed.d.ts +4103 -0
- package/dist/observation/arrays/arrays.api.json +2621 -0
- package/dist/scoped/scoped.api.json +891 -0
- package/dist/trust/trust.api.json +629 -0
- package/package.json +21 -23
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CommandRegistry singleton for managing command lifecycle.
|
|
3
|
+
* Enables AI agents and command palettes to discover and execute commands.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* - SSR-safe: Guards against document access in Node.js
|
|
7
|
+
* - The `cmd-exec` document event bridge is opt-in (`enableEventBridge()`)
|
|
8
|
+
* - Uses AFD's failure() helper for error results
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { CommandRegistry } from '@lushly-dev/fast-element/command.js';
|
|
13
|
+
*
|
|
14
|
+
* // Execute a command
|
|
15
|
+
* const result = await CommandRegistry.execute('editor.save');
|
|
16
|
+
* if (result.success) {
|
|
17
|
+
* console.log('Saved:', result.data);
|
|
18
|
+
* } else {
|
|
19
|
+
* console.error('Failed:', result.error.message);
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* // Get actions for command palette
|
|
23
|
+
* const actions = CommandRegistry.getActions();
|
|
24
|
+
*
|
|
25
|
+
* // Subscribe to changes
|
|
26
|
+
* const unsubscribe = CommandRegistry.onChange((added, removed) => {
|
|
27
|
+
* console.log('Commands changed:', { added, removed });
|
|
28
|
+
* });
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
import { isDevMode } from "../dev-mode.js";
|
|
32
|
+
import { validateCommandArguments } from "./schema-validation.js";
|
|
33
|
+
import { DEFAULT_EXPOSE_CONFIG, ErrorCodes, failure } from "./types.js";
|
|
34
|
+
/**
|
|
35
|
+
* Reads a `canExecute`/`when` getter, treating a throwing getter as `false` so a
|
|
36
|
+
* single faulty command cannot break command listing.
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
function readFlag(read) {
|
|
40
|
+
try {
|
|
41
|
+
return read();
|
|
42
|
+
}
|
|
43
|
+
catch (_a) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Internal implementation of the CommandRegistry.
|
|
49
|
+
* @internal
|
|
50
|
+
*/
|
|
51
|
+
class CommandRegistryImpl {
|
|
52
|
+
constructor() {
|
|
53
|
+
this.commands = new Map();
|
|
54
|
+
this.shadowed = new Map();
|
|
55
|
+
this.listeners = new Set();
|
|
56
|
+
this.interceptors = [];
|
|
57
|
+
this.postHooks = [];
|
|
58
|
+
this.eventBridgeUsers = 0;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Enables the document-level `cmd-exec` event bridge.
|
|
62
|
+
*
|
|
63
|
+
* @returns A function that disables the bridge again
|
|
64
|
+
*
|
|
65
|
+
* @remarks
|
|
66
|
+
* The bridge is **opt-in**. A `cmd-exec` event
|
|
67
|
+
* (`new CustomEvent("cmd-exec", { detail: { id, args } })`) is an external
|
|
68
|
+
* invocation path: any script, extension or agent that can dispatch events on
|
|
69
|
+
* the document can use it. Events are therefore treated as `agent` origin
|
|
70
|
+
* invocations (see {@link CommandRegistryAPI.invoke}): the command must be
|
|
71
|
+
* exposed to `agent`, its `parameters` schema is enforced, and destructive
|
|
72
|
+
* commands fail closed unless a confirmation interceptor is installed.
|
|
73
|
+
*
|
|
74
|
+
* Calling this more than once is safe; the single shared listener is removed
|
|
75
|
+
* only when every caller has disposed. SSR-safe: a no-op outside a browser.
|
|
76
|
+
*
|
|
77
|
+
* @public
|
|
78
|
+
*/
|
|
79
|
+
enableEventBridge() {
|
|
80
|
+
if (typeof document === "undefined") {
|
|
81
|
+
return () => { };
|
|
82
|
+
}
|
|
83
|
+
if (this.eventBridge === void 0) {
|
|
84
|
+
const bridge = (e) => {
|
|
85
|
+
const detail = e.detail;
|
|
86
|
+
if (!detail ||
|
|
87
|
+
typeof detail !== "object" ||
|
|
88
|
+
typeof detail.id !== "string") {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const args = Array.isArray(detail.args) ? detail.args : [];
|
|
92
|
+
void this.invoke(detail.id, args, { origin: "agent" });
|
|
93
|
+
};
|
|
94
|
+
this.eventBridge = bridge;
|
|
95
|
+
document.addEventListener("cmd-exec", bridge);
|
|
96
|
+
}
|
|
97
|
+
this.eventBridgeUsers++;
|
|
98
|
+
let disposed = false;
|
|
99
|
+
return () => {
|
|
100
|
+
if (disposed)
|
|
101
|
+
return;
|
|
102
|
+
disposed = true;
|
|
103
|
+
if (--this.eventBridgeUsers === 0 && this.eventBridge !== void 0) {
|
|
104
|
+
document.removeEventListener("cmd-exec", this.eventBridge);
|
|
105
|
+
this.eventBridge = void 0;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Registers a command entry in the registry.
|
|
111
|
+
*
|
|
112
|
+
* @param id - The command identifier
|
|
113
|
+
* @param command - The command entry to register
|
|
114
|
+
*
|
|
115
|
+
* @remarks
|
|
116
|
+
* - Emits a warning in DEV_MODE if overwriting an existing command
|
|
117
|
+
* - Notifies listeners of the registration
|
|
118
|
+
* - Emits telemetry event
|
|
119
|
+
*
|
|
120
|
+
* @public
|
|
121
|
+
*/
|
|
122
|
+
register(id, command) {
|
|
123
|
+
const current = this.commands.get(id);
|
|
124
|
+
const stack = this.shadowed.get(id);
|
|
125
|
+
// Re-registering an entry that is currently shadowed promotes it.
|
|
126
|
+
if (stack !== void 0) {
|
|
127
|
+
const index = stack.indexOf(command);
|
|
128
|
+
if (index !== -1)
|
|
129
|
+
stack.splice(index, 1);
|
|
130
|
+
}
|
|
131
|
+
if (current !== void 0 && current !== command) {
|
|
132
|
+
if (isDevMode()) {
|
|
133
|
+
console.warn(`[fast-af] Command "${id}" already registered. Overwriting.`);
|
|
134
|
+
}
|
|
135
|
+
// Keep the displaced entry so removing the newest source falls back to it.
|
|
136
|
+
const shadows = stack !== null && stack !== void 0 ? stack : [];
|
|
137
|
+
shadows.push(current);
|
|
138
|
+
this.shadowed.set(id, shadows);
|
|
139
|
+
}
|
|
140
|
+
this.commands.set(id, command);
|
|
141
|
+
this.notify([id], []);
|
|
142
|
+
this.emitTelemetry("command.registered", { id, label: command.label });
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Unregisters a command from the registry.
|
|
146
|
+
*
|
|
147
|
+
* @param id - The command identifier to unregister
|
|
148
|
+
* @param expected - When provided, only this registration is removed
|
|
149
|
+
*
|
|
150
|
+
* @remarks
|
|
151
|
+
* - No-op if command doesn't exist
|
|
152
|
+
* - The most recent registration wins, but earlier registrations of the same ID
|
|
153
|
+
* (for example from several connected elements) are remembered: removing the
|
|
154
|
+
* active `expected` entry makes the previous one active again, and the
|
|
155
|
+
* command is only unregistered when no registration remains
|
|
156
|
+
* - Without `expected`, the command and all its registrations are removed
|
|
157
|
+
* - Notifies listeners of the removal (or of the replacement when falling back)
|
|
158
|
+
* - Emits telemetry event on removal
|
|
159
|
+
*
|
|
160
|
+
* @public
|
|
161
|
+
*/
|
|
162
|
+
unregister(id, expected) {
|
|
163
|
+
var _a, _b;
|
|
164
|
+
var _c;
|
|
165
|
+
if (expected !== void 0 && this.commands.get(id) !== expected) {
|
|
166
|
+
// A stale removal of a shadowed registration.
|
|
167
|
+
const stack = this.shadowed.get(id);
|
|
168
|
+
const index = (_c = stack === null || stack === void 0 ? void 0 : stack.indexOf(expected)) !== null && _c !== void 0 ? _c : -1;
|
|
169
|
+
if (stack !== void 0 && index !== -1) {
|
|
170
|
+
stack.splice(index, 1);
|
|
171
|
+
if (stack.length === 0)
|
|
172
|
+
this.shadowed.delete(id);
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const fallback = expected !== void 0 ? (_a = this.shadowed.get(id)) === null || _a === void 0 ? void 0 : _a.pop() : void 0;
|
|
177
|
+
if (fallback !== void 0) {
|
|
178
|
+
if (((_b = this.shadowed.get(id)) === null || _b === void 0 ? void 0 : _b.length) === 0)
|
|
179
|
+
this.shadowed.delete(id);
|
|
180
|
+
this.commands.set(id, fallback);
|
|
181
|
+
this.notify([id], []);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
this.shadowed.delete(id);
|
|
185
|
+
if (this.commands.delete(id)) {
|
|
186
|
+
this.notify([], [id]);
|
|
187
|
+
this.emitTelemetry("command.unregistered", { id });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Retrieves a command entry by ID.
|
|
192
|
+
*
|
|
193
|
+
* @param id - The command identifier
|
|
194
|
+
* @returns The command entry, or undefined if not found
|
|
195
|
+
*
|
|
196
|
+
* @public
|
|
197
|
+
*/
|
|
198
|
+
get(id) {
|
|
199
|
+
return this.commands.get(id);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Executes a command by ID on behalf of the application's own code.
|
|
203
|
+
*
|
|
204
|
+
* @param id - The command identifier
|
|
205
|
+
* @param args - Arguments to pass to the command
|
|
206
|
+
* @returns A Promise resolving to the CommandResult
|
|
207
|
+
*
|
|
208
|
+
* @remarks
|
|
209
|
+
* This is the trusted, programmatic path: `expose`, `parameters` and the
|
|
210
|
+
* confirmation requirement are not enforced here (use `invoke()` for
|
|
211
|
+
* invocations that originate from an agent, MCP server, CLI or event).
|
|
212
|
+
*
|
|
213
|
+
* Never throws. Returns a failure result if:
|
|
214
|
+
* - Command not found (COMMAND_NOT_FOUND)
|
|
215
|
+
* - Command is disabled (COMMAND_DISABLED)
|
|
216
|
+
* - A `canExecute` getter throws (EXECUTION_FAILED)
|
|
217
|
+
*
|
|
218
|
+
* Interceptors are run in priority order before the command handler.
|
|
219
|
+
*
|
|
220
|
+
* @public
|
|
221
|
+
*/
|
|
222
|
+
execute(id, ...args) {
|
|
223
|
+
return this.run(id, args);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Invokes a command on behalf of an external caller (agent, MCP, CLI, palette
|
|
227
|
+
* or the `cmd-exec` event bridge).
|
|
228
|
+
*
|
|
229
|
+
* @param id - The command identifier
|
|
230
|
+
* @param args - Arguments to pass to the command
|
|
231
|
+
* @param options - Describes the invocation origin
|
|
232
|
+
* @returns A Promise resolving to the CommandResult; never rejects
|
|
233
|
+
*
|
|
234
|
+
* @remarks
|
|
235
|
+
* Applies the safety properties that `execute()` leaves to the caller:
|
|
236
|
+
* - `expose`: the command must be exposed to `options.origin`
|
|
237
|
+
* (COMMAND_NOT_EXPOSED); same secure defaults as `getActions()`
|
|
238
|
+
* - `parameters`: when the command declares a JSON Schema it is applied to
|
|
239
|
+
* `args[0]` (INVALID_PARAMETERS); see the `parameters` option docs
|
|
240
|
+
* - `destructive`: fails closed (CONFIRMATION_REQUIRED) unless a confirmation
|
|
241
|
+
* interceptor (named `confirm`, or flagged `confirms: true`) is installed
|
|
242
|
+
*
|
|
243
|
+
* @public
|
|
244
|
+
*/
|
|
245
|
+
invoke(id, args, options) {
|
|
246
|
+
return this.run(id, Array.isArray(args) ? args : [], options);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Shared execution path for `execute()` and `invoke()`.
|
|
250
|
+
* @internal
|
|
251
|
+
*/
|
|
252
|
+
run(id, args, options) {
|
|
253
|
+
var _a;
|
|
254
|
+
var _b;
|
|
255
|
+
const cmd = this.commands.get(id);
|
|
256
|
+
if (!cmd) {
|
|
257
|
+
return Promise.resolve(failure({
|
|
258
|
+
code: ErrorCodes.COMMAND_NOT_FOUND,
|
|
259
|
+
message: `Command not found: ${id}`,
|
|
260
|
+
suggestion: "Check command ID or ensure component is connected",
|
|
261
|
+
retryable: false,
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
const origin = options === null || options === void 0 ? void 0 : options.origin;
|
|
265
|
+
if (origin !== void 0) {
|
|
266
|
+
const expose = Object.assign(Object.assign({}, DEFAULT_EXPOSE_CONFIG), cmd.expose);
|
|
267
|
+
if (expose[origin] !== true) {
|
|
268
|
+
return Promise.resolve(this.reject(id, origin, failure({
|
|
269
|
+
code: ErrorCodes.COMMAND_NOT_EXPOSED,
|
|
270
|
+
message: `Command not exposed to ${String(origin)}: ${id}`,
|
|
271
|
+
suggestion: "Enable the interface in the command's expose configuration",
|
|
272
|
+
retryable: false,
|
|
273
|
+
})));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
let canExecute;
|
|
277
|
+
try {
|
|
278
|
+
canExecute = cmd.canExecute;
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
return Promise.resolve(failure({
|
|
282
|
+
code: ErrorCodes.EXECUTION_FAILED,
|
|
283
|
+
message: `Command availability check failed: ${id}`,
|
|
284
|
+
suggestion: "Inspect the command's canExecute implementation",
|
|
285
|
+
retryable: false,
|
|
286
|
+
details: {
|
|
287
|
+
cause: error instanceof Error
|
|
288
|
+
? { message: error.message, name: error.name }
|
|
289
|
+
: { value: String(error) },
|
|
290
|
+
},
|
|
291
|
+
}));
|
|
292
|
+
}
|
|
293
|
+
if (!canExecute) {
|
|
294
|
+
return Promise.resolve(failure({
|
|
295
|
+
code: ErrorCodes.COMMAND_DISABLED,
|
|
296
|
+
message: `Command disabled: ${id}`,
|
|
297
|
+
suggestion: "Wait for the command to become available",
|
|
298
|
+
retryable: true,
|
|
299
|
+
}));
|
|
300
|
+
}
|
|
301
|
+
if (origin !== void 0) {
|
|
302
|
+
if (cmd.parameters) {
|
|
303
|
+
const issues = validateCommandArguments(args, cmd.parameters);
|
|
304
|
+
if (issues.length > 0) {
|
|
305
|
+
return Promise.resolve(this.reject(id, origin, failure({
|
|
306
|
+
code: ErrorCodes.INVALID_PARAMETERS,
|
|
307
|
+
message: `Invalid parameters for ${id}: ${issues
|
|
308
|
+
.map(i => `${i.path} ${i.message}`)
|
|
309
|
+
.join("; ")}`,
|
|
310
|
+
suggestion: "Provide arguments matching the command's parameters schema",
|
|
311
|
+
retryable: false,
|
|
312
|
+
details: { issues },
|
|
313
|
+
})));
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (cmd.destructive &&
|
|
317
|
+
!this.interceptors.some(i => i.confirms === true || i.name === "confirm")) {
|
|
318
|
+
return Promise.resolve(this.reject(id, origin, failure({
|
|
319
|
+
code: ErrorCodes.CONFIRMATION_REQUIRED,
|
|
320
|
+
message: `Command requires confirmation: ${id}`,
|
|
321
|
+
suggestion: "Install a confirmation interceptor (for example ConfirmBehavior) before invoking destructive commands externally",
|
|
322
|
+
retryable: false,
|
|
323
|
+
})));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const start = (_b = (_a = globalThis.performance) === null || _a === void 0 ? void 0 : _a.now()) !== null && _b !== void 0 ? _b : Date.now();
|
|
327
|
+
// Build the handler that executes the command
|
|
328
|
+
const executeHandler = () => {
|
|
329
|
+
return Promise.resolve(cmd.execute(...args));
|
|
330
|
+
};
|
|
331
|
+
// Build the interceptor chain and execute
|
|
332
|
+
const chain = this.buildInterceptorChain(cmd, args, executeHandler);
|
|
333
|
+
return Promise.resolve()
|
|
334
|
+
.then(chain)
|
|
335
|
+
.then(result => this.normalizeResult(id, result))
|
|
336
|
+
.catch(error => failure({
|
|
337
|
+
code: ErrorCodes.EXECUTION_FAILED,
|
|
338
|
+
message: `Command execution failed: ${id}`,
|
|
339
|
+
suggestion: "Inspect the command implementation and retry after resolving the error",
|
|
340
|
+
retryable: false,
|
|
341
|
+
details: {
|
|
342
|
+
cause: error instanceof Error
|
|
343
|
+
? { message: error.message, name: error.name }
|
|
344
|
+
: { value: String(error) },
|
|
345
|
+
},
|
|
346
|
+
}))
|
|
347
|
+
.then(result => {
|
|
348
|
+
var _a;
|
|
349
|
+
var _b;
|
|
350
|
+
this.emitTelemetry("command.executed", {
|
|
351
|
+
id,
|
|
352
|
+
duration: ((_b = (_a = globalThis.performance) === null || _a === void 0 ? void 0 : _a.now()) !== null && _b !== void 0 ? _b : Date.now()) - start,
|
|
353
|
+
success: result.success,
|
|
354
|
+
});
|
|
355
|
+
// Run post-hooks after execution
|
|
356
|
+
this.runPostHooks(cmd, args, result);
|
|
357
|
+
return result;
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Gets all available commands as Action objects for command palette integration.
|
|
362
|
+
*
|
|
363
|
+
* @param filter - Optional filter to restrict which commands are returned
|
|
364
|
+
* @returns Array of Action objects for visible, available commands
|
|
365
|
+
*
|
|
366
|
+
* @remarks
|
|
367
|
+
* - Filters out commands declared `hidden: true`
|
|
368
|
+
* - Filters out commands where `when` is false
|
|
369
|
+
* - Sets `disabled` based on `canExecute`
|
|
370
|
+
* - Compatible with ninja-keys and similar libraries
|
|
371
|
+
* - When filter.interface is specified, only returns commands exposed to that interface
|
|
372
|
+
* - Secure defaults: MCP and CLI are opt-in (false by default)
|
|
373
|
+
*
|
|
374
|
+
* @example
|
|
375
|
+
* ```typescript
|
|
376
|
+
* // Get all visible commands (for command palette)
|
|
377
|
+
* const allActions = CommandRegistry.getActions();
|
|
378
|
+
*
|
|
379
|
+
* // Get only commands exposed to MCP
|
|
380
|
+
* const mcpActions = CommandRegistry.getActions({ interface: 'mcp' });
|
|
381
|
+
*
|
|
382
|
+
* // Get only commands exposed to CLI
|
|
383
|
+
* const cliActions = CommandRegistry.getActions({ interface: 'cli' });
|
|
384
|
+
* ```
|
|
385
|
+
*
|
|
386
|
+
* @public
|
|
387
|
+
*/
|
|
388
|
+
getActions(filter) {
|
|
389
|
+
return Array.from(this.commands.values())
|
|
390
|
+
.filter(cmd => {
|
|
391
|
+
// Hidden commands stay registered/executable but never surface as actions
|
|
392
|
+
if (cmd.hidden)
|
|
393
|
+
return false;
|
|
394
|
+
// Always filter by visibility
|
|
395
|
+
if (!readFlag(() => cmd.when))
|
|
396
|
+
return false;
|
|
397
|
+
// If interface filter is specified, check expose config
|
|
398
|
+
if (filter === null || filter === void 0 ? void 0 : filter.interface) {
|
|
399
|
+
const expose = Object.assign(Object.assign({}, DEFAULT_EXPOSE_CONFIG), cmd.expose);
|
|
400
|
+
return expose[filter.interface] === true;
|
|
401
|
+
}
|
|
402
|
+
return true;
|
|
403
|
+
})
|
|
404
|
+
.map(cmd => ({
|
|
405
|
+
id: cmd.id,
|
|
406
|
+
title: cmd.label,
|
|
407
|
+
icon: cmd.icon,
|
|
408
|
+
section: cmd.category,
|
|
409
|
+
hotkey: cmd.keybinding,
|
|
410
|
+
disabled: !readFlag(() => cmd.canExecute),
|
|
411
|
+
handler: () => {
|
|
412
|
+
this.execute(cmd.id);
|
|
413
|
+
},
|
|
414
|
+
}));
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Subscribes to command registry changes.
|
|
418
|
+
*
|
|
419
|
+
* @param listener - Callback invoked when commands are added or removed
|
|
420
|
+
* @returns Unsubscribe function
|
|
421
|
+
*
|
|
422
|
+
* @example
|
|
423
|
+
* ```typescript
|
|
424
|
+
* const unsubscribe = CommandRegistry.onChange((added, removed) => {
|
|
425
|
+
* console.log('Added:', added, 'Removed:', removed);
|
|
426
|
+
* });
|
|
427
|
+
*
|
|
428
|
+
* // Later, to stop listening:
|
|
429
|
+
* unsubscribe();
|
|
430
|
+
* ```
|
|
431
|
+
*
|
|
432
|
+
* @public
|
|
433
|
+
*/
|
|
434
|
+
onChange(listener) {
|
|
435
|
+
this.listeners.add(listener);
|
|
436
|
+
return () => this.listeners.delete(listener);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Clears all registered commands.
|
|
440
|
+
*
|
|
441
|
+
* @remarks
|
|
442
|
+
* For testing purposes only.
|
|
443
|
+
* Notifies listeners of all removed commands.
|
|
444
|
+
*
|
|
445
|
+
* @public
|
|
446
|
+
*/
|
|
447
|
+
clear() {
|
|
448
|
+
const ids = Array.from(this.commands.keys());
|
|
449
|
+
this.commands.clear();
|
|
450
|
+
this.shadowed.clear();
|
|
451
|
+
if (ids.length > 0) {
|
|
452
|
+
this.notify([], ids);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Gets a scoped registry for micro-frontend isolation.
|
|
457
|
+
*
|
|
458
|
+
* @param _shadowRoot - The shadow root to scope to (unused in Phase 1)
|
|
459
|
+
* @returns The global registry (scoped registries are Phase 2)
|
|
460
|
+
*
|
|
461
|
+
* @remarks
|
|
462
|
+
* Phase 2 stub: Returns global registry with warning in DEV_MODE.
|
|
463
|
+
* Future implementation will support isolated command registries
|
|
464
|
+
* per shadow root for micro-frontend architectures.
|
|
465
|
+
*
|
|
466
|
+
* @public
|
|
467
|
+
*/
|
|
468
|
+
forScope(_shadowRoot) {
|
|
469
|
+
if (isDevMode()) {
|
|
470
|
+
console.warn("[fast-af] Scoped registries are Phase 2. Returning global registry.");
|
|
471
|
+
}
|
|
472
|
+
return this;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Sets a telemetry handler for command events.
|
|
476
|
+
*
|
|
477
|
+
* @param handler - Function to handle telemetry events
|
|
478
|
+
*
|
|
479
|
+
* @remarks
|
|
480
|
+
* Events emitted:
|
|
481
|
+
* - "command.registered" - \{ id, label \}
|
|
482
|
+
* - "command.unregistered" - \{ id \}
|
|
483
|
+
* - "command.executed" - \{ id, duration, success \}
|
|
484
|
+
* - "command.rejected" - \{ id, origin, code \} (external invocation refused)
|
|
485
|
+
*
|
|
486
|
+
* @example
|
|
487
|
+
* ```typescript
|
|
488
|
+
* CommandRegistry.setTelemetryHandler((event, data) => {
|
|
489
|
+
* analytics.track(event, data);
|
|
490
|
+
* });
|
|
491
|
+
* ```
|
|
492
|
+
*
|
|
493
|
+
* @public
|
|
494
|
+
*/
|
|
495
|
+
setTelemetryHandler(handler) {
|
|
496
|
+
this.telemetryHandler = handler;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Adds an interceptor to the command execution chain.
|
|
500
|
+
*
|
|
501
|
+
* @param interceptor - The interceptor to add
|
|
502
|
+
*
|
|
503
|
+
* @remarks
|
|
504
|
+
* - Interceptors run in priority order (lower priority runs first)
|
|
505
|
+
* - Default priority is 100
|
|
506
|
+
* - Multiple interceptors can have the same priority
|
|
507
|
+
* - Interceptors with the same name will not be duplicated
|
|
508
|
+
*
|
|
509
|
+
* @example
|
|
510
|
+
* ```typescript
|
|
511
|
+
* CommandRegistry.addInterceptor({
|
|
512
|
+
* name: 'logging',
|
|
513
|
+
* priority: 1,
|
|
514
|
+
* intercept: async (cmd, args, next) => {
|
|
515
|
+
* console.log('Executing:', cmd.id);
|
|
516
|
+
* const result = await next();
|
|
517
|
+
* console.log('Result:', result);
|
|
518
|
+
* return result;
|
|
519
|
+
* }
|
|
520
|
+
* });
|
|
521
|
+
* ```
|
|
522
|
+
*
|
|
523
|
+
* @public
|
|
524
|
+
*/
|
|
525
|
+
addInterceptor(interceptor) {
|
|
526
|
+
var _a;
|
|
527
|
+
// Avoid duplicates by name
|
|
528
|
+
if (this.interceptors.some(i => i.name === interceptor.name)) {
|
|
529
|
+
if (isDevMode()) {
|
|
530
|
+
console.warn(`[fast-af] Interceptor "${interceptor.name}" already registered. Skipping.`);
|
|
531
|
+
}
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
this.interceptors.push(interceptor);
|
|
535
|
+
this.interceptors.sort((a, b) => { var _a, _b; return ((_a = a.priority) !== null && _a !== void 0 ? _a : 100) - ((_b = b.priority) !== null && _b !== void 0 ? _b : 100); });
|
|
536
|
+
this.emitTelemetry("interceptor.added", {
|
|
537
|
+
name: interceptor.name,
|
|
538
|
+
priority: (_a = interceptor.priority) !== null && _a !== void 0 ? _a : 100,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Removes an interceptor by name.
|
|
543
|
+
*
|
|
544
|
+
* @param name - The name of the interceptor to remove
|
|
545
|
+
*
|
|
546
|
+
* @remarks
|
|
547
|
+
* - No-op if interceptor doesn't exist
|
|
548
|
+
* - Emits telemetry event on removal
|
|
549
|
+
*
|
|
550
|
+
* @example
|
|
551
|
+
* ```typescript
|
|
552
|
+
* CommandRegistry.removeInterceptor('confirm');
|
|
553
|
+
* ```
|
|
554
|
+
*
|
|
555
|
+
* @public
|
|
556
|
+
*/
|
|
557
|
+
removeInterceptor(name) {
|
|
558
|
+
const before = this.interceptors.length;
|
|
559
|
+
this.interceptors = this.interceptors.filter(i => i.name !== name);
|
|
560
|
+
if (this.interceptors.length < before) {
|
|
561
|
+
this.emitTelemetry("interceptor.removed", { name });
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Adds a post-hook to observe command execution results.
|
|
566
|
+
*
|
|
567
|
+
* @param hook - The post-hook to add
|
|
568
|
+
*
|
|
569
|
+
* @remarks
|
|
570
|
+
* - Post-hooks run after command execution completes
|
|
571
|
+
* - They cannot modify the result
|
|
572
|
+
* - Post-hooks with the same name will not be duplicated
|
|
573
|
+
*
|
|
574
|
+
* @example
|
|
575
|
+
* ```typescript
|
|
576
|
+
* CommandRegistry.addPostHook({
|
|
577
|
+
* name: 'undo',
|
|
578
|
+
* afterExecute: (cmd, args, result) => {
|
|
579
|
+
* if (result.success) {
|
|
580
|
+
* undoStack.push({ cmd, args, result });
|
|
581
|
+
* }
|
|
582
|
+
* }
|
|
583
|
+
* });
|
|
584
|
+
* ```
|
|
585
|
+
*
|
|
586
|
+
* @public
|
|
587
|
+
*/
|
|
588
|
+
addPostHook(hook) {
|
|
589
|
+
// Avoid duplicates by name
|
|
590
|
+
if (this.postHooks.some(h => h.name === hook.name)) {
|
|
591
|
+
if (isDevMode()) {
|
|
592
|
+
console.warn(`[fast-af] PostHook "${hook.name}" already registered. Skipping.`);
|
|
593
|
+
}
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
this.postHooks.push(hook);
|
|
597
|
+
this.emitTelemetry("posthook.added", { name: hook.name });
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Removes a post-hook by name.
|
|
601
|
+
*
|
|
602
|
+
* @param name - The name of the post-hook to remove
|
|
603
|
+
*
|
|
604
|
+
* @remarks
|
|
605
|
+
* - No-op if post-hook doesn't exist
|
|
606
|
+
* - Emits telemetry event on removal
|
|
607
|
+
*
|
|
608
|
+
* @example
|
|
609
|
+
* ```typescript
|
|
610
|
+
* CommandRegistry.removePostHook('undo');
|
|
611
|
+
* ```
|
|
612
|
+
*
|
|
613
|
+
* @public
|
|
614
|
+
*/
|
|
615
|
+
removePostHook(name) {
|
|
616
|
+
const before = this.postHooks.length;
|
|
617
|
+
this.postHooks = this.postHooks.filter(h => h.name !== name);
|
|
618
|
+
if (this.postHooks.length < before) {
|
|
619
|
+
this.emitTelemetry("posthook.removed", { name });
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Notifies all listeners of command changes.
|
|
624
|
+
* @internal
|
|
625
|
+
*/
|
|
626
|
+
notify(added, removed) {
|
|
627
|
+
this.listeners.forEach(listener => {
|
|
628
|
+
try {
|
|
629
|
+
listener(added, removed);
|
|
630
|
+
}
|
|
631
|
+
catch (error) {
|
|
632
|
+
if (isDevMode()) {
|
|
633
|
+
console.error("[fast-af] Command listener threw an error:", error);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Emits a telemetry event if handler is set.
|
|
640
|
+
* @internal
|
|
641
|
+
*/
|
|
642
|
+
emitTelemetry(event, data) {
|
|
643
|
+
var _a;
|
|
644
|
+
try {
|
|
645
|
+
(_a = this.telemetryHandler) === null || _a === void 0 ? void 0 : _a.call(this, event, data);
|
|
646
|
+
}
|
|
647
|
+
catch (error) {
|
|
648
|
+
if (isDevMode()) {
|
|
649
|
+
console.error("[fast-af] Telemetry handler threw an error:", error);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Emits telemetry for an externally-originated invocation that was refused
|
|
655
|
+
* before reaching the command, and passes the failure through.
|
|
656
|
+
* @internal
|
|
657
|
+
*/
|
|
658
|
+
reject(id, origin, result) {
|
|
659
|
+
this.emitTelemetry("command.rejected", {
|
|
660
|
+
id,
|
|
661
|
+
origin,
|
|
662
|
+
code: result.success ? void 0 : result.error.code,
|
|
663
|
+
});
|
|
664
|
+
return result;
|
|
665
|
+
}
|
|
666
|
+
normalizeResult(id, result) {
|
|
667
|
+
if (result &&
|
|
668
|
+
typeof result === "object" &&
|
|
669
|
+
result.success === true &&
|
|
670
|
+
"data" in result) {
|
|
671
|
+
return result;
|
|
672
|
+
}
|
|
673
|
+
if (result &&
|
|
674
|
+
typeof result === "object" &&
|
|
675
|
+
result.success === false &&
|
|
676
|
+
"error" in result) {
|
|
677
|
+
return result;
|
|
678
|
+
}
|
|
679
|
+
return failure({
|
|
680
|
+
code: ErrorCodes.INTERNAL_ERROR,
|
|
681
|
+
message: `Command returned an invalid result: ${id}`,
|
|
682
|
+
suggestion: "Return success(data) or failure(error) from the command",
|
|
683
|
+
retryable: false,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Builds the interceptor chain for command execution.
|
|
688
|
+
* @internal
|
|
689
|
+
*/
|
|
690
|
+
buildInterceptorChain(command, args, handler) {
|
|
691
|
+
// Build chain from back to front
|
|
692
|
+
let chain = handler;
|
|
693
|
+
for (let i = this.interceptors.length - 1; i >= 0; i--) {
|
|
694
|
+
const interceptor = this.interceptors[i];
|
|
695
|
+
const next = chain;
|
|
696
|
+
chain = () => interceptor.intercept(command, args, next);
|
|
697
|
+
}
|
|
698
|
+
return chain;
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Runs all post-hooks after command execution.
|
|
702
|
+
* @internal
|
|
703
|
+
*/
|
|
704
|
+
runPostHooks(command, args, result) {
|
|
705
|
+
for (const hook of this.postHooks) {
|
|
706
|
+
try {
|
|
707
|
+
hook.afterExecute(command, args, result);
|
|
708
|
+
}
|
|
709
|
+
catch (error) {
|
|
710
|
+
if (isDevMode()) {
|
|
711
|
+
console.error(`[fast-af] PostHook "${hook.name}" threw an error:`, error);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Global CommandRegistry singleton.
|
|
719
|
+
*
|
|
720
|
+
* @remarks
|
|
721
|
+
* The CommandRegistry is the central hub for command management.
|
|
722
|
+
* Commands are registered automatically when FASTElement components
|
|
723
|
+
* with \@command decorators connect to the DOM.
|
|
724
|
+
*
|
|
725
|
+
* Key features:
|
|
726
|
+
* - SSR-safe with lazy initialization
|
|
727
|
+
* - Reactive: subscribe to changes via onChange()
|
|
728
|
+
* - Command palette integration via getActions()
|
|
729
|
+
* - Telemetry hooks for analytics
|
|
730
|
+
*
|
|
731
|
+
* @example
|
|
732
|
+
* ```typescript
|
|
733
|
+
* import { CommandRegistry, success, failure } from '\@lushly-dev/fast-element/command.js';
|
|
734
|
+
*
|
|
735
|
+
* // Manual command registration (usually done automatically)
|
|
736
|
+
* CommandRegistry.register('custom.action', \{
|
|
737
|
+
* id: 'custom.action',
|
|
738
|
+
* label: 'Custom Action',
|
|
739
|
+
* execute: () => success(\{ done: true \}),
|
|
740
|
+
* canExecute: true,
|
|
741
|
+
* when: true,
|
|
742
|
+
* source: new WeakRef(document.body),
|
|
743
|
+
* \});
|
|
744
|
+
*
|
|
745
|
+
* // Execute command
|
|
746
|
+
* const result = await CommandRegistry.execute('custom.action');
|
|
747
|
+
*
|
|
748
|
+
* // Command palette integration
|
|
749
|
+
* const actions = CommandRegistry.getActions();
|
|
750
|
+
* ninjaKeys.data = actions;
|
|
751
|
+
* ```
|
|
752
|
+
*
|
|
753
|
+
* @public
|
|
754
|
+
*/
|
|
755
|
+
export const CommandRegistry = new CommandRegistryImpl();
|