@soederpop/luca 0.0.29 → 0.0.31
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/commands/try-all-challenges.ts +1 -1
- package/docs/TABLE-OF-CONTENTS.md +0 -3
- package/docs/tutorials/20-browser-esm.md +234 -0
- package/package.json +1 -1
- package/src/agi/container.server.ts +4 -0
- package/src/agi/features/assistant.ts +120 -3
- package/src/agi/features/browser-use.ts +623 -0
- package/src/bootstrap/generated.ts +236 -308
- package/src/cli/build-info.ts +2 -2
- package/src/clients/rest.ts +7 -7
- package/src/command.ts +20 -1
- package/src/commands/chat.ts +22 -0
- package/src/commands/describe.ts +67 -2
- package/src/commands/prompt.ts +23 -3
- package/src/commands/serve.ts +27 -0
- package/src/container.ts +411 -113
- package/src/endpoint.ts +6 -0
- package/src/helper.ts +226 -5
- package/src/introspection/generated.agi.ts +16089 -10021
- package/src/introspection/generated.node.ts +5102 -2077
- package/src/introspection/generated.web.ts +379 -291
- package/src/introspection/index.ts +7 -0
- package/src/introspection/scan.ts +224 -7
- package/src/node/container.ts +31 -10
- package/src/node/features/content-db.ts +7 -7
- package/src/node/features/disk-cache.ts +11 -11
- package/src/node/features/esbuild.ts +3 -3
- package/src/node/features/file-manager.ts +15 -15
- package/src/node/features/fs.ts +23 -22
- package/src/node/features/git.ts +10 -10
- package/src/node/features/helpers.ts +5 -2
- package/src/node/features/ink.ts +13 -13
- package/src/node/features/ipc-socket.ts +8 -8
- package/src/node/features/networking.ts +3 -3
- package/src/node/features/os.ts +7 -7
- package/src/node/features/package-finder.ts +15 -15
- package/src/node/features/proc.ts +1 -1
- package/src/node/features/ui.ts +13 -13
- package/src/node/features/vm.ts +4 -4
- package/src/scaffolds/generated.ts +1 -1
- package/src/servers/express.ts +24 -6
- package/src/servers/mcp.ts +4 -4
- package/src/servers/socket.ts +6 -6
- package/docs/apis/features/node/window-manager.md +0 -445
- package/docs/examples/window-manager-layouts.md +0 -180
- package/docs/examples/window-manager.md +0 -125
- package/docs/window-manager-fix.md +0 -249
- package/scripts/test-window-manager-lifecycle.ts +0 -86
- package/scripts/test-window-manager.ts +0 -43
- package/src/node/features/window-manager.ts +0 -1603
package/src/container.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { Bus } from './bus'
|
|
3
3
|
import { SetStateValue, State } from './state'
|
|
4
4
|
import { AvailableFeatures, features, Feature, FeaturesRegistry } from './feature'
|
|
5
|
-
import { Helper } from './helper'
|
|
5
|
+
import { Helper, normalizeTypeString, renderTypeScriptParams, isGenericObjectType } from './helper'
|
|
6
6
|
import uuid from 'node-uuid'
|
|
7
7
|
import hashObject from './hash-object'
|
|
8
8
|
import { uniq, keyBy, uniqBy, groupBy, debounce, throttle, mapValues, mapKeys, pick, get, set, omit, kebabCase, camelCase, upperFirst, lowerFirst } from 'lodash-es'
|
|
@@ -40,22 +40,53 @@ export interface Plugin<T> {
|
|
|
40
40
|
|
|
41
41
|
export type Extension<T> = 'string' | keyof AvailableFeatures | Plugin<T> | { attach: (container: Container<any>, options?: any) => T}
|
|
42
42
|
|
|
43
|
+
export interface ContainerUtils {
|
|
44
|
+
/** Generate a v4 UUID */
|
|
45
|
+
uuid: () => string
|
|
46
|
+
/** Deterministic hash of any object */
|
|
47
|
+
hashObject: (obj: any) => string
|
|
48
|
+
/** String case conversion and inflection utilities */
|
|
49
|
+
stringUtils: { kebabCase: typeof kebabCase; camelCase: typeof camelCase; upperFirst: typeof upperFirst; lowerFirst: typeof lowerFirst; pluralize: typeof pluralize; singularize: typeof singularize }
|
|
50
|
+
/** Lodash utility subset */
|
|
51
|
+
lodash: { uniq: typeof uniq; keyBy: typeof keyBy; uniqBy: typeof uniqBy; groupBy: typeof groupBy; debounce: typeof debounce; throttle: typeof throttle; mapValues: typeof mapValues; mapKeys: typeof mapKeys; pick: typeof pick; get: typeof get; set: typeof set; omit: typeof omit }
|
|
52
|
+
}
|
|
53
|
+
|
|
43
54
|
export interface ContainerContext<T extends AvailableFeatures = any> {
|
|
44
|
-
container: Container<T>
|
|
55
|
+
container: Container<T>
|
|
45
56
|
}
|
|
46
57
|
|
|
47
|
-
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
58
|
+
/**
|
|
59
|
+
* The Container is the core runtime object in Luca. It is a singleton per process that acts as an
|
|
60
|
+
* event bus, state machine, and dependency injector. It holds registries of helpers (features, clients,
|
|
61
|
+
* servers, commands, endpoints) and provides factory methods to create instances from them.
|
|
62
|
+
*
|
|
63
|
+
* All helper instances share the container's context, enabling them to communicate and coordinate.
|
|
64
|
+
* The container detects its runtime environment (Node, Bun, browser, Electron) and can load
|
|
65
|
+
* platform-specific feature implementations accordingly.
|
|
66
|
+
*
|
|
67
|
+
* Use `container.feature('name')` to create feature instances, `container.use(Plugin)` to extend
|
|
68
|
+
* the container with new capabilities, and `container.on('event', handler)` to react to lifecycle events.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```ts
|
|
72
|
+
* // Create a feature instance (cached — same args return same instance)
|
|
73
|
+
* const fs = container.feature('fs')
|
|
74
|
+
* const content = fs.readFile('README.md')
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```ts
|
|
79
|
+
* // Listen for state changes
|
|
80
|
+
* container.on('stateChange', (state) => console.log('State changed:', state))
|
|
81
|
+
* container.setState({ started: true })
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* // Extend with a plugin
|
|
87
|
+
* container.use(MyClient) // calls MyClient.attach(container)
|
|
88
|
+
* container.use('contentDb') // enable a feature by name
|
|
89
|
+
* ```
|
|
59
90
|
*/
|
|
60
91
|
export class Container<Features extends AvailableFeatures = AvailableFeatures, ContainerState extends ContainerState = ContainerState > {
|
|
61
92
|
static stateSchema = ContainerStateSchema
|
|
@@ -100,13 +131,18 @@ export class Container<Features extends AvailableFeatures = AvailableFeatures, C
|
|
|
100
131
|
|
|
101
132
|
/**
|
|
102
133
|
* Creates a new subcontainer instance of the same concrete Container subclass.
|
|
103
|
-
*
|
|
104
134
|
* The new instance is constructed with the same options as this container,
|
|
105
135
|
* shallow-merged with any overrides you provide. This preserves the runtime
|
|
106
|
-
* container type (e.g. NodeContainer,
|
|
136
|
+
* container type (e.g. NodeContainer, AGIContainer, etc.).
|
|
137
|
+
*
|
|
138
|
+
* @param options - Options to override for the new container instance
|
|
139
|
+
* @returns A new container instance of the same subclass
|
|
107
140
|
*
|
|
108
|
-
* @
|
|
109
|
-
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```ts
|
|
143
|
+
* const child = container.subcontainer({ cwd: '/tmp/workspace' })
|
|
144
|
+
* child.cwd // '/tmp/workspace'
|
|
145
|
+
* ```
|
|
110
146
|
*/
|
|
111
147
|
subcontainer<This extends Container<any, any>>(
|
|
112
148
|
this: This,
|
|
@@ -124,12 +160,12 @@ export class Container<Features extends AvailableFeatures = AvailableFeatures, C
|
|
|
124
160
|
|
|
125
161
|
|
|
126
162
|
/** The observable state object for this container instance. */
|
|
127
|
-
get state() {
|
|
163
|
+
get state(): State<ContainerState> {
|
|
128
164
|
return this._state
|
|
129
165
|
}
|
|
130
166
|
|
|
131
167
|
/** Returns the list of shortcut IDs for all currently enabled features. */
|
|
132
|
-
get enabledFeatureIds() {
|
|
168
|
+
get enabledFeatureIds(): string[] {
|
|
133
169
|
return this.state.get('enabledFeatures') || []
|
|
134
170
|
}
|
|
135
171
|
|
|
@@ -140,18 +176,40 @@ export class Container<Features extends AvailableFeatures = AvailableFeatures, C
|
|
|
140
176
|
) as AvailableInstanceTypes<Features>
|
|
141
177
|
}
|
|
142
178
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
179
|
+
/**
|
|
180
|
+
* Common utilities available on every container. Provides UUID generation, object hashing,
|
|
181
|
+
* string case conversion, and lodash helpers — no imports needed.
|
|
182
|
+
*
|
|
183
|
+
* - `utils.uuid()` — generate a v4 UUID
|
|
184
|
+
* - `utils.hashObject(obj)` — deterministic hash of any object
|
|
185
|
+
* - `utils.stringUtils` — `{ kebabCase, camelCase, upperFirst, lowerFirst, pluralize, singularize }`
|
|
186
|
+
* - `utils.lodash` — `{ uniq, keyBy, uniqBy, groupBy, debounce, throttle, mapValues, mapKeys, pick, get, set, omit }`
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```ts
|
|
190
|
+
* const id = container.utils.uuid()
|
|
191
|
+
* const hash = container.utils.hashObject({ foo: 'bar' })
|
|
192
|
+
* const name = container.utils.stringUtils.camelCase('my-feature')
|
|
193
|
+
* const unique = container.utils.lodash.uniq([1, 2, 2, 3])
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
get utils(): ContainerUtils {
|
|
197
|
+
return {
|
|
198
|
+
hashObject: (obj: any) => hashObject(obj),
|
|
199
|
+
get stringUtils() { return stringUtils },
|
|
200
|
+
uuid: () => v4(),
|
|
201
|
+
lodash: {
|
|
202
|
+
uniq, keyBy, uniqBy, groupBy, debounce, throttle, mapValues, mapKeys, pick, get, set, omit,
|
|
203
|
+
}
|
|
149
204
|
}
|
|
150
205
|
}
|
|
151
206
|
|
|
152
207
|
private _describer?: ContainerDescriber
|
|
153
208
|
|
|
154
|
-
/**
|
|
209
|
+
/**
|
|
210
|
+
* Lazy-initialized ContainerDescriber for introspecting registries, helpers, and members.
|
|
211
|
+
* @internal
|
|
212
|
+
*/
|
|
155
213
|
get describer(): ContainerDescriber {
|
|
156
214
|
if (!this._describer) {
|
|
157
215
|
this._describer = new ContainerDescriber(this)
|
|
@@ -159,15 +217,22 @@ export class Container<Features extends AvailableFeatures = AvailableFeatures, C
|
|
|
159
217
|
return this._describer
|
|
160
218
|
}
|
|
161
219
|
|
|
220
|
+
addContext<K extends keyof ContainerContext>(key: K, value: ContainerContext[K]): this
|
|
221
|
+
addContext(context: Partial<ContainerContext>): this
|
|
162
222
|
/**
|
|
163
223
|
* Add a value to the container's shared context, which is passed to all helper instances.
|
|
164
|
-
* Accepts either a key and value, or an object of key-value pairs to
|
|
224
|
+
* Accepts either a key and value, or an object of key-value pairs to merge in.
|
|
165
225
|
*
|
|
166
|
-
* @param
|
|
167
|
-
* @param
|
|
226
|
+
* @param key - The context key, or an object of key-value pairs to merge
|
|
227
|
+
* @param value - The context value (omit when passing an object)
|
|
228
|
+
* @returns The container instance (for chaining)
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* ```ts
|
|
232
|
+
* container.addContext('db', dbConnection)
|
|
233
|
+
* container.addContext({ db: dbConnection, cache: redisClient })
|
|
234
|
+
* ```
|
|
168
235
|
*/
|
|
169
|
-
addContext<K extends keyof ContainerContext>(key: K, value: ContainerContext[K]): this
|
|
170
|
-
addContext(context: Partial<ContainerContext>): this
|
|
171
236
|
addContext(keyOrContext: keyof ContainerContext | Partial<ContainerContext>, value?: ContainerContext[keyof ContainerContext]): this {
|
|
172
237
|
if (arguments.length === 1 && typeof keyOrContext === 'object' && keyOrContext !== null) {
|
|
173
238
|
for (const [k, v] of Object.entries(keyOrContext)) {
|
|
@@ -205,17 +270,24 @@ export class Container<Features extends AvailableFeatures = AvailableFeatures, C
|
|
|
205
270
|
*
|
|
206
271
|
* @returns The current state of the container.
|
|
207
272
|
*/
|
|
208
|
-
get currentState() {
|
|
273
|
+
get currentState(): ContainerState {
|
|
209
274
|
return this.state.current
|
|
210
275
|
}
|
|
211
276
|
|
|
212
|
-
/**
|
|
213
|
-
* Sets the state of the container.
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
* @
|
|
277
|
+
/**
|
|
278
|
+
* Sets the state of the container. Accepts a partial state object to merge, or a function
|
|
279
|
+
* that receives the current state and returns the new state.
|
|
280
|
+
*
|
|
281
|
+
* @param newState - A partial state object to merge, or a function `(current) => newState`
|
|
282
|
+
* @returns The container instance (for chaining)
|
|
283
|
+
*
|
|
284
|
+
* @example
|
|
285
|
+
* ```ts
|
|
286
|
+
* container.setState({ started: true })
|
|
287
|
+
* container.setState((prev) => ({ ...prev, started: true }))
|
|
288
|
+
* ```
|
|
217
289
|
*/
|
|
218
|
-
setState(newState: SetStateValue<ContainerState>) {
|
|
290
|
+
setState(newState: SetStateValue<ContainerState>): this {
|
|
219
291
|
this.state.setState(newState)
|
|
220
292
|
return this
|
|
221
293
|
}
|
|
@@ -232,26 +304,59 @@ export class Container<Features extends AvailableFeatures = AvailableFeatures, C
|
|
|
232
304
|
return State
|
|
233
305
|
}
|
|
234
306
|
|
|
307
|
+
/**
|
|
308
|
+
* The features registry. Use it to check what features are available, look up feature classes,
|
|
309
|
+
* or check if a feature is registered.
|
|
310
|
+
*
|
|
311
|
+
* @example
|
|
312
|
+
* ```ts
|
|
313
|
+
* container.features.available // ['fs', 'git', 'grep', ...]
|
|
314
|
+
* container.features.has('fs') // true
|
|
315
|
+
* container.features.lookup('fs') // FS class
|
|
316
|
+
* ```
|
|
317
|
+
*/
|
|
235
318
|
get features(): FeaturesRegistry {
|
|
236
319
|
return features
|
|
237
320
|
}
|
|
238
321
|
|
|
239
|
-
/**
|
|
240
|
-
*
|
|
322
|
+
/**
|
|
323
|
+
* Create a new standalone event bus instance. Useful when you need a scoped event channel
|
|
324
|
+
* that is independent of the container's own event bus.
|
|
325
|
+
*
|
|
326
|
+
* @returns A new Bus instance
|
|
327
|
+
*
|
|
328
|
+
* @example
|
|
329
|
+
* ```ts
|
|
330
|
+
* const myBus = container.bus()
|
|
331
|
+
* myBus.on('data', (payload) => console.log(payload))
|
|
332
|
+
* myBus.emit('data', { count: 42 })
|
|
333
|
+
* ```
|
|
241
334
|
*/
|
|
242
|
-
bus() {
|
|
335
|
+
bus(): Bus {
|
|
243
336
|
return new Bus()
|
|
244
337
|
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
*
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Create a new standalone observable State object. Useful when you need reactive state
|
|
341
|
+
* that is independent of the container's own state.
|
|
342
|
+
*
|
|
343
|
+
* @param initialState - The initial state object (defaults to empty)
|
|
344
|
+
* @returns A new State instance
|
|
345
|
+
*
|
|
346
|
+
* @example
|
|
347
|
+
* ```ts
|
|
348
|
+
* const myState = container.newState({ count: 0, loading: false })
|
|
349
|
+
* myState.observe(() => console.log('Changed:', myState.current))
|
|
350
|
+
* myState.set('count', 1)
|
|
351
|
+
* ```
|
|
248
352
|
*/
|
|
249
|
-
newState<T extends object = any>(initialState: T = {} as T) {
|
|
353
|
+
newState<T extends object = any>(initialState: T = {} as T): State<T> {
|
|
250
354
|
return new State<T>({ initialState })
|
|
251
355
|
}
|
|
252
356
|
|
|
253
357
|
/**
|
|
254
358
|
* Parse helper options through the helper's static options schema so defaults are materialized.
|
|
359
|
+
* @internal
|
|
255
360
|
*/
|
|
256
361
|
normalizeHelperOptions(BaseClass: any, options: any, fallbackName?: string) {
|
|
257
362
|
const candidate = { ...(options || {}) }
|
|
@@ -277,6 +382,7 @@ export class Container<Features extends AvailableFeatures = AvailableFeatures, C
|
|
|
277
382
|
throw new Error(`Invalid options for ${target}: ${details || parsed.error.message}`)
|
|
278
383
|
}
|
|
279
384
|
|
|
385
|
+
/** @internal */
|
|
280
386
|
buildHelperCacheKey(type: string, id: string, options: any, omitOptionKeys: string[] = []) {
|
|
281
387
|
const hashableOptions = omit(options || {}, uniq(['_cacheKey', ...omitOptionKeys]))
|
|
282
388
|
|
|
@@ -288,6 +394,7 @@ export class Container<Features extends AvailableFeatures = AvailableFeatures, C
|
|
|
288
394
|
})
|
|
289
395
|
}
|
|
290
396
|
|
|
397
|
+
/** @internal */
|
|
291
398
|
createHelperInstance({
|
|
292
399
|
cache,
|
|
293
400
|
type,
|
|
@@ -353,17 +460,22 @@ export class Container<Features extends AvailableFeatures = AvailableFeatures, C
|
|
|
353
460
|
}) as InstanceType<Features[T]>
|
|
354
461
|
}
|
|
355
462
|
|
|
356
|
-
/**
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
463
|
+
/**
|
|
464
|
+
* Start the container. Emits the 'started' event and sets `state.started` to true.
|
|
465
|
+
* Plugins and features can listen for this event to perform initialization.
|
|
466
|
+
*
|
|
467
|
+
* @returns The container instance
|
|
468
|
+
*
|
|
469
|
+
* @example
|
|
470
|
+
* ```ts
|
|
471
|
+
* container.on('started', () => console.log('Ready'))
|
|
472
|
+
* await container.start()
|
|
473
|
+
* ```
|
|
362
474
|
*/
|
|
363
|
-
async start() {
|
|
475
|
+
async start(): Promise<this> {
|
|
364
476
|
this.emit('started', this as Container<Features>)
|
|
365
477
|
this.state.set('started', true)
|
|
366
|
-
return this
|
|
478
|
+
return this
|
|
367
479
|
}
|
|
368
480
|
|
|
369
481
|
/**
|
|
@@ -377,80 +489,126 @@ export class Container<Features extends AvailableFeatures = AvailableFeatures, C
|
|
|
377
489
|
/**
|
|
378
490
|
* Returns true if the container is running in a browser.
|
|
379
491
|
*/
|
|
380
|
-
get isBrowser() {
|
|
492
|
+
get isBrowser(): boolean {
|
|
381
493
|
return typeof window !== 'undefined' && typeof document !== 'undefined'
|
|
382
494
|
}
|
|
383
|
-
|
|
384
|
-
/**
|
|
495
|
+
|
|
496
|
+
/**
|
|
385
497
|
* Returns true if the container is running in Bun.
|
|
386
498
|
*/
|
|
387
|
-
get isBun() {
|
|
499
|
+
get isBun(): boolean {
|
|
388
500
|
return this.isNode && typeof Bun !== 'undefined'
|
|
389
501
|
}
|
|
390
502
|
|
|
391
|
-
/**
|
|
503
|
+
/**
|
|
392
504
|
* Returns true if the container is running in Node.
|
|
393
505
|
*/
|
|
394
|
-
get isNode() {
|
|
506
|
+
get isNode(): boolean {
|
|
395
507
|
return typeof process !== 'undefined' && typeof process.versions !== 'undefined' && typeof process.versions.node !== 'undefined'
|
|
396
508
|
}
|
|
397
|
-
|
|
398
|
-
/**
|
|
509
|
+
|
|
510
|
+
/**
|
|
399
511
|
* Returns true if the container is running in Electron.
|
|
400
512
|
*/
|
|
401
|
-
get isElectron() {
|
|
513
|
+
get isElectron(): boolean {
|
|
402
514
|
return typeof process !== 'undefined' && typeof process.versions !== 'undefined' && typeof process.versions.electron !== 'undefined'
|
|
403
515
|
}
|
|
404
|
-
|
|
405
|
-
/**
|
|
516
|
+
|
|
517
|
+
/**
|
|
406
518
|
* Returns true if the container is running in development mode.
|
|
407
519
|
*/
|
|
408
|
-
get isDevelopment() {
|
|
520
|
+
get isDevelopment(): boolean {
|
|
409
521
|
return typeof process !== 'undefined' && process.env.NODE_ENV === 'development'
|
|
410
522
|
}
|
|
411
|
-
|
|
412
|
-
/**
|
|
523
|
+
|
|
524
|
+
/**
|
|
413
525
|
* Returns true if the container is running in production mode.
|
|
414
526
|
*/
|
|
415
|
-
get isProduction() {
|
|
527
|
+
get isProduction(): boolean {
|
|
416
528
|
return typeof process !== 'undefined' && process.env.NODE_ENV === 'production'
|
|
417
529
|
}
|
|
418
|
-
|
|
419
|
-
/**
|
|
530
|
+
|
|
531
|
+
/**
|
|
420
532
|
* Returns true if the container is running in a CI environment.
|
|
421
533
|
*/
|
|
422
|
-
get isCI() {
|
|
534
|
+
get isCI(): boolean {
|
|
423
535
|
return typeof process !== 'undefined' && process.env.CI !== undefined && String(process.env.CI).length > 0
|
|
424
536
|
}
|
|
425
537
|
|
|
426
|
-
/**
|
|
427
|
-
|
|
538
|
+
/**
|
|
539
|
+
* Emit an event on the container's event bus.
|
|
540
|
+
*
|
|
541
|
+
* @param event - The event name
|
|
542
|
+
* @param args - Arguments to pass to listeners
|
|
543
|
+
* @returns The container instance (for chaining)
|
|
544
|
+
*
|
|
545
|
+
* @example
|
|
546
|
+
* ```ts
|
|
547
|
+
* container.emit('taskCompleted', { id: 'abc', result: 42 })
|
|
548
|
+
* ```
|
|
549
|
+
*/
|
|
550
|
+
emit(event: string, ...args: any[]): this {
|
|
428
551
|
this._events.emit(event, ...args)
|
|
429
552
|
return this
|
|
430
553
|
}
|
|
431
554
|
|
|
432
|
-
/**
|
|
433
|
-
|
|
555
|
+
/**
|
|
556
|
+
* Subscribe to an event on the container's event bus.
|
|
557
|
+
*
|
|
558
|
+
* @param event - The event name
|
|
559
|
+
* @param listener - The callback function
|
|
560
|
+
* @returns The container instance (for chaining)
|
|
561
|
+
*
|
|
562
|
+
* @example
|
|
563
|
+
* ```ts
|
|
564
|
+
* container.on('featureEnabled', (id, feature) => {
|
|
565
|
+
* console.log(`Feature ${id} enabled`)
|
|
566
|
+
* })
|
|
567
|
+
* ```
|
|
568
|
+
*/
|
|
569
|
+
on(event: string, listener: (...args: any[]) => void): this {
|
|
434
570
|
this._events.on(event, listener)
|
|
435
571
|
return this
|
|
436
572
|
}
|
|
437
573
|
|
|
438
|
-
/**
|
|
439
|
-
|
|
574
|
+
/**
|
|
575
|
+
* Unsubscribe a listener from an event on the container's event bus.
|
|
576
|
+
*
|
|
577
|
+
* @param event - The event name
|
|
578
|
+
* @param listener - The listener to remove
|
|
579
|
+
* @returns The container instance (for chaining)
|
|
580
|
+
*/
|
|
581
|
+
off(event: string, listener?: (...args: any[]) => void): this {
|
|
440
582
|
this._events.off(event, listener)
|
|
441
583
|
return this
|
|
442
584
|
}
|
|
443
585
|
|
|
444
|
-
/**
|
|
445
|
-
|
|
586
|
+
/**
|
|
587
|
+
* Subscribe to an event on the container's event bus, but only fire once.
|
|
588
|
+
*
|
|
589
|
+
* @param event - The event name
|
|
590
|
+
* @param listener - The callback function (invoked at most once)
|
|
591
|
+
* @returns The container instance (for chaining)
|
|
592
|
+
*/
|
|
593
|
+
once(event: string, listener: (...args: any[]) => void): this {
|
|
446
594
|
this._events.once(event, listener)
|
|
447
595
|
return this
|
|
448
596
|
}
|
|
449
597
|
|
|
450
598
|
/**
|
|
451
|
-
* Returns a promise that
|
|
599
|
+
* Returns a promise that resolves the next time the given event is emitted.
|
|
600
|
+
* Useful for awaiting one-time lifecycle transitions.
|
|
601
|
+
*
|
|
602
|
+
* @param event - The event name to wait for
|
|
603
|
+
* @returns A promise that resolves with the event arguments
|
|
604
|
+
*
|
|
605
|
+
* @example
|
|
606
|
+
* ```ts
|
|
607
|
+
* await container.waitFor('started')
|
|
608
|
+
* console.log('Container is ready')
|
|
609
|
+
* ```
|
|
452
610
|
*/
|
|
453
|
-
async waitFor(event: string) {
|
|
611
|
+
async waitFor(event: string): Promise<any> {
|
|
454
612
|
const resp = await this._events.waitFor(event)
|
|
455
613
|
return resp
|
|
456
614
|
}
|
|
@@ -458,9 +616,7 @@ export class Container<Features extends AvailableFeatures = AvailableFeatures, C
|
|
|
458
616
|
/**
|
|
459
617
|
* Register a helper type (registry + factory pair) on this container.
|
|
460
618
|
* Called automatically by Helper.attach() methods (e.g. Client.attach, Server.attach).
|
|
461
|
-
*
|
|
462
|
-
* @param registryName - The plural name of the registry, e.g. "clients", "servers"
|
|
463
|
-
* @param factoryName - The singular factory method name, e.g. "client", "server"
|
|
619
|
+
* @internal
|
|
464
620
|
*/
|
|
465
621
|
registerHelperType(registryName: string, factoryName: string) {
|
|
466
622
|
const registries = uniq([...this.state.get('registries')!, registryName])
|
|
@@ -484,7 +640,15 @@ export class Container<Features extends AvailableFeatures = AvailableFeatures, C
|
|
|
484
640
|
* Returns a full introspection object for this container, merging build-time AST data
|
|
485
641
|
* (JSDoc descriptions, methods, getters) with runtime data (registries, factories, state, environment).
|
|
486
642
|
*
|
|
487
|
-
* @returns
|
|
643
|
+
* @returns The complete introspection data as a structured object
|
|
644
|
+
*
|
|
645
|
+
* @example
|
|
646
|
+
* ```ts
|
|
647
|
+
* const info = container.inspect()
|
|
648
|
+
* console.log(info.methods) // all public methods with descriptions
|
|
649
|
+
* console.log(info.getters) // all getters with return types
|
|
650
|
+
* console.log(info.registries) // features, clients, servers, etc.
|
|
651
|
+
* ```
|
|
488
652
|
*/
|
|
489
653
|
inspect(): ContainerIntrospection {
|
|
490
654
|
const className = this.constructor.name
|
|
@@ -550,12 +714,18 @@ export class Container<Features extends AvailableFeatures = AvailableFeatures, C
|
|
|
550
714
|
|
|
551
715
|
/**
|
|
552
716
|
* Returns a human-readable markdown representation of this container's introspection data.
|
|
553
|
-
* Useful in REPLs, AI agent contexts, or documentation generation.
|
|
717
|
+
* Useful in REPLs, AI agent contexts, or documentation generation. Pass a section name
|
|
718
|
+
* to render only that section (e.g. 'methods', 'getters', 'events', 'state').
|
|
554
719
|
*
|
|
555
|
-
*
|
|
556
|
-
*
|
|
720
|
+
* @param sectionOrDepth - A section name to render, or heading depth number
|
|
721
|
+
* @param startHeadingDepth - Starting markdown heading depth (default 1)
|
|
722
|
+
* @returns Markdown-formatted introspection text
|
|
557
723
|
*
|
|
558
|
-
* @
|
|
724
|
+
* @example
|
|
725
|
+
* ```ts
|
|
726
|
+
* console.log(container.inspectAsText()) // full description
|
|
727
|
+
* console.log(container.inspectAsText('methods')) // just methods
|
|
728
|
+
* ```
|
|
559
729
|
*/
|
|
560
730
|
inspectAsText(sectionOrDepth?: IntrospectionSection | number, startHeadingDepth?: number): string {
|
|
561
731
|
let section: IntrospectionSection | undefined
|
|
@@ -572,15 +742,37 @@ export class Container<Features extends AvailableFeatures = AvailableFeatures, C
|
|
|
572
742
|
return presentContainerIntrospectionAsMarkdown(data, depth, section)
|
|
573
743
|
}
|
|
574
744
|
|
|
575
|
-
/** Alias for inspectAsText */
|
|
745
|
+
/** Alias for inspectAsText. */
|
|
576
746
|
introspectAsText(sectionOrDepth?: IntrospectionSection | number, startHeadingDepth?: number): string {
|
|
577
747
|
return this.inspectAsText(sectionOrDepth, startHeadingDepth)
|
|
578
748
|
}
|
|
579
749
|
|
|
580
|
-
/** Alias for
|
|
581
|
-
introspectAsJSON(
|
|
750
|
+
/** Alias for inspect, returns JSON introspection data. */
|
|
751
|
+
introspectAsJSON(): ContainerIntrospection {
|
|
752
|
+
return this.inspect()
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Returns the container's introspection data formatted as a TypeScript interface declaration.
|
|
757
|
+
* Includes the container's own methods, getters, factories, and registered helper types.
|
|
758
|
+
*
|
|
759
|
+
* @example
|
|
760
|
+
* ```ts
|
|
761
|
+
* console.log(container.inspectAsType())
|
|
762
|
+
* // interface NodeContainer {
|
|
763
|
+
* // feature<T>(id: string, options?: object): T;
|
|
764
|
+
* // readonly uuid: string;
|
|
765
|
+
* // ...
|
|
766
|
+
* // }
|
|
767
|
+
* ```
|
|
768
|
+
*/
|
|
769
|
+
inspectAsType(): string {
|
|
770
|
+
return this.introspectAsType()
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
introspectAsType(): string {
|
|
582
774
|
const data = this.inspect()
|
|
583
|
-
return
|
|
775
|
+
return presentContainerIntrospectionAsTypeScript(data)
|
|
584
776
|
}
|
|
585
777
|
|
|
586
778
|
/** Make a property non-enumerable, which is nice for inspecting it in the REPL */
|
|
@@ -593,7 +785,7 @@ export class Container<Features extends AvailableFeatures = AvailableFeatures, C
|
|
|
593
785
|
}
|
|
594
786
|
|
|
595
787
|
/** Sleep for the specified number of milliseconds. Useful for scripting and sequencing. */
|
|
596
|
-
async sleep(ms = 1000) {
|
|
788
|
+
async sleep(ms: number = 1000): Promise<this> {
|
|
597
789
|
await new Promise((res) => setTimeout(res,ms))
|
|
598
790
|
return this
|
|
599
791
|
}
|
|
@@ -601,10 +793,22 @@ export class Container<Features extends AvailableFeatures = AvailableFeatures, C
|
|
|
601
793
|
_plugins: (() => void)[] = []
|
|
602
794
|
|
|
603
795
|
/**
|
|
604
|
-
* Apply a plugin or enable a feature by string name. Plugins
|
|
796
|
+
* Apply a plugin or enable a feature by string name. Plugins are classes with a static `attach(container)` method
|
|
797
|
+
* that extend the container with new registries, factories, or capabilities.
|
|
798
|
+
*
|
|
799
|
+
* @param plugin - A feature name string, or a class/object with a static attach method
|
|
800
|
+
* @param options - Options to pass to the plugin's attach method
|
|
801
|
+
* @returns The container instance (with the plugin's type merged in)
|
|
605
802
|
*
|
|
606
|
-
* @
|
|
607
|
-
*
|
|
803
|
+
* @example
|
|
804
|
+
* ```ts
|
|
805
|
+
* // Enable a feature by name
|
|
806
|
+
* container.use('contentDb')
|
|
807
|
+
*
|
|
808
|
+
* // Attach a plugin class (e.g. Client, Server, or custom)
|
|
809
|
+
* container.use(Client) // registers the clients registry + client() factory
|
|
810
|
+
* container.use(Server) // registers the servers registry + server() factory
|
|
811
|
+
* ```
|
|
608
812
|
*/
|
|
609
813
|
use<T = {}>(plugin: Extension<T>, options: any = {}) : this & T {
|
|
610
814
|
const container = this
|
|
@@ -640,20 +844,8 @@ function presentContainerIntrospectionAsMarkdown(data: ContainerIntrospection, s
|
|
|
640
844
|
// Header
|
|
641
845
|
sections.push(`${heading(1)} ${data.className}\n\n${data.description || ''}`)
|
|
642
846
|
|
|
643
|
-
// Container Properties section
|
|
644
|
-
|
|
645
|
-
sections.push([
|
|
646
|
-
`${heading(2)} Container Properties`,
|
|
647
|
-
'',
|
|
648
|
-
'| Property | Description |',
|
|
649
|
-
'|----------|-------------|',
|
|
650
|
-
'| `container.cwd` | Current working directory |',
|
|
651
|
-
'| `container.paths` | Path utilities scoped to cwd: `resolve()`, `join()`, `relative()`, `dirname()`, `basename()`, `parse()` |',
|
|
652
|
-
'| `container.manifest` | Parsed `package.json` for the current directory (`name`, `version`, `dependencies`, etc.) |',
|
|
653
|
-
'| `container.argv` | Raw parsed CLI arguments (from minimist). Prefer `positionals` export for positional args in commands |',
|
|
654
|
-
'| `container.utils` | Common utilities: `uuid()`, `hashObject()`, `stringUtils`, `lodash` |',
|
|
655
|
-
].join('\n'))
|
|
656
|
-
}
|
|
847
|
+
// Container Properties section — dynamic from getters data, not hardcoded
|
|
848
|
+
// (cwd, paths, manifest, argv, utils etc. come through as getters from the introspection scanner)
|
|
657
849
|
|
|
658
850
|
// Registries section
|
|
659
851
|
if (data.registries && data.registries.length > 0) {
|
|
@@ -718,6 +910,15 @@ function presentContainerIntrospectionAsMarkdown(data: ContainerIntrospection, s
|
|
|
718
910
|
sections.push(`**Returns:** \`${methodInfo.returns}\``)
|
|
719
911
|
}
|
|
720
912
|
|
|
913
|
+
if ((methodInfo as any).examples && (methodInfo as any).examples.length > 0) {
|
|
914
|
+
for (const example of (methodInfo as any).examples) {
|
|
915
|
+
if (example.title) {
|
|
916
|
+
sections.push(`**Example: ${example.title}**`)
|
|
917
|
+
}
|
|
918
|
+
sections.push(`\`\`\`${example.language || 'ts'}\n${example.code}\n\`\`\``)
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
721
922
|
sections.push('')
|
|
722
923
|
}
|
|
723
924
|
}
|
|
@@ -731,10 +932,27 @@ function presentContainerIntrospectionAsMarkdown(data: ContainerIntrospection, s
|
|
|
731
932
|
`|----------|------|-------------|`,
|
|
732
933
|
]
|
|
733
934
|
|
|
935
|
+
const gettersWithExamples: [string, any][] = []
|
|
936
|
+
|
|
734
937
|
for (const [getterName, getterInfo] of Object.entries(data.getters)) {
|
|
735
|
-
|
|
938
|
+
// Truncate long descriptions in the table
|
|
939
|
+
const desc = getterInfo.description || ''
|
|
940
|
+
const shortDesc = desc.length > 120 ? desc.slice(0, 117) + '...' : desc
|
|
941
|
+
getterTableRows.push(`| \`${getterName}\` | \`${getterInfo.returns || 'any'}\` | ${shortDesc} |`)
|
|
942
|
+
if ((getterInfo as any).examples && (getterInfo as any).examples.length > 0) {
|
|
943
|
+
gettersWithExamples.push([getterName, getterInfo])
|
|
944
|
+
}
|
|
736
945
|
}
|
|
737
946
|
sections.push(getterTableRows.join('\n'))
|
|
947
|
+
|
|
948
|
+
// Render examples for getters that have them
|
|
949
|
+
if (gettersWithExamples.length > 0) {
|
|
950
|
+
for (const [getterName, getterInfo] of gettersWithExamples) {
|
|
951
|
+
for (const example of (getterInfo as any).examples) {
|
|
952
|
+
sections.push(`\`\`\`${example.language || 'ts'}\n${example.code}\n\`\`\``)
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
738
956
|
}
|
|
739
957
|
|
|
740
958
|
// Events section
|
|
@@ -805,3 +1023,83 @@ function presentContainerIntrospectionAsMarkdown(data: ContainerIntrospection, s
|
|
|
805
1023
|
|
|
806
1024
|
return sections.join('\n\n')
|
|
807
1025
|
}
|
|
1026
|
+
|
|
1027
|
+
function presentContainerIntrospectionAsTypeScript(data: ContainerIntrospection): string {
|
|
1028
|
+
const members: string[] = []
|
|
1029
|
+
|
|
1030
|
+
// Getters
|
|
1031
|
+
if (data.getters && Object.keys(data.getters).length > 0) {
|
|
1032
|
+
for (const [name, info] of Object.entries(data.getters)) {
|
|
1033
|
+
if (info.description) {
|
|
1034
|
+
members.push(` /** ${info.description} */`)
|
|
1035
|
+
}
|
|
1036
|
+
members.push(` readonly ${name}: ${normalizeTypeString(info.returns || 'any')};`)
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// Methods
|
|
1041
|
+
if (data.methods && Object.keys(data.methods).length > 0) {
|
|
1042
|
+
if (members.length > 0) members.push('')
|
|
1043
|
+
for (const [name, info] of Object.entries(data.methods)) {
|
|
1044
|
+
if (info.description) {
|
|
1045
|
+
members.push(` /** ${info.description} */`)
|
|
1046
|
+
}
|
|
1047
|
+
const params = renderTypeScriptParams(info)
|
|
1048
|
+
members.push(` ${name}(${params}): ${normalizeTypeString(info.returns || 'void')};`)
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// Factory methods
|
|
1053
|
+
if (data.factories && data.factories.length > 0) {
|
|
1054
|
+
if (members.length > 0) members.push('')
|
|
1055
|
+
members.push(' // Factory methods')
|
|
1056
|
+
for (const factory of data.factories) {
|
|
1057
|
+
members.push(` ${factory}(id: string, options?: Record<string, any>): any;`)
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// Registries
|
|
1062
|
+
if (data.registries && data.registries.length > 0) {
|
|
1063
|
+
if (members.length > 0) members.push('')
|
|
1064
|
+
members.push(' // Registries')
|
|
1065
|
+
for (const reg of data.registries) {
|
|
1066
|
+
const available = reg.available.length > 0
|
|
1067
|
+
? reg.available.map(a => `'${a}'`).join(' | ')
|
|
1068
|
+
: 'string'
|
|
1069
|
+
members.push(` readonly ${reg.name}: { available: (${available})[]; lookup(id: string): any; };`)
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// Events — as on() overloads
|
|
1074
|
+
if (data.events && Object.keys(data.events).length > 0) {
|
|
1075
|
+
if (members.length > 0) members.push('')
|
|
1076
|
+
for (const [eventName, eventInfo] of Object.entries(data.events)) {
|
|
1077
|
+
const args = Object.entries(eventInfo.arguments || {})
|
|
1078
|
+
const listenerParams = args.length > 0
|
|
1079
|
+
? args.map(([argName, argInfo]) => `${argName}: ${normalizeTypeString(argInfo.type || 'any')}`).join(', ')
|
|
1080
|
+
: ''
|
|
1081
|
+
if (eventInfo.description) {
|
|
1082
|
+
members.push(` /** ${eventInfo.description} */`)
|
|
1083
|
+
}
|
|
1084
|
+
members.push(` on(event: '${eventName}', listener: (${listenerParams}) => void): this;`)
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// State
|
|
1089
|
+
if (data.state && Object.keys(data.state).length > 0) {
|
|
1090
|
+
if (members.length > 0) members.push('')
|
|
1091
|
+
const stateMembers = Object.entries(data.state)
|
|
1092
|
+
.map(([name, info]) => {
|
|
1093
|
+
const comment = info.description ? ` /** ${info.description} */\n` : ''
|
|
1094
|
+
return `${comment} ${name}: ${normalizeTypeString(info.type || 'any')};`
|
|
1095
|
+
})
|
|
1096
|
+
.join('\n')
|
|
1097
|
+
members.push(` state: {\n${stateMembers}\n };`)
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const description = data.description
|
|
1101
|
+
? `/**\n * ${data.description.split('\n').join('\n * ')}\n */\n`
|
|
1102
|
+
: ''
|
|
1103
|
+
|
|
1104
|
+
return `${description}interface ${data.className} {\n${members.join('\n')}\n}`
|
|
1105
|
+
}
|