@soederpop/luca 0.0.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/CLAUDE.md +71 -0
- package/README.md +78 -0
- package/bun.lock +2928 -0
- package/bunfig.toml +3 -0
- package/commands/audit-docs.ts +740 -0
- package/commands/build-scaffolds.ts +154 -0
- package/commands/generate-api-docs.ts +114 -0
- package/commands/update-introspection.ts +67 -0
- package/docs/CLI.md +335 -0
- package/docs/README.md +88 -0
- package/docs/TABLE-OF-CONTENTS.md +157 -0
- package/docs/apis/clients/elevenlabs.md +84 -0
- package/docs/apis/clients/graph.md +56 -0
- package/docs/apis/clients/openai.md +69 -0
- package/docs/apis/clients/rest.md +41 -0
- package/docs/apis/clients/websocket.md +107 -0
- package/docs/apis/features/agi/assistant.md +471 -0
- package/docs/apis/features/agi/assistants-manager.md +154 -0
- package/docs/apis/features/agi/claude-code.md +602 -0
- package/docs/apis/features/agi/conversation-history.md +352 -0
- package/docs/apis/features/agi/conversation.md +333 -0
- package/docs/apis/features/agi/docs-reader.md +121 -0
- package/docs/apis/features/agi/openai-codex.md +318 -0
- package/docs/apis/features/agi/openapi.md +138 -0
- package/docs/apis/features/agi/semantic-search.md +387 -0
- package/docs/apis/features/agi/skills-library.md +216 -0
- package/docs/apis/features/node/container-link.md +133 -0
- package/docs/apis/features/node/content-db.md +313 -0
- package/docs/apis/features/node/disk-cache.md +379 -0
- package/docs/apis/features/node/dns.md +651 -0
- package/docs/apis/features/node/docker.md +705 -0
- package/docs/apis/features/node/downloader.md +81 -0
- package/docs/apis/features/node/esbuild.md +59 -0
- package/docs/apis/features/node/file-manager.md +182 -0
- package/docs/apis/features/node/fs.md +581 -0
- package/docs/apis/features/node/git.md +330 -0
- package/docs/apis/features/node/google-auth.md +174 -0
- package/docs/apis/features/node/google-calendar.md +187 -0
- package/docs/apis/features/node/google-docs.md +151 -0
- package/docs/apis/features/node/google-drive.md +225 -0
- package/docs/apis/features/node/google-sheets.md +179 -0
- package/docs/apis/features/node/grep.md +290 -0
- package/docs/apis/features/node/helpers.md +135 -0
- package/docs/apis/features/node/ink.md +334 -0
- package/docs/apis/features/node/ipc-socket.md +260 -0
- package/docs/apis/features/node/json-tree.md +86 -0
- package/docs/apis/features/node/launcher-app-command-listener.md +145 -0
- package/docs/apis/features/node/networking.md +281 -0
- package/docs/apis/features/node/nlp.md +133 -0
- package/docs/apis/features/node/opener.md +97 -0
- package/docs/apis/features/node/os.md +118 -0
- package/docs/apis/features/node/package-finder.md +402 -0
- package/docs/apis/features/node/postgres.md +212 -0
- package/docs/apis/features/node/proc.md +430 -0
- package/docs/apis/features/node/process-manager.md +210 -0
- package/docs/apis/features/node/python.md +278 -0
- package/docs/apis/features/node/repl.md +88 -0
- package/docs/apis/features/node/runpod.md +673 -0
- package/docs/apis/features/node/secure-shell.md +169 -0
- package/docs/apis/features/node/semantic-search.md +401 -0
- package/docs/apis/features/node/sqlite.md +211 -0
- package/docs/apis/features/node/telegram.md +254 -0
- package/docs/apis/features/node/tts.md +118 -0
- package/docs/apis/features/node/ui.md +703 -0
- package/docs/apis/features/node/vault.md +64 -0
- package/docs/apis/features/node/vm.md +84 -0
- package/docs/apis/features/node/window-manager.md +337 -0
- package/docs/apis/features/node/yaml-tree.md +85 -0
- package/docs/apis/features/node/yaml.md +176 -0
- package/docs/apis/features/web/asset-loader.md +47 -0
- package/docs/apis/features/web/container-link.md +133 -0
- package/docs/apis/features/web/esbuild.md +59 -0
- package/docs/apis/features/web/helpers.md +135 -0
- package/docs/apis/features/web/network.md +30 -0
- package/docs/apis/features/web/speech.md +55 -0
- package/docs/apis/features/web/vault.md +64 -0
- package/docs/apis/features/web/vm.md +84 -0
- package/docs/apis/features/web/voice.md +67 -0
- package/docs/apis/servers/express.md +127 -0
- package/docs/apis/servers/mcp.md +213 -0
- package/docs/apis/servers/websocket.md +99 -0
- package/docs/documentation-audit.md +134 -0
- package/docs/examples/content-db.md +77 -0
- package/docs/examples/disk-cache.md +83 -0
- package/docs/examples/docker.md +101 -0
- package/docs/examples/downloader.md +70 -0
- package/docs/examples/esbuild.md +80 -0
- package/docs/examples/file-manager.md +82 -0
- package/docs/examples/fs.md +83 -0
- package/docs/examples/git.md +85 -0
- package/docs/examples/google-auth.md +88 -0
- package/docs/examples/google-calendar.md +94 -0
- package/docs/examples/google-docs.md +82 -0
- package/docs/examples/google-drive.md +96 -0
- package/docs/examples/google-sheets.md +95 -0
- package/docs/examples/grep.md +85 -0
- package/docs/examples/ink-blocks.md +75 -0
- package/docs/examples/ink-renderer.md +41 -0
- package/docs/examples/ink.md +103 -0
- package/docs/examples/ipc-socket.md +103 -0
- package/docs/examples/json-tree.md +91 -0
- package/docs/examples/launcher-app-command-listener.md +120 -0
- package/docs/examples/networking.md +58 -0
- package/docs/examples/nlp.md +91 -0
- package/docs/examples/opener.md +78 -0
- package/docs/examples/os.md +72 -0
- package/docs/examples/package-finder.md +89 -0
- package/docs/examples/port-exposer.md +89 -0
- package/docs/examples/postgres.md +91 -0
- package/docs/examples/proc.md +81 -0
- package/docs/examples/process-manager.md +79 -0
- package/docs/examples/python.md +91 -0
- package/docs/examples/repl.md +93 -0
- package/docs/examples/runpod.md +119 -0
- package/docs/examples/secure-shell.md +92 -0
- package/docs/examples/sqlite.md +86 -0
- package/docs/examples/telegram.md +77 -0
- package/docs/examples/tts.md +86 -0
- package/docs/examples/ui.md +80 -0
- package/docs/examples/vault.md +70 -0
- package/docs/examples/vm.md +86 -0
- package/docs/examples/window-manager.md +125 -0
- package/docs/examples/yaml-tree.md +93 -0
- package/docs/examples/yaml.md +104 -0
- package/docs/ideas/class-registration-refactor-possibilities.md +197 -0
- package/docs/ideas/container-use-api.md +9 -0
- package/docs/ideas/easy-auth-for-express-servers-and-luca-serve.md +0 -0
- package/docs/ideas/feature-stacks.md +22 -0
- package/docs/ideas/luca-cli-self-sufficiency-demo.md +23 -0
- package/docs/ideas/mcp-design.md +9 -0
- package/docs/ideas/web-container-debugging-feature.md +13 -0
- package/docs/introspection-audit.md +49 -0
- package/docs/introspection.md +154 -0
- package/docs/mcp/readme.md +162 -0
- package/docs/models.ts +38 -0
- package/docs/philosophy.md +85 -0
- package/docs/principles.md +7 -0
- package/docs/prompts/audit-codebase-for-failures-to-use-the-container.md +34 -0
- package/docs/prompts/mcp-test-easy-command.md +27 -0
- package/docs/reports/assistant-bugs.md +38 -0
- package/docs/reports/attach-pattern-usage.md +18 -0
- package/docs/reports/code-audit-results.md +391 -0
- package/docs/reports/introspection-audit-tasks.md +378 -0
- package/docs/reports/luca-mcp-improvements.md +128 -0
- package/docs/scaffolds/client.md +140 -0
- package/docs/scaffolds/command.md +106 -0
- package/docs/scaffolds/endpoint.md +176 -0
- package/docs/scaffolds/feature.md +148 -0
- package/docs/scaffolds/server.md +187 -0
- package/docs/tasks/web-container-helper-discovery.md +71 -0
- package/docs/todos.md +1 -0
- package/docs/tutorials/01-getting-started.md +106 -0
- package/docs/tutorials/02-container.md +210 -0
- package/docs/tutorials/03-scripts.md +194 -0
- package/docs/tutorials/04-features-overview.md +196 -0
- package/docs/tutorials/05-state-and-events.md +171 -0
- package/docs/tutorials/06-servers.md +157 -0
- package/docs/tutorials/07-endpoints.md +198 -0
- package/docs/tutorials/08-commands.md +171 -0
- package/docs/tutorials/09-clients.md +162 -0
- package/docs/tutorials/10-creating-features.md +198 -0
- package/docs/tutorials/11-contentbase.md +191 -0
- package/docs/tutorials/12-assistants.md +215 -0
- package/docs/tutorials/13-introspection.md +147 -0
- package/docs/tutorials/14-type-system.md +174 -0
- package/docs/tutorials/15-project-patterns.md +222 -0
- package/docs/tutorials/16-google-features.md +534 -0
- package/docs/tutorials/17-tui-blocks.md +530 -0
- package/docs/tutorials/18-semantic-search.md +334 -0
- package/index.ts +1 -0
- package/luca.console.ts +9 -0
- package/main.py +6 -0
- package/package.json +154 -0
- package/pyproject.toml +7 -0
- package/scripts/animations/chrome-glitch.ts +55 -0
- package/scripts/animations/index.ts +16 -0
- package/scripts/animations/neon-pulse.ts +64 -0
- package/scripts/animations/types.ts +6 -0
- package/scripts/build-web.ts +28 -0
- package/scripts/examples/ask-luca-expert.ts +42 -0
- package/scripts/examples/assistant-questions.ts +12 -0
- package/scripts/examples/excalidraw-expert.ts +75 -0
- package/scripts/examples/expert-chat.ts +0 -0
- package/scripts/examples/file-manager.ts +14 -0
- package/scripts/examples/ideas.ts +12 -0
- package/scripts/examples/interactive-chat.ts +20 -0
- package/scripts/examples/openai-tool-calls.ts +113 -0
- package/scripts/examples/opening-a-web-browser.ts +5 -0
- package/scripts/examples/telegram-bot.ts +79 -0
- package/scripts/examples/telegram-ink-ui.ts +302 -0
- package/scripts/examples/using-assistant-with-mcp.ts +560 -0
- package/scripts/examples/using-claude-code.ts +10 -0
- package/scripts/examples/using-contentdb.ts +35 -0
- package/scripts/examples/using-conversations.ts +35 -0
- package/scripts/examples/using-disk-cache.ts +10 -0
- package/scripts/examples/using-docker-shell.ts +75 -0
- package/scripts/examples/using-elevenlabs.ts +25 -0
- package/scripts/examples/using-google-calendar.ts +57 -0
- package/scripts/examples/using-google-docs.ts +74 -0
- package/scripts/examples/using-google-drive.ts +74 -0
- package/scripts/examples/using-google-sheets.ts +89 -0
- package/scripts/examples/using-nlp.ts +55 -0
- package/scripts/examples/using-ollama.ts +10 -0
- package/scripts/examples/using-openai-codex.ts +23 -0
- package/scripts/examples/using-postgres.ts +55 -0
- package/scripts/examples/using-runpod.ts +32 -0
- package/scripts/examples/using-tts.ts +40 -0
- package/scripts/examples/vm-loading-esm-modules.ts +16 -0
- package/scripts/scaffold.ts +391 -0
- package/scripts/scratch.ts +15 -0
- package/scripts/test-command-listener.ts +123 -0
- package/scripts/test-window-manager-lifecycle.ts +86 -0
- package/scripts/test-window-manager.ts +43 -0
- package/scripts/update-introspection-data.ts +58 -0
- package/src/agi/README.md +14 -0
- package/src/agi/container.server.ts +114 -0
- package/src/agi/endpoints/ask.ts +60 -0
- package/src/agi/endpoints/conversations/[id].ts +45 -0
- package/src/agi/endpoints/conversations.ts +31 -0
- package/src/agi/endpoints/experts.ts +37 -0
- package/src/agi/features/assistant.ts +767 -0
- package/src/agi/features/assistants-manager.ts +260 -0
- package/src/agi/features/claude-code.ts +1111 -0
- package/src/agi/features/conversation-history.ts +497 -0
- package/src/agi/features/conversation.ts +799 -0
- package/src/agi/features/openai-codex.ts +631 -0
- package/src/agi/features/openapi.ts +438 -0
- package/src/agi/features/skills-library.ts +425 -0
- package/src/agi/index.ts +6 -0
- package/src/agi/lib/token-counter.ts +122 -0
- package/src/browser.ts +25 -0
- package/src/bus.ts +100 -0
- package/src/cli/cli.ts +70 -0
- package/src/client.ts +461 -0
- package/src/clients/civitai/index.ts +541 -0
- package/src/clients/client-template.ts +41 -0
- package/src/clients/comfyui/index.ts +597 -0
- package/src/clients/elevenlabs/index.ts +291 -0
- package/src/clients/openai/index.ts +451 -0
- package/src/clients/supabase/index.ts +366 -0
- package/src/command.ts +164 -0
- package/src/commands/chat.ts +182 -0
- package/src/commands/console.ts +192 -0
- package/src/commands/describe.ts +433 -0
- package/src/commands/eval.ts +116 -0
- package/src/commands/help.ts +214 -0
- package/src/commands/index.ts +14 -0
- package/src/commands/mcp.ts +64 -0
- package/src/commands/prompt.ts +807 -0
- package/src/commands/run.ts +257 -0
- package/src/commands/sandbox-mcp.ts +439 -0
- package/src/commands/scaffold.ts +79 -0
- package/src/commands/serve.ts +172 -0
- package/src/container.ts +781 -0
- package/src/endpoint.ts +340 -0
- package/src/feature.ts +75 -0
- package/src/hash-object.ts +97 -0
- package/src/helper.ts +543 -0
- package/src/introspection/generated.agi.ts +23388 -0
- package/src/introspection/generated.node.ts +18899 -0
- package/src/introspection/generated.web.ts +2021 -0
- package/src/introspection/index.ts +256 -0
- package/src/introspection/scan.ts +912 -0
- package/src/node/container.ts +354 -0
- package/src/node/feature.ts +13 -0
- package/src/node/features/container-link.ts +558 -0
- package/src/node/features/content-db.ts +475 -0
- package/src/node/features/disk-cache.ts +382 -0
- package/src/node/features/dns.ts +655 -0
- package/src/node/features/docker.ts +912 -0
- package/src/node/features/downloader.ts +92 -0
- package/src/node/features/esbuild.ts +68 -0
- package/src/node/features/file-manager.ts +357 -0
- package/src/node/features/fs.ts +534 -0
- package/src/node/features/git.ts +492 -0
- package/src/node/features/google-auth.ts +502 -0
- package/src/node/features/google-calendar.ts +300 -0
- package/src/node/features/google-docs.ts +404 -0
- package/src/node/features/google-drive.ts +339 -0
- package/src/node/features/google-sheets.ts +279 -0
- package/src/node/features/grep.ts +406 -0
- package/src/node/features/helpers.ts +374 -0
- package/src/node/features/ink.ts +490 -0
- package/src/node/features/ipc-socket.ts +459 -0
- package/src/node/features/json-tree.ts +188 -0
- package/src/node/features/launcher-app-command-listener.ts +388 -0
- package/src/node/features/networking.ts +925 -0
- package/src/node/features/nlp.ts +211 -0
- package/src/node/features/opener.ts +166 -0
- package/src/node/features/os.ts +157 -0
- package/src/node/features/package-finder.ts +539 -0
- package/src/node/features/port-exposer.ts +342 -0
- package/src/node/features/postgres.ts +273 -0
- package/src/node/features/proc.ts +502 -0
- package/src/node/features/process-manager.ts +542 -0
- package/src/node/features/python.ts +444 -0
- package/src/node/features/repl.ts +194 -0
- package/src/node/features/runpod.ts +802 -0
- package/src/node/features/secure-shell.ts +248 -0
- package/src/node/features/semantic-search.ts +924 -0
- package/src/node/features/sqlite.ts +289 -0
- package/src/node/features/telegram.ts +342 -0
- package/src/node/features/tts.ts +184 -0
- package/src/node/features/ui.ts +857 -0
- package/src/node/features/vault.ts +164 -0
- package/src/node/features/vm.ts +312 -0
- package/src/node/features/window-manager.ts +804 -0
- package/src/node/features/yaml-tree.ts +149 -0
- package/src/node/features/yaml.ts +132 -0
- package/src/node.ts +70 -0
- package/src/react/index.ts +175 -0
- package/src/registry.ts +199 -0
- package/src/scaffolds/generated.ts +1613 -0
- package/src/scaffolds/template.ts +37 -0
- package/src/schemas/base.ts +255 -0
- package/src/server.ts +135 -0
- package/src/servers/express.ts +209 -0
- package/src/servers/mcp.ts +805 -0
- package/src/servers/socket.ts +120 -0
- package/src/state.ts +101 -0
- package/src/web/clients/socket.ts +82 -0
- package/src/web/container.ts +74 -0
- package/src/web/extension.ts +30 -0
- package/src/web/feature.ts +12 -0
- package/src/web/features/asset-loader.ts +64 -0
- package/src/web/features/container-link.ts +385 -0
- package/src/web/features/esbuild.ts +79 -0
- package/src/web/features/helpers.ts +267 -0
- package/src/web/features/network.ts +61 -0
- package/src/web/features/speech.ts +87 -0
- package/src/web/features/vault.ts +189 -0
- package/src/web/features/vm.ts +78 -0
- package/src/web/features/voice-recognition.ts +129 -0
- package/src/web/shims/isomorphic-vm.ts +149 -0
- package/test/bus.test.ts +134 -0
- package/test/clients-servers.test.ts +216 -0
- package/test/container-link.test.ts +274 -0
- package/test/features.test.ts +160 -0
- package/test/integration.test.ts +787 -0
- package/test/node-container.test.ts +121 -0
- package/test/rate-limit.test.ts +272 -0
- package/test/semantic-search.test.ts +550 -0
- package/test/state.test.ts +121 -0
- package/test-integration/assistant.test.ts +138 -0
- package/test-integration/assistants-manager.test.ts +123 -0
- package/test-integration/claude-code.test.ts +98 -0
- package/test-integration/conversation-history.test.ts +205 -0
- package/test-integration/conversation.test.ts +137 -0
- package/test-integration/elevenlabs.test.ts +55 -0
- package/test-integration/google-services.test.ts +80 -0
- package/test-integration/helpers.ts +89 -0
- package/test-integration/openai-codex.test.ts +93 -0
- package/test-integration/runpod.test.ts +58 -0
- package/test-integration/server-endpoints.test.ts +97 -0
- package/test-integration/skills-library.test.ts +157 -0
- package/test-integration/telegram.test.ts +46 -0
- package/tsconfig.json +58 -0
- package/uv.lock +8 -0
package/src/container.ts
ADDED
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { Bus } from './bus'
|
|
3
|
+
import { SetStateValue, State } from './state'
|
|
4
|
+
import { AvailableFeatures, features, Feature, FeaturesRegistry } from './feature'
|
|
5
|
+
import { Helper } from './helper'
|
|
6
|
+
import uuid from 'node-uuid'
|
|
7
|
+
import hashObject from './hash-object'
|
|
8
|
+
import { uniq, keyBy, uniqBy, groupBy, debounce, throttle, mapValues, mapKeys, pick, get, set, omit, kebabCase, camelCase, upperFirst, lowerFirst } from 'lodash-es'
|
|
9
|
+
import { pluralize, singularize } from 'inflect'
|
|
10
|
+
import { z } from 'zod'
|
|
11
|
+
import { ContainerStateSchema, describeZodShape } from './schemas/base'
|
|
12
|
+
import { getContainerBuildTimeData, type ContainerIntrospection, type RegistryIntrospection, type IntrospectionSection } from './introspection/index'
|
|
13
|
+
|
|
14
|
+
export { z }
|
|
15
|
+
|
|
16
|
+
const { v4 } = uuid
|
|
17
|
+
|
|
18
|
+
const stringUtils = { kebabCase, camelCase, upperFirst, lowerFirst, pluralize, singularize }
|
|
19
|
+
|
|
20
|
+
export type { AvailableFeatures }
|
|
21
|
+
|
|
22
|
+
// I want the InstanceType of each value of AvailableFeatures, AvailableClients, whatever
|
|
23
|
+
export type AvailableInstanceTypes<T> = {
|
|
24
|
+
[K in keyof T]: T[K] extends new (...args: any) => any ? InstanceType<T[K]> : never
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* You'll want to use module augmentation to add your own options to the ContainerArgv interface
|
|
29
|
+
*/
|
|
30
|
+
export interface ContainerArgv {
|
|
31
|
+
_?: string[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type ContainerState = z.infer<typeof ContainerStateSchema>
|
|
35
|
+
|
|
36
|
+
export interface Plugin<T> {
|
|
37
|
+
attach?: (container: Container<any> & T, options?: any) => any
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type Extension<T> = 'string' | keyof AvailableFeatures | Plugin<T> | { attach: (container: Container<any>, options?: any) => T}
|
|
41
|
+
|
|
42
|
+
export interface ContainerContext<T extends AvailableFeatures = any> {
|
|
43
|
+
container: Container<T>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Containers are single objects that contain state, an event bus, and registries of helpers such as:
|
|
48
|
+
*
|
|
49
|
+
* - features
|
|
50
|
+
* - clients
|
|
51
|
+
* - servers
|
|
52
|
+
*
|
|
53
|
+
* A Helper represents a category of components in your program which have a common interface, e.g. all servers can be started / stopped, all features can be enabled, if supported, all clients can connect to something.
|
|
54
|
+
*
|
|
55
|
+
* A Helper can be introspected at runtime to learn about the interface of the helper. A helper has state, and emits events.
|
|
56
|
+
*
|
|
57
|
+
* You can design your own containers and load them up with the helpers you want for that environment.
|
|
58
|
+
*/
|
|
59
|
+
export class Container<Features extends AvailableFeatures = AvailableFeatures, ContainerState extends ContainerState = ContainerState > {
|
|
60
|
+
static stateSchema = ContainerStateSchema
|
|
61
|
+
|
|
62
|
+
readonly uuid = v4()
|
|
63
|
+
private readonly _events = new Bus()
|
|
64
|
+
private readonly _state: State<ContainerState>
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* You can use module augmentation to define the starting interface for your container
|
|
68
|
+
* whether it is process.argv, process.env, or some combination thereof
|
|
69
|
+
*/
|
|
70
|
+
readonly options: ContainerArgv
|
|
71
|
+
|
|
72
|
+
constructor(options: ContainerArgv) {
|
|
73
|
+
this.options = options
|
|
74
|
+
this._state = new State<ContainerState>()
|
|
75
|
+
this.z = z
|
|
76
|
+
this.state
|
|
77
|
+
.set('enabledFeatures', [])
|
|
78
|
+
.set('started', false)
|
|
79
|
+
.set('registries', ['features'])
|
|
80
|
+
.set('factories', ['feature'])
|
|
81
|
+
|
|
82
|
+
this._hide('options', '_state', '_events', 'uuid', '_plugins', 'z')
|
|
83
|
+
|
|
84
|
+
this.on('featureEnabled', (featureId: string, feature: any) => {
|
|
85
|
+
const featureKey = featureId.replace(/^features\./,'')
|
|
86
|
+
const mapKey = `${this.uuid}/${featureKey}`
|
|
87
|
+
featureIdToHelperCacheKeyMap.set(mapKey, feature.cacheKey)
|
|
88
|
+
this.state.set('enabledFeatures', uniq([
|
|
89
|
+
...this.state.get('enabledFeatures')!,
|
|
90
|
+
featureKey
|
|
91
|
+
]))
|
|
92
|
+
this.addContext(featureKey, feature)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
this.state.observe(() => {
|
|
96
|
+
this.emit('stateChange', this.state.current)
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Creates a new subcontainer instance of the same concrete Container subclass.
|
|
102
|
+
*
|
|
103
|
+
* The new instance is constructed with the same options as this container,
|
|
104
|
+
* shallow-merged with any overrides you provide. This preserves the runtime
|
|
105
|
+
* container type (e.g. NodeContainer, BrowserContainer, etc.).
|
|
106
|
+
*
|
|
107
|
+
* @param options - Options to override for the new container instance.
|
|
108
|
+
* @returns A new container instance of the same subclass.
|
|
109
|
+
*/
|
|
110
|
+
subcontainer<This extends Container<any, any>>(
|
|
111
|
+
this: This,
|
|
112
|
+
options: ConstructorParameters<This['constructor']>[0]
|
|
113
|
+
): This {
|
|
114
|
+
const Ctor = this.constructor as new (options: ConstructorParameters<This['constructor']>[0]) => This
|
|
115
|
+
const mergedOptions = {
|
|
116
|
+
...(this as any).options || {},
|
|
117
|
+
...(options || {}),
|
|
118
|
+
}
|
|
119
|
+
return new Ctor(mergedOptions)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
z!: typeof z
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
/** The observable state object for this container instance. */
|
|
126
|
+
get state() {
|
|
127
|
+
return this._state
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Returns the list of shortcut IDs for all currently enabled features. */
|
|
131
|
+
get enabledFeatureIds() {
|
|
132
|
+
return this.state.get('enabledFeatures') || []
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Returns a map of enabled feature shortcut IDs to their instances. */
|
|
136
|
+
get enabledFeatures() : Partial<AvailableInstanceTypes<Features>> {
|
|
137
|
+
return Object.fromEntries(
|
|
138
|
+
this.enabledFeatureIds.map((featureId) => [featureId, (this as any)[featureId]])
|
|
139
|
+
) as AvailableInstanceTypes<Features>
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
utils = {
|
|
143
|
+
hashObject: (obj: any) => hashObject(obj),
|
|
144
|
+
get stringUtils() { return stringUtils },
|
|
145
|
+
uuid: () => v4(),
|
|
146
|
+
lodash: {
|
|
147
|
+
uniq, keyBy, uniqBy, groupBy, debounce, throttle, mapValues, mapKeys, pick, get, set, omit,
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Add a value to the container's shared context, which is passed to all helper instances.
|
|
153
|
+
* Accepts either a key and value, or an object of key-value pairs to add.
|
|
154
|
+
*
|
|
155
|
+
* @param {K} key - The context key (or object of key-value pairs)
|
|
156
|
+
* @param {ContainerContext[K]} value - The context value (omit when passing an object)
|
|
157
|
+
*/
|
|
158
|
+
addContext<K extends keyof ContainerContext>(key: K, value: ContainerContext[K]): this
|
|
159
|
+
addContext(context: Partial<ContainerContext>): this
|
|
160
|
+
addContext(keyOrContext: keyof ContainerContext | Partial<ContainerContext>, value?: ContainerContext[keyof ContainerContext]): this {
|
|
161
|
+
if (arguments.length === 1 && typeof keyOrContext === 'object' && keyOrContext !== null) {
|
|
162
|
+
for (const [k, v] of Object.entries(keyOrContext)) {
|
|
163
|
+
if (v !== undefined) {
|
|
164
|
+
this.addContext(k as keyof ContainerContext, v as ContainerContext[keyof ContainerContext])
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return this
|
|
168
|
+
}
|
|
169
|
+
const contexts = contextMap.get(this) || new Map()
|
|
170
|
+
contexts.set(keyOrContext as keyof ContainerContext, value)
|
|
171
|
+
contextMap.set(this, contexts)
|
|
172
|
+
return this
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The Container's context is an object that contains the enabled features, the container itself, and any additional context that has been added to the container.
|
|
177
|
+
*
|
|
178
|
+
* All helper instances that are created by the container will have access to the shared context.
|
|
179
|
+
*/
|
|
180
|
+
get context(): ContainerContext<Features> & Partial<AvailableInstanceTypes<AvailableFeatures>> {
|
|
181
|
+
const contexts = contextMap.get(this) || new Map()
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
...this.enabledFeatures,
|
|
185
|
+
...Object.fromEntries(Array.from(contexts.entries())) as ContainerContext<Features>,
|
|
186
|
+
container: this as Container<Features>,
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The current state of the container.
|
|
192
|
+
*
|
|
193
|
+
* This is a snapshot of the container's state at the time this method is called.
|
|
194
|
+
*
|
|
195
|
+
* @returns The current state of the container.
|
|
196
|
+
*/
|
|
197
|
+
get currentState() {
|
|
198
|
+
return this.state.current
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Sets the state of the container.
|
|
203
|
+
*
|
|
204
|
+
* @param newState - The new state of the container.
|
|
205
|
+
* @returns The container instance.
|
|
206
|
+
*/
|
|
207
|
+
setState(newState: SetStateValue<ContainerState>) {
|
|
208
|
+
this.state.setState(newState)
|
|
209
|
+
return this
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
get Feature() {
|
|
213
|
+
return Feature
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
get Helper() {
|
|
217
|
+
return Helper
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
get State() {
|
|
221
|
+
return State
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
get features(): FeaturesRegistry {
|
|
225
|
+
return features
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Convenience method for creating a new event bus instance.
|
|
230
|
+
*/
|
|
231
|
+
bus() {
|
|
232
|
+
return new Bus()
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Convenience method for creating a new observable State object.
|
|
237
|
+
*/
|
|
238
|
+
newState<T extends object = any>(initialState: T = {} as T) {
|
|
239
|
+
return new State<T>({ initialState })
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Parse helper options through the helper's static options schema so defaults are materialized.
|
|
244
|
+
*/
|
|
245
|
+
normalizeHelperOptions(BaseClass: any, options: any, fallbackName?: string) {
|
|
246
|
+
const candidate = { ...(options || {}) }
|
|
247
|
+
|
|
248
|
+
if (fallbackName && (candidate.name === undefined || candidate.name === null || candidate.name === '')) {
|
|
249
|
+
candidate.name = fallbackName
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const schema = BaseClass?.optionsSchema
|
|
253
|
+
if (!schema || typeof schema.safeParse !== 'function') {
|
|
254
|
+
return candidate
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const parsed = schema.safeParse(candidate)
|
|
258
|
+
if (parsed.success) {
|
|
259
|
+
return parsed.data
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const target = BaseClass?.shortcut || BaseClass?.name || 'helper'
|
|
263
|
+
const details = parsed.error.issues
|
|
264
|
+
.map((issue: any) => `${issue.path?.join('.') || 'options'}: ${issue.message}`)
|
|
265
|
+
.join('; ')
|
|
266
|
+
throw new Error(`Invalid options for ${target}: ${details || parsed.error.message}`)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
buildHelperCacheKey(type: string, id: string, options: any, omitOptionKeys: string[] = []) {
|
|
270
|
+
const hashableOptions = omit(options || {}, uniq(['_cacheKey', ...omitOptionKeys]))
|
|
271
|
+
|
|
272
|
+
return hashObject({
|
|
273
|
+
__type: type,
|
|
274
|
+
id,
|
|
275
|
+
options: hashableOptions,
|
|
276
|
+
uuid: this.uuid,
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
createHelperInstance({
|
|
281
|
+
cache,
|
|
282
|
+
type,
|
|
283
|
+
id,
|
|
284
|
+
BaseClass,
|
|
285
|
+
options,
|
|
286
|
+
fallbackName,
|
|
287
|
+
omitOptionKeys = [],
|
|
288
|
+
context,
|
|
289
|
+
}: {
|
|
290
|
+
cache: Map<string, any>
|
|
291
|
+
type: string
|
|
292
|
+
id: string
|
|
293
|
+
BaseClass: any
|
|
294
|
+
options?: any
|
|
295
|
+
fallbackName?: string
|
|
296
|
+
omitOptionKeys?: string[]
|
|
297
|
+
context?: any
|
|
298
|
+
}) {
|
|
299
|
+
const normalizedOptions = this.normalizeHelperOptions(BaseClass, options, fallbackName || id)
|
|
300
|
+
const cacheKey = this.buildHelperCacheKey(type, id, normalizedOptions, omitOptionKeys)
|
|
301
|
+
const cached = cache.get(cacheKey)
|
|
302
|
+
|
|
303
|
+
if (cached) {
|
|
304
|
+
return cached
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const helperOptions = {
|
|
308
|
+
...normalizedOptions,
|
|
309
|
+
_cacheKey: normalizedOptions._cacheKey || cacheKey,
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const instance = new (BaseClass as any)(helperOptions, context || this.context)
|
|
313
|
+
cache.set(cacheKey, instance)
|
|
314
|
+
return instance
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Creates a new instance of a feature.
|
|
319
|
+
*
|
|
320
|
+
* If you pass the same arguments, it will return the same instance as last time you created that.
|
|
321
|
+
*
|
|
322
|
+
* If you need the ability to create fresh instances, it is up to you how you define your options to support that.
|
|
323
|
+
*
|
|
324
|
+
* @param id - The id of the feature to create.
|
|
325
|
+
* @param options - The options to pass to the feature constructor.
|
|
326
|
+
* @returns The new feature instance.
|
|
327
|
+
*/
|
|
328
|
+
feature<T extends keyof Features>(
|
|
329
|
+
id: T,
|
|
330
|
+
options?: ConstructorParameters<Features[T]>[0]
|
|
331
|
+
): InstanceType<Features[T]> {
|
|
332
|
+
const BaseClass = this.features.lookup(id as string) as Features[T]
|
|
333
|
+
|
|
334
|
+
return this.createHelperInstance({
|
|
335
|
+
cache: helperCache,
|
|
336
|
+
type: 'feature',
|
|
337
|
+
id: String(id),
|
|
338
|
+
BaseClass,
|
|
339
|
+
options,
|
|
340
|
+
omitOptionKeys: ['enable'],
|
|
341
|
+
context: { container: this },
|
|
342
|
+
}) as InstanceType<Features[T]>
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* TODO:
|
|
347
|
+
*
|
|
348
|
+
* A container should be able to container.use(plugin) and that plugin should be able to define
|
|
349
|
+
* an asynchronous method that will be run when the container is started. Right now there's nothing
|
|
350
|
+
* to do with starting / stopping a container but that might be neat.
|
|
351
|
+
*/
|
|
352
|
+
async start() {
|
|
353
|
+
this.emit('started', this as Container<Features>)
|
|
354
|
+
this.state.set('started', true)
|
|
355
|
+
return this
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* ENVIRONMENT DETECTION METHODS
|
|
360
|
+
*
|
|
361
|
+
* One of the ideas of the container is that it can detect what kind of environment it is running in and
|
|
362
|
+
* e.g. perhaps load different versions of features to provide the same API with different implementations.
|
|
363
|
+
*
|
|
364
|
+
*/
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Returns true if the container is running in a browser.
|
|
368
|
+
*/
|
|
369
|
+
get isBrowser() {
|
|
370
|
+
return typeof window !== 'undefined' && typeof document !== 'undefined'
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Returns true if the container is running in Bun.
|
|
375
|
+
*/
|
|
376
|
+
get isBun() {
|
|
377
|
+
return this.isNode && typeof Bun !== 'undefined'
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Returns true if the container is running in Node.
|
|
382
|
+
*/
|
|
383
|
+
get isNode() {
|
|
384
|
+
return typeof process !== 'undefined' && typeof process.versions !== 'undefined' && typeof process.versions.node !== 'undefined'
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Returns true if the container is running in Electron.
|
|
389
|
+
*/
|
|
390
|
+
get isElectron() {
|
|
391
|
+
return typeof process !== 'undefined' && typeof process.versions !== 'undefined' && typeof process.versions.electron !== 'undefined'
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Returns true if the container is running in development mode.
|
|
396
|
+
*/
|
|
397
|
+
get isDevelopment() {
|
|
398
|
+
return typeof process !== 'undefined' && process.env.NODE_ENV === 'development'
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Returns true if the container is running in production mode.
|
|
403
|
+
*/
|
|
404
|
+
get isProduction() {
|
|
405
|
+
return typeof process !== 'undefined' && process.env.NODE_ENV === 'production'
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Returns true if the container is running in a CI environment.
|
|
410
|
+
*/
|
|
411
|
+
get isCI() {
|
|
412
|
+
return typeof process !== 'undefined' && process.env.CI !== undefined && String(process.env.CI).length > 0
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Emit an event on the container's event bus. */
|
|
416
|
+
emit(event: string, ...args: any[]) {
|
|
417
|
+
this._events.emit(event, ...args)
|
|
418
|
+
return this
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** Subscribe to an event on the container's event bus. */
|
|
422
|
+
on(event: string, listener: (...args: any[]) => void) {
|
|
423
|
+
this._events.on(event, listener)
|
|
424
|
+
return this
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** Unsubscribe a listener from an event on the container's event bus. */
|
|
428
|
+
off(event: string, listener?: (...args: any[]) => void) {
|
|
429
|
+
this._events.off(event, listener)
|
|
430
|
+
return this
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** Subscribe to an event on the container's event bus, but only fire once. */
|
|
434
|
+
once(event: string, listener: (...args: any[]) => void) {
|
|
435
|
+
this._events.once(event, listener)
|
|
436
|
+
return this
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Returns a promise that will resolve when the event is emitted
|
|
441
|
+
*/
|
|
442
|
+
async waitFor(event: string) {
|
|
443
|
+
const resp = await this._events.waitFor(event)
|
|
444
|
+
return resp
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Register a helper type (registry + factory pair) on this container.
|
|
449
|
+
* Called automatically by Helper.attach() methods (e.g. Client.attach, Server.attach).
|
|
450
|
+
*
|
|
451
|
+
* @param registryName - The plural name of the registry, e.g. "clients", "servers"
|
|
452
|
+
* @param factoryName - The singular factory method name, e.g. "client", "server"
|
|
453
|
+
*/
|
|
454
|
+
registerHelperType(registryName: string, factoryName: string) {
|
|
455
|
+
const registries = uniq([...this.state.get('registries')!, registryName])
|
|
456
|
+
const factories = uniq([...this.state.get('factories')!, factoryName])
|
|
457
|
+
this.state.set('registries', registries)
|
|
458
|
+
this.state.set('factories', factories)
|
|
459
|
+
return this
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** Returns the names of all attached registries (e.g. ["features", "clients", "servers"]). */
|
|
463
|
+
get registryNames(): string[] {
|
|
464
|
+
return this.state.get('registries') || ['features']
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Returns the names of all available factory methods (e.g. ["feature", "client", "server"]). */
|
|
468
|
+
get factoryNames(): string[] {
|
|
469
|
+
return this.state.get('factories') || ['feature']
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Returns a full introspection object for this container, merging build-time AST data
|
|
474
|
+
* (JSDoc descriptions, methods, getters) with runtime data (registries, factories, state, environment).
|
|
475
|
+
*
|
|
476
|
+
* @returns {ContainerIntrospection} The complete introspection data
|
|
477
|
+
*/
|
|
478
|
+
inspect(): ContainerIntrospection {
|
|
479
|
+
const className = this.constructor.name
|
|
480
|
+
const buildTimeData = getContainerBuildTimeData(className) || {}
|
|
481
|
+
|
|
482
|
+
// Walk up the prototype chain to merge inherited build-time data
|
|
483
|
+
let mergedMethods = { ...(buildTimeData.methods || {}) }
|
|
484
|
+
let mergedGetters = { ...(buildTimeData.getters || {}) }
|
|
485
|
+
let mergedEvents = { ...(buildTimeData.events || {}) }
|
|
486
|
+
let mergedDescription = buildTimeData.description || ''
|
|
487
|
+
|
|
488
|
+
let proto = Object.getPrototypeOf(this.constructor)
|
|
489
|
+
while (proto && proto.name) {
|
|
490
|
+
const parentData = getContainerBuildTimeData(proto.name)
|
|
491
|
+
if (parentData) {
|
|
492
|
+
mergedMethods = { ...parentData.methods, ...mergedMethods }
|
|
493
|
+
mergedGetters = { ...parentData.getters, ...mergedGetters }
|
|
494
|
+
mergedEvents = { ...parentData.events, ...mergedEvents }
|
|
495
|
+
if (!mergedDescription && parentData.description) {
|
|
496
|
+
mergedDescription = parentData.description
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
proto = Object.getPrototypeOf(proto)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Build registry introspection from runtime data
|
|
503
|
+
const registryNames = this.registryNames
|
|
504
|
+
const registries: RegistryIntrospection[] = registryNames.map((name) => {
|
|
505
|
+
const registry = (this as any)[name]
|
|
506
|
+
return {
|
|
507
|
+
name,
|
|
508
|
+
baseClass: registry?.baseClass?.name || 'Helper',
|
|
509
|
+
available: registry?.available || []
|
|
510
|
+
}
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
// Get state description from the Zod schema
|
|
514
|
+
const stateSchema = (this.constructor as any).stateSchema
|
|
515
|
+
const stateDescription = stateSchema ? describeZodShape(stateSchema) : {}
|
|
516
|
+
|
|
517
|
+
return {
|
|
518
|
+
className,
|
|
519
|
+
uuid: this.uuid,
|
|
520
|
+
description: mergedDescription,
|
|
521
|
+
registries,
|
|
522
|
+
factories: this.factoryNames,
|
|
523
|
+
methods: mergedMethods,
|
|
524
|
+
getters: mergedGetters,
|
|
525
|
+
events: mergedEvents,
|
|
526
|
+
state: stateDescription,
|
|
527
|
+
enabledFeatures: this.enabledFeatureIds,
|
|
528
|
+
environment: {
|
|
529
|
+
isBrowser: this.isBrowser,
|
|
530
|
+
isNode: this.isNode,
|
|
531
|
+
isBun: this.isBun,
|
|
532
|
+
isElectron: this.isElectron,
|
|
533
|
+
isDevelopment: this.isDevelopment,
|
|
534
|
+
isProduction: this.isProduction,
|
|
535
|
+
isCI: this.isCI
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Returns a human-readable markdown representation of this container's introspection data.
|
|
542
|
+
* Useful in REPLs, AI agent contexts, or documentation generation.
|
|
543
|
+
*
|
|
544
|
+
* The first argument can be a section name (`'methods'`, `'getters'`, etc.) to render only
|
|
545
|
+
* that section, or a number for the starting heading depth (backward compatible).
|
|
546
|
+
*
|
|
547
|
+
* @returns {string} Markdown-formatted introspection text
|
|
548
|
+
*/
|
|
549
|
+
inspectAsText(sectionOrDepth?: IntrospectionSection | number, startHeadingDepth?: number): string {
|
|
550
|
+
let section: IntrospectionSection | undefined
|
|
551
|
+
let depth = 1
|
|
552
|
+
|
|
553
|
+
if (typeof sectionOrDepth === 'string') {
|
|
554
|
+
section = sectionOrDepth
|
|
555
|
+
depth = startHeadingDepth ?? 1
|
|
556
|
+
} else if (typeof sectionOrDepth === 'number') {
|
|
557
|
+
depth = sectionOrDepth
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const data = this.inspect()
|
|
561
|
+
return presentContainerIntrospectionAsMarkdown(data, depth, section)
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** Alias for inspectAsText */
|
|
565
|
+
introspectAsText(sectionOrDepth?: IntrospectionSection | number, startHeadingDepth?: number): string {
|
|
566
|
+
return this.inspectAsText(sectionOrDepth, startHeadingDepth)
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** Alias for inspectAsJSON */
|
|
570
|
+
introspectAsJSON(sectionOrDepth?: IntrospectionSection | number, startHeadingDepth?: number): any {
|
|
571
|
+
const data = this.inspect()
|
|
572
|
+
return presentContainerIntrospectionAsJSON(data, depth, section)
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** Make a property non-enumerable, which is nice for inspecting it in the REPL */
|
|
576
|
+
_hide(...propNames: string[]) {
|
|
577
|
+
propNames.map((propName) => {
|
|
578
|
+
Object.defineProperty(this, propName, { enumerable: false })
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
return this
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** Sleep for the specified number of milliseconds. Useful for scripting and sequencing. */
|
|
585
|
+
async sleep(ms = 1000) {
|
|
586
|
+
await new Promise((res) => setTimeout(res,ms))
|
|
587
|
+
return this
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
_plugins: (() => void)[] = []
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Apply a plugin or enable a feature by string name. Plugins must have a static attach(container) method.
|
|
594
|
+
*
|
|
595
|
+
* @param {Extension<T>} plugin - A feature name string, or a class/object with a static attach method
|
|
596
|
+
* @param {any} options - Options to pass to the plugin's attach method
|
|
597
|
+
*/
|
|
598
|
+
use<T = {}>(plugin: Extension<T>, options: any = {}) : this & T {
|
|
599
|
+
const container = this
|
|
600
|
+
|
|
601
|
+
if(typeof plugin === 'string' && features.has(plugin)) {
|
|
602
|
+
const featureId = plugin as keyof AvailableFeatures
|
|
603
|
+
this.feature(featureId, {
|
|
604
|
+
...options,
|
|
605
|
+
enable: true
|
|
606
|
+
})
|
|
607
|
+
} else if (typeof plugin === 'string' && !features.has(plugin)) {
|
|
608
|
+
throw new Error(`Feature ${plugin} is not available.`)
|
|
609
|
+
} else if ((typeof plugin === 'object' || typeof plugin === 'function') && typeof plugin?.attach === 'function') {
|
|
610
|
+
// This is like using a Helper or Feature subclass which declares a static attach method
|
|
611
|
+
plugin.attach(container as this & T, options)
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
return this as (this & T)
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const helperCache = new Map()
|
|
619
|
+
const featureIdToHelperCacheKeyMap= new Map()
|
|
620
|
+
const contextMap = new WeakMap()
|
|
621
|
+
|
|
622
|
+
function presentContainerIntrospectionAsMarkdown(data: ContainerIntrospection, startHeadingDepth: number = 1, section?: IntrospectionSection): string {
|
|
623
|
+
const sections: string[] = []
|
|
624
|
+
const heading = (level: number) => '#'.repeat(Math.max(1, startHeadingDepth + level - 1))
|
|
625
|
+
|
|
626
|
+
const shouldRender = (name: IntrospectionSection | string) => !section || section === name
|
|
627
|
+
|
|
628
|
+
if (!section) {
|
|
629
|
+
// Header
|
|
630
|
+
sections.push(`${heading(1)} ${data.className}\n\n${data.description || ''}`)
|
|
631
|
+
|
|
632
|
+
// Registries section
|
|
633
|
+
if (data.registries && data.registries.length > 0) {
|
|
634
|
+
sections.push(`${heading(2)} Registries`)
|
|
635
|
+
|
|
636
|
+
for (const reg of data.registries) {
|
|
637
|
+
sections.push(`${heading(3)} ${reg.name} (${reg.baseClass})`)
|
|
638
|
+
if (reg.available.length > 0) {
|
|
639
|
+
sections.push(reg.available.map(a => `- \`${a}\``).join('\n'))
|
|
640
|
+
} else {
|
|
641
|
+
sections.push('_No members registered_')
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Factories section
|
|
647
|
+
if (data.factories && data.factories.length > 0) {
|
|
648
|
+
sections.push(`${heading(2)} Factory Methods`)
|
|
649
|
+
sections.push(data.factories.map(f => `- \`${f}()\``).join('\n'))
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Methods section
|
|
654
|
+
if (shouldRender('methods') && data.methods && Object.keys(data.methods).length > 0) {
|
|
655
|
+
sections.push(`${heading(2)} Methods`)
|
|
656
|
+
|
|
657
|
+
for (const [methodName, methodInfo] of Object.entries(data.methods)) {
|
|
658
|
+
sections.push(`${heading(3)} ${methodName}`)
|
|
659
|
+
|
|
660
|
+
if (methodInfo.description) {
|
|
661
|
+
sections.push(methodInfo.description)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (methodInfo.parameters && Object.keys(methodInfo.parameters).length > 0) {
|
|
665
|
+
const tableRows = [
|
|
666
|
+
`**Parameters:**`,
|
|
667
|
+
'',
|
|
668
|
+
`| Name | Type | Required | Description |`,
|
|
669
|
+
`|------|------|----------|-------------|`,
|
|
670
|
+
]
|
|
671
|
+
|
|
672
|
+
for (const [paramName, paramInfo] of Object.entries(methodInfo.parameters)) {
|
|
673
|
+
const isRequired = methodInfo.required?.includes(paramName) ? '✓' : ''
|
|
674
|
+
tableRows.push(`| \`${paramName}\` | \`${paramInfo.type || 'any'}\` | ${isRequired} | ${paramInfo.description || ''} |`)
|
|
675
|
+
|
|
676
|
+
if (paramInfo.properties && Object.keys(paramInfo.properties).length > 0) {
|
|
677
|
+
tableRows.push('')
|
|
678
|
+
tableRows.push(`\`${paramInfo.type}\` properties:`)
|
|
679
|
+
tableRows.push('')
|
|
680
|
+
tableRows.push(`| Property | Type | Description |`)
|
|
681
|
+
tableRows.push(`|----------|------|-------------|`)
|
|
682
|
+
|
|
683
|
+
for (const [propName, propInfo] of Object.entries(paramInfo.properties)) {
|
|
684
|
+
tableRows.push(`| \`${propName}\` | \`${propInfo.type || 'any'}\` | ${propInfo.description || ''} |`)
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
sections.push(tableRows.join('\n'))
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (methodInfo.returns) {
|
|
692
|
+
sections.push(`**Returns:** \`${methodInfo.returns}\``)
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
sections.push('')
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Getters section
|
|
700
|
+
if (shouldRender('getters') && data.getters && Object.keys(data.getters).length > 0) {
|
|
701
|
+
const getterTableRows = [
|
|
702
|
+
`${heading(2)} Getters`,
|
|
703
|
+
'',
|
|
704
|
+
`| Property | Type | Description |`,
|
|
705
|
+
`|----------|------|-------------|`,
|
|
706
|
+
]
|
|
707
|
+
|
|
708
|
+
for (const [getterName, getterInfo] of Object.entries(data.getters)) {
|
|
709
|
+
getterTableRows.push(`| \`${getterName}\` | \`${getterInfo.returns || 'any'}\` | ${getterInfo.description || ''} |`)
|
|
710
|
+
}
|
|
711
|
+
sections.push(getterTableRows.join('\n'))
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Events section
|
|
715
|
+
if (shouldRender('events') && data.events && Object.keys(data.events).length > 0) {
|
|
716
|
+
sections.push(`${heading(2)} Events`)
|
|
717
|
+
|
|
718
|
+
for (const [eventName, eventInfo] of Object.entries(data.events)) {
|
|
719
|
+
sections.push(`${heading(3)} ${eventName}`)
|
|
720
|
+
|
|
721
|
+
if (eventInfo.description) {
|
|
722
|
+
sections.push(eventInfo.description)
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (eventInfo.arguments && Object.keys(eventInfo.arguments).length > 0) {
|
|
726
|
+
const tableRows = [
|
|
727
|
+
`**Event Arguments:**`,
|
|
728
|
+
'',
|
|
729
|
+
`| Name | Type | Description |`,
|
|
730
|
+
`|------|------|-------------|`,
|
|
731
|
+
]
|
|
732
|
+
|
|
733
|
+
for (const [argName, argInfo] of Object.entries(eventInfo.arguments)) {
|
|
734
|
+
tableRows.push(`| \`${argName}\` | \`${argInfo.type || 'any'}\` | ${argInfo.description || ''} |`)
|
|
735
|
+
}
|
|
736
|
+
sections.push(tableRows.join('\n'))
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
sections.push('')
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// State section
|
|
744
|
+
if (shouldRender('state') && data.state && Object.keys(data.state).length > 0) {
|
|
745
|
+
const stateTableRows = [
|
|
746
|
+
`${heading(2)} State`,
|
|
747
|
+
'',
|
|
748
|
+
`| Property | Type | Description |`,
|
|
749
|
+
`|----------|------|-------------|`,
|
|
750
|
+
]
|
|
751
|
+
|
|
752
|
+
for (const [stateName, stateInfo] of Object.entries(data.state)) {
|
|
753
|
+
stateTableRows.push(`| \`${stateName}\` | \`${stateInfo.type || 'any'}\` | ${stateInfo.description || ''} |`)
|
|
754
|
+
}
|
|
755
|
+
sections.push(stateTableRows.join('\n'))
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (!section) {
|
|
759
|
+
// Enabled features section
|
|
760
|
+
if (data.enabledFeatures && data.enabledFeatures.length > 0) {
|
|
761
|
+
sections.push(`${heading(2)} Enabled Features`)
|
|
762
|
+
sections.push(data.enabledFeatures.map(f => `- \`${f}\``).join('\n'))
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Environment section
|
|
766
|
+
if (data.environment) {
|
|
767
|
+
const envTableRows = [
|
|
768
|
+
`${heading(2)} Environment`,
|
|
769
|
+
'',
|
|
770
|
+
`| Flag | Value |`,
|
|
771
|
+
`|------|-------|`,
|
|
772
|
+
]
|
|
773
|
+
for (const [key, value] of Object.entries(data.environment)) {
|
|
774
|
+
envTableRows.push(`| \`${key}\` | ${value} |`)
|
|
775
|
+
}
|
|
776
|
+
sections.push(envTableRows.join('\n'))
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
return sections.join('\n\n')
|
|
781
|
+
}
|