@soederpop/luca 0.0.6 → 0.0.7
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 +10 -1
- package/bun.lock +1 -1
- package/commands/build-bootstrap.ts +78 -0
- package/commands/build-scaffolds.ts +24 -2
- package/commands/try-all-challenges.ts +543 -0
- package/commands/try-challenge.ts +100 -0
- package/docs/README.md +52 -80
- package/docs/TABLE-OF-CONTENTS.md +82 -51
- package/docs/apis/clients/elevenlabs.md +232 -8
- package/docs/apis/clients/graph.md +59 -8
- package/docs/apis/clients/openai.md +362 -2
- package/docs/apis/clients/rest.md +122 -2
- package/docs/apis/clients/websocket.md +71 -17
- package/docs/apis/features/agi/assistant.md +9 -3
- package/docs/apis/features/agi/assistants-manager.md +2 -2
- package/docs/apis/features/agi/claude-code.md +153 -14
- package/docs/apis/features/agi/conversation-history.md +15 -3
- package/docs/apis/features/agi/conversation.md +133 -20
- package/docs/apis/features/agi/openai-codex.md +90 -12
- package/docs/apis/features/agi/skills-library.md +23 -5
- package/docs/apis/features/node/container-link.md +59 -0
- package/docs/apis/features/node/content-db.md +1 -1
- package/docs/apis/features/node/disk-cache.md +1 -1
- package/docs/apis/features/node/dns.md +1 -0
- package/docs/apis/features/node/docker.md +2 -1
- package/docs/apis/features/node/esbuild.md +4 -3
- package/docs/apis/features/node/file-manager.md +13 -4
- package/docs/apis/features/node/fs.md +726 -171
- package/docs/apis/features/node/git.md +1 -0
- package/docs/apis/features/node/google-auth.md +23 -4
- package/docs/apis/features/node/google-calendar.md +14 -2
- package/docs/apis/features/node/google-docs.md +15 -2
- package/docs/apis/features/node/google-drive.md +21 -3
- package/docs/apis/features/node/google-sheets.md +14 -2
- package/docs/apis/features/node/grep.md +2 -0
- package/docs/apis/features/node/helpers.md +29 -0
- package/docs/apis/features/node/ink.md +2 -2
- package/docs/apis/features/node/networking.md +39 -4
- package/docs/apis/features/node/os.md +28 -0
- package/docs/apis/features/node/postgres.md +26 -4
- package/docs/apis/features/node/proc.md +37 -28
- package/docs/apis/features/node/process-manager.md +33 -5
- package/docs/apis/features/node/repl.md +1 -1
- package/docs/apis/features/node/runpod.md +1 -0
- package/docs/apis/features/node/secure-shell.md +7 -0
- package/docs/apis/features/node/semantic-search.md +12 -5
- package/docs/apis/features/node/sqlite.md +26 -4
- package/docs/apis/features/node/telegram.md +30 -5
- package/docs/apis/features/node/tts.md +17 -2
- package/docs/apis/features/node/ui.md +1 -1
- package/docs/apis/features/node/vault.md +4 -9
- package/docs/apis/features/node/vm.md +3 -12
- package/docs/apis/features/node/window-manager.md +128 -20
- package/docs/apis/features/web/asset-loader.md +13 -1
- package/docs/apis/features/web/container-link.md +59 -0
- package/docs/apis/features/web/esbuild.md +4 -3
- package/docs/apis/features/web/helpers.md +29 -0
- package/docs/apis/features/web/network.md +16 -2
- package/docs/apis/features/web/speech.md +16 -2
- package/docs/apis/features/web/vault.md +4 -9
- package/docs/apis/features/web/vm.md +3 -12
- package/docs/apis/features/web/voice.md +18 -1
- package/docs/apis/servers/express.md +18 -2
- package/docs/apis/servers/mcp.md +29 -4
- package/docs/apis/servers/websocket.md +34 -6
- package/docs/bootstrap/CLAUDE.md +100 -0
- package/docs/bootstrap/SKILL.md +222 -0
- package/docs/bootstrap/templates/about-command.ts +41 -0
- package/docs/bootstrap/templates/docs-models.ts +22 -0
- package/docs/bootstrap/templates/docs-readme.md +43 -0
- package/docs/bootstrap/templates/example-feature.ts +53 -0
- package/docs/bootstrap/templates/health-endpoint.ts +15 -0
- package/docs/bootstrap/templates/luca-cli.ts +25 -0
- package/docs/challenges/caching-proxy.md +16 -0
- package/docs/challenges/content-db-round-trip.md +14 -0
- package/docs/challenges/custom-command.md +9 -0
- package/docs/challenges/file-watcher-pipeline.md +11 -0
- package/docs/challenges/grep-audit-report.md +15 -0
- package/docs/challenges/multi-feature-dashboard.md +14 -0
- package/docs/challenges/process-orchestrator.md +17 -0
- package/docs/challenges/rest-api-server-with-client.md +12 -0
- package/docs/challenges/script-runner-with-vm.md +11 -0
- package/docs/challenges/simple-rest-api.md +15 -0
- package/docs/challenges/websocket-serve-and-client.md +11 -0
- package/docs/challenges/yaml-config-system.md +14 -0
- package/docs/command-system-overhaul.md +94 -0
- package/docs/examples/assistant/CORE.md +18 -0
- package/docs/examples/assistant/hooks.ts +3 -0
- package/docs/examples/assistant/tools.ts +10 -0
- package/docs/examples/window-manager-layouts.md +180 -0
- package/docs/in-memory-fs.md +4 -0
- package/docs/models.ts +13 -10
- package/docs/philosophy.md +4 -3
- package/docs/reports/console-hmr-design.md +170 -0
- package/docs/reports/helper-semantic-search.md +72 -0
- package/docs/scaffolds/client.md +29 -20
- package/docs/scaffolds/command.md +64 -50
- package/docs/scaffolds/endpoint.md +31 -36
- package/docs/scaffolds/feature.md +28 -18
- package/docs/scaffolds/selector.md +91 -0
- package/docs/scaffolds/server.md +18 -9
- package/docs/selectors.md +115 -0
- package/docs/sessions/custom-command/attempt-log-2.md +195 -0
- package/docs/sessions/file-watcher-pipeline/attempt-log-1.md +728 -0
- package/docs/sessions/file-watcher-pipeline/attempt-log-2.md +555 -0
- package/docs/sessions/grep-audit-report/attempt-log-1.md +289 -0
- package/docs/sessions/multi-feature-dashboard/attempt-log-2.md +679 -0
- package/docs/sessions/rest-api-server-with-client/attempt-log-1.md +1 -0
- package/docs/sessions/rest-api-server-with-client/attempt-log-3.md +920 -0
- package/docs/sessions/simple-rest-api/attempt-log-1.md +593 -0
- package/docs/sessions/websocket-serve-and-client/attempt-log-2.md +995 -0
- package/docs/tutorials/00-bootstrap.md +148 -0
- package/docs/tutorials/07-endpoints.md +7 -7
- package/docs/tutorials/08-commands.md +153 -72
- package/luca.cli.ts +3 -0
- package/package.json +6 -5
- package/public/index.html +1430 -0
- package/scripts/examples/using-ollama.ts +2 -1
- package/scripts/update-introspection-data.ts +2 -2
- package/src/agi/endpoints/experts.ts +1 -1
- package/src/agi/features/assistant.ts +7 -0
- package/src/agi/features/assistants-manager.ts +5 -5
- package/src/agi/features/claude-code.ts +263 -3
- package/src/agi/features/conversation-history.ts +7 -1
- package/src/agi/features/conversation.ts +26 -3
- package/src/agi/features/openai-codex.ts +26 -2
- package/src/agi/features/openapi.ts +6 -1
- package/src/agi/features/skills-library.ts +9 -1
- package/src/bootstrap/generated.ts +540 -0
- package/src/cli/cli.ts +64 -21
- package/src/client.ts +23 -357
- package/src/clients/civitai/index.ts +1 -1
- package/src/clients/client-template.ts +1 -1
- package/src/clients/comfyui/index.ts +13 -2
- package/src/clients/elevenlabs/index.ts +2 -1
- package/src/clients/graph.ts +87 -0
- package/src/clients/openai/index.ts +10 -1
- package/src/clients/rest.ts +207 -0
- package/src/clients/websocket.ts +176 -0
- package/src/command.ts +281 -34
- package/src/commands/bootstrap.ts +181 -0
- package/src/commands/chat.ts +5 -4
- package/src/commands/describe.ts +225 -2
- package/src/commands/help.ts +35 -9
- package/src/commands/index.ts +3 -0
- package/src/commands/introspect.ts +92 -2
- package/src/commands/prompt.ts +5 -6
- package/src/commands/run.ts +33 -10
- package/src/commands/save-api-docs.ts +49 -0
- package/src/commands/scaffold.ts +169 -23
- package/src/commands/select.ts +94 -0
- package/src/commands/serve.ts +10 -1
- package/src/container.ts +15 -0
- package/src/endpoint.ts +19 -0
- package/src/graft.ts +181 -0
- package/src/introspection/generated.agi.ts +12458 -8968
- package/src/introspection/generated.node.ts +10573 -7145
- package/src/introspection/generated.web.ts +1 -1
- package/src/introspection/index.ts +26 -0
- package/src/node/container.ts +6 -7
- package/src/node/features/content-db.ts +49 -2
- package/src/node/features/disk-cache.ts +16 -9
- package/src/node/features/dns.ts +16 -3
- package/src/node/features/docker.ts +16 -4
- package/src/node/features/esbuild.ts +20 -0
- package/src/node/features/file-manager.ts +184 -29
- package/src/node/features/fs.ts +704 -248
- package/src/node/features/git.ts +21 -8
- package/src/node/features/grep.ts +23 -3
- package/src/node/features/helpers.ts +372 -43
- package/src/node/features/networking.ts +39 -4
- package/src/node/features/opener.ts +28 -15
- package/src/node/features/os.ts +76 -0
- package/src/node/features/port-exposer.ts +11 -1
- package/src/node/features/postgres.ts +17 -1
- package/src/node/features/proc.ts +4 -1
- package/src/node/features/python.ts +63 -14
- package/src/node/features/repl.ts +11 -7
- package/src/node/features/runpod.ts +16 -3
- package/src/node/features/secure-shell.ts +27 -2
- package/src/node/features/semantic-search.ts +12 -1
- package/src/node/features/ui.ts +5 -69
- package/src/node/features/vm.ts +17 -0
- package/src/node/features/window-manager.ts +68 -20
- package/src/node.ts +5 -0
- package/src/scaffolds/generated.ts +492 -290
- package/src/scaffolds/template.ts +9 -0
- package/src/schemas/base.ts +46 -5
- package/src/selector.ts +282 -0
- package/src/server.ts +11 -0
- package/src/servers/express.ts +27 -12
- package/src/servers/socket.ts +45 -11
- package/src/web/clients/socket.ts +4 -1
- package/src/web/container.ts +2 -1
- package/src/web/features/network.ts +7 -1
- package/src/web/features/voice-recognition.ts +16 -1
- package/test/clients-servers.test.ts +2 -1
- package/test/command.test.ts +267 -0
- package/test-integration/assistants-manager.test.ts +10 -20
- package/tmp/.cache/luca-disk-cache/content-v2/sha512/1b/b5/c75b28794f00f94c4d609a98978e9420e9b7146d204a7fbf5b0b30477292581705d207c0100dabaac27eef540aaaece3374af75104a93219d4ec8bfb44e7 +1 -0
- package/tmp/.cache/luca-disk-cache/content-v2/sha512/da/df/1d90ce4e042abeb035a197832c6d6893420a747a056be773eb00e4f745a037d505c8db13dde7d36b36b6b893addbb7df0f5fe9f0c13e665f20056447318b +1 -0
- package/tmp/.cache/luca-disk-cache/content-v2/sha512/ed/04/e1d0c2a58c2db29b3921ca2affb3ea4febe831c53b38ebc21019fb799823aba6ed5b4611873d2cd25d422d49955b852a9c326da0d678899bc1c2c2960901 +1 -0
- package/tmp/.cache/luca-disk-cache/index-v5/00/13/572aa4c9a94f99eda999695d050cdd0ca7fe2d23a50af03234d4c8ce0791 +2 -0
- package/tmp/.cache/luca-disk-cache/index-v5/75/a9/cb61dc0f0589e8ec10a9aca27b834bc73884c479941042d22a2b22324cd3 +2 -0
- package/tmp/.cache/luca-disk-cache/index-v5/9f/0f/8b1f915ee64cfff7667dd96acd7a5ac0a96aa91a346e19cefd45909a9c9c +2 -0
- package/docs/apis/features/node/launcher-app-command-listener.md +0 -145
- package/docs/examples/launcher-app-command-listener.md +0 -120
- package/docs/tasks/web-container-helper-discovery.md +0 -71
- package/docs/todos.md +0 -1
- package/scripts/test-command-listener.ts +0 -123
- package/src/node/features/launcher-app-command-listener.ts +0 -389
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Auto-generated scaffold and MCP readme content
|
|
2
|
-
// Generated at: 2026-03-
|
|
3
|
-
// Source: docs/scaffolds/*.md and docs/mcp/readme.md
|
|
2
|
+
// Generated at: 2026-03-19T00:28:06.880Z
|
|
3
|
+
// Source: docs/scaffolds/*.md, docs/examples/assistant/, and docs/mcp/readme.md
|
|
4
4
|
//
|
|
5
5
|
// Do not edit manually. Run: luca build-scaffolds
|
|
6
6
|
|
|
@@ -20,8 +20,7 @@ export const scaffolds: Record<string, ScaffoldData> = {
|
|
|
20
20
|
sections: [
|
|
21
21
|
{ heading: "Imports", code: `import { z } from 'zod'
|
|
22
22
|
import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '@soederpop/luca'
|
|
23
|
-
import { Feature
|
|
24
|
-
import type { ContainerContext } from '@soederpop/luca'` },
|
|
23
|
+
import { Feature } from '@soederpop/luca'` },
|
|
25
24
|
{ heading: "Schemas", code: `export const {{PascalName}}StateSchema = FeatureStateSchema.extend({
|
|
26
25
|
// Add your state fields here. These are observable — changes emit events.
|
|
27
26
|
// Example: itemCount: z.number().default(0).describe('Number of items stored'),
|
|
@@ -40,8 +39,6 @@ export const {{PascalName}}EventsSchema = FeatureEventsSchema.extend({
|
|
|
40
39
|
})` },
|
|
41
40
|
{ heading: "Class", code: `/**
|
|
42
41
|
* {{description}}
|
|
43
|
-
*
|
|
44
|
-
* @example
|
|
45
42
|
* \`\`\`typescript
|
|
46
43
|
* const {{camelName}} = container.feature('{{camelName}}')
|
|
47
44
|
* \`\`\`
|
|
@@ -53,25 +50,30 @@ export class {{PascalName}} extends Feature<{{PascalName}}State, {{PascalName}}O
|
|
|
53
50
|
static override stateSchema = {{PascalName}}StateSchema
|
|
54
51
|
static override optionsSchema = {{PascalName}}OptionsSchema
|
|
55
52
|
static override eventsSchema = {{PascalName}}EventsSchema
|
|
56
|
-
static override description = '{{description}}'
|
|
57
53
|
|
|
58
|
-
|
|
59
|
-
super(options, context)
|
|
60
|
-
// Initialize state, set up resources
|
|
61
|
-
}
|
|
54
|
+
static { Feature.register(this, '{{camelName}}') }
|
|
62
55
|
|
|
63
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Called after the feature is initialized. Use this for any setup logic
|
|
58
|
+
* instead of overriding the constructor.
|
|
59
|
+
*/
|
|
60
|
+
async afterInitialize() {
|
|
61
|
+
// Set up initial state, start background tasks, etc.
|
|
62
|
+
}
|
|
64
63
|
}` },
|
|
65
64
|
{ heading: "Module Augmentation", code: `declare module '@soederpop/luca' {
|
|
66
65
|
interface AvailableFeatures {
|
|
67
66
|
{{camelName}}: typeof {{PascalName}}
|
|
68
67
|
}
|
|
69
68
|
}` },
|
|
70
|
-
{ heading: "Registration", code:
|
|
69
|
+
{ heading: "Registration", code: `// Inside the class:
|
|
70
|
+
static { Feature.register(this, '{{camelName}}') }
|
|
71
|
+
|
|
72
|
+
// At module level:
|
|
73
|
+
export default {{PascalName}}` },
|
|
71
74
|
{ heading: "Complete Example", code: `import { z } from 'zod'
|
|
72
75
|
import { FeatureStateSchema, FeatureOptionsSchema } from '@soederpop/luca'
|
|
73
|
-
import { Feature
|
|
74
|
-
import type { ContainerContext } from '@soederpop/luca'
|
|
76
|
+
import { Feature } from '@soederpop/luca'
|
|
75
77
|
|
|
76
78
|
declare module '@soederpop/luca' {
|
|
77
79
|
interface AvailableFeatures {
|
|
@@ -99,19 +101,18 @@ export class {{PascalName}} extends Feature<{{PascalName}}State, {{PascalName}}O
|
|
|
99
101
|
static override shortcut = 'features.{{camelName}}' as const
|
|
100
102
|
static override stateSchema = {{PascalName}}StateSchema
|
|
101
103
|
static override optionsSchema = {{PascalName}}OptionsSchema
|
|
102
|
-
static
|
|
104
|
+
static { Feature.register(this, '{{camelName}}') }
|
|
103
105
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
+
async afterInitialize() {
|
|
107
|
+
// Setup logic goes here — not in the constructor
|
|
106
108
|
}
|
|
107
109
|
}
|
|
108
110
|
|
|
109
|
-
export default
|
|
111
|
+
export default {{PascalName}}` }
|
|
110
112
|
],
|
|
111
113
|
full: `import { z } from 'zod'
|
|
112
114
|
import { FeatureStateSchema, FeatureOptionsSchema } from '@soederpop/luca'
|
|
113
|
-
import { Feature
|
|
114
|
-
import type { ContainerContext } from '@soederpop/luca'
|
|
115
|
+
import { Feature } from '@soederpop/luca'
|
|
115
116
|
|
|
116
117
|
declare module '@soederpop/luca' {
|
|
117
118
|
interface AvailableFeatures {
|
|
@@ -139,14 +140,14 @@ export class {{PascalName}} extends Feature<{{PascalName}}State, {{PascalName}}O
|
|
|
139
140
|
static override shortcut = 'features.{{camelName}}' as const
|
|
140
141
|
static override stateSchema = {{PascalName}}StateSchema
|
|
141
142
|
static override optionsSchema = {{PascalName}}OptionsSchema
|
|
142
|
-
static
|
|
143
|
+
static { Feature.register(this, '{{camelName}}') }
|
|
143
144
|
|
|
144
|
-
|
|
145
|
-
|
|
145
|
+
async afterInitialize() {
|
|
146
|
+
// Setup logic goes here — not in the constructor
|
|
146
147
|
}
|
|
147
148
|
}
|
|
148
149
|
|
|
149
|
-
export default
|
|
150
|
+
export default {{PascalName}}`,
|
|
150
151
|
tutorial: `# Building a Feature
|
|
151
152
|
|
|
152
153
|
A feature is a container-managed capability — something your application needs that lives on the machine (file I/O, caching, encryption, etc). Features are lazy-loaded, observable, and self-documenting.
|
|
@@ -161,12 +162,15 @@ When to build a feature:
|
|
|
161
162
|
\`\`\`ts
|
|
162
163
|
import { z } from 'zod'
|
|
163
164
|
import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '@soederpop/luca'
|
|
164
|
-
import { Feature
|
|
165
|
-
import type { ContainerContext } from '@soederpop/luca'
|
|
165
|
+
import { Feature } from '@soederpop/luca'
|
|
166
166
|
\`\`\`
|
|
167
167
|
|
|
168
168
|
These are the only imports your feature file needs from luca. If your feature wraps a third-party library, import it here too — feature implementations are the ONE place where direct library imports are allowed.
|
|
169
169
|
|
|
170
|
+
The use of dynamic imports is encouraged here, only import libraries you need when the feature is used, and only when necessary in the lifecycle of the feature if it can be done.
|
|
171
|
+
|
|
172
|
+
feature's have built in ways to check if their requirements are supported and can be enabled cautiously.
|
|
173
|
+
|
|
170
174
|
## Schemas
|
|
171
175
|
|
|
172
176
|
Define the shape of your feature's state, options, and events using Zod. Every field must have a \`.describe()\` — this becomes the documentation.
|
|
@@ -194,11 +198,11 @@ export const {{PascalName}}EventsSchema = FeatureEventsSchema.extend({
|
|
|
194
198
|
|
|
195
199
|
The class extends \`Feature\` with your state and options types. Static properties drive registration and introspection. Every public method needs a JSDoc block with \`@param\`, \`@returns\`, and \`@example\`.
|
|
196
200
|
|
|
201
|
+
Running \`luca introspect\` captures JSDoc blocks and Zod schemas and includes them in the description whenever somebody calls \`container.features.describe('{{camelName}}')\` or \`luca describe {{camelName}}\`.
|
|
202
|
+
|
|
197
203
|
\`\`\`ts
|
|
198
204
|
/**
|
|
199
205
|
* {{description}}
|
|
200
|
-
*
|
|
201
|
-
* @example
|
|
202
206
|
* \`\`\`typescript
|
|
203
207
|
* const {{camelName}} = container.feature('{{camelName}}')
|
|
204
208
|
* \`\`\`
|
|
@@ -210,17 +214,21 @@ export class {{PascalName}} extends Feature<{{PascalName}}State, {{PascalName}}O
|
|
|
210
214
|
static override stateSchema = {{PascalName}}StateSchema
|
|
211
215
|
static override optionsSchema = {{PascalName}}OptionsSchema
|
|
212
216
|
static override eventsSchema = {{PascalName}}EventsSchema
|
|
213
|
-
static override description = '{{description}}'
|
|
214
217
|
|
|
215
|
-
|
|
216
|
-
super(options, context)
|
|
217
|
-
// Initialize state, set up resources
|
|
218
|
-
}
|
|
218
|
+
static { Feature.register(this, '{{camelName}}') }
|
|
219
219
|
|
|
220
|
-
|
|
220
|
+
/**
|
|
221
|
+
* Called after the feature is initialized. Use this for any setup logic
|
|
222
|
+
* instead of overriding the constructor.
|
|
223
|
+
*/
|
|
224
|
+
async afterInitialize() {
|
|
225
|
+
// Set up initial state, start background tasks, etc.
|
|
226
|
+
}
|
|
221
227
|
}
|
|
222
228
|
\`\`\`
|
|
223
229
|
|
|
230
|
+
**Important**: You almost never need to override the constructor. Use \`afterInitialize()\` for any setup logic — it runs after the feature is fully wired into the container and has access to \`this.container\`, \`this.options\`, \`this.state\`, etc.
|
|
231
|
+
|
|
224
232
|
## Module Augmentation
|
|
225
233
|
|
|
226
234
|
This is what gives \`container.feature('yourName')\` TypeScript autocomplete. Without it, the feature works but TypeScript won't know about it.
|
|
@@ -235,10 +243,14 @@ declare module '@soederpop/luca' {
|
|
|
235
243
|
|
|
236
244
|
## Registration
|
|
237
245
|
|
|
238
|
-
|
|
246
|
+
Registration happens inside the class body using a static block. The default export is just the class itself.
|
|
239
247
|
|
|
240
248
|
\`\`\`ts
|
|
241
|
-
|
|
249
|
+
// Inside the class:
|
|
250
|
+
static { Feature.register(this, '{{camelName}}') }
|
|
251
|
+
|
|
252
|
+
// At module level:
|
|
253
|
+
export default {{PascalName}}
|
|
242
254
|
\`\`\`
|
|
243
255
|
|
|
244
256
|
## Complete Example
|
|
@@ -248,8 +260,7 @@ Here's a minimal but complete feature. This is what a real feature file looks li
|
|
|
248
260
|
\`\`\`ts
|
|
249
261
|
import { z } from 'zod'
|
|
250
262
|
import { FeatureStateSchema, FeatureOptionsSchema } from '@soederpop/luca'
|
|
251
|
-
import { Feature
|
|
252
|
-
import type { ContainerContext } from '@soederpop/luca'
|
|
263
|
+
import { Feature } from '@soederpop/luca'
|
|
253
264
|
|
|
254
265
|
declare module '@soederpop/luca' {
|
|
255
266
|
interface AvailableFeatures {
|
|
@@ -277,14 +288,14 @@ export class {{PascalName}} extends Feature<{{PascalName}}State, {{PascalName}}O
|
|
|
277
288
|
static override shortcut = 'features.{{camelName}}' as const
|
|
278
289
|
static override stateSchema = {{PascalName}}StateSchema
|
|
279
290
|
static override optionsSchema = {{PascalName}}OptionsSchema
|
|
280
|
-
static
|
|
291
|
+
static { Feature.register(this, '{{camelName}}') }
|
|
281
292
|
|
|
282
|
-
|
|
283
|
-
|
|
293
|
+
async afterInitialize() {
|
|
294
|
+
// Setup logic goes here — not in the constructor
|
|
284
295
|
}
|
|
285
296
|
}
|
|
286
297
|
|
|
287
|
-
export default
|
|
298
|
+
export default {{PascalName}}
|
|
288
299
|
\`\`\`
|
|
289
300
|
|
|
290
301
|
## Conventions
|
|
@@ -300,9 +311,8 @@ export default features.register('{{camelName}}', {{PascalName}})
|
|
|
300
311
|
client: {
|
|
301
312
|
sections: [
|
|
302
313
|
{ heading: "Imports", code: `import { z } from 'zod'
|
|
303
|
-
import { Client,
|
|
304
|
-
import { ClientStateSchema, ClientOptionsSchema, ClientEventsSchema } from '@soederpop/luca'
|
|
305
|
-
import type { ContainerContext } from '@soederpop/luca'` },
|
|
314
|
+
import { Client, RestClient } from '@soederpop/luca/client'
|
|
315
|
+
import { ClientStateSchema, ClientOptionsSchema, ClientEventsSchema } from '@soederpop/luca'` },
|
|
306
316
|
{ heading: "Schemas", code: `export const {{PascalName}}StateSchema = ClientStateSchema.extend({
|
|
307
317
|
// Add your state fields here.
|
|
308
318
|
// Example: authenticated: z.boolean().default(false).describe('Whether API auth is configured'),
|
|
@@ -328,14 +338,14 @@ export class {{PascalName}} extends RestClient<{{PascalName}}State, {{PascalName
|
|
|
328
338
|
static override shortcut = 'clients.{{camelName}}' as const
|
|
329
339
|
static override stateSchema = {{PascalName}}StateSchema
|
|
330
340
|
static override optionsSchema = {{PascalName}}OptionsSchema
|
|
331
|
-
static
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
341
|
+
static { Client.register(this, '{{camelName}}') }
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Called after the client is initialized. Use this for any setup logic
|
|
345
|
+
* instead of overriding the constructor.
|
|
346
|
+
*/
|
|
347
|
+
async afterInitialize() {
|
|
348
|
+
// Set up default headers, configure auth, etc.
|
|
339
349
|
}
|
|
340
350
|
|
|
341
351
|
// Add API methods here. Each wraps an endpoint.
|
|
@@ -349,11 +359,14 @@ export class {{PascalName}} extends RestClient<{{PascalName}}State, {{PascalName
|
|
|
349
359
|
{{camelName}}: typeof {{PascalName}}
|
|
350
360
|
}
|
|
351
361
|
}` },
|
|
352
|
-
{ heading: "Registration", code:
|
|
362
|
+
{ heading: "Registration", code: `// Inside the class:
|
|
363
|
+
static { Client.register(this, '{{camelName}}') }
|
|
364
|
+
|
|
365
|
+
// At module level:
|
|
366
|
+
export default {{PascalName}}` },
|
|
353
367
|
{ heading: "Complete Example", code: `import { z } from 'zod'
|
|
354
|
-
import {
|
|
368
|
+
import { Client, RestClient } from '@soederpop/luca/client'
|
|
355
369
|
import { ClientStateSchema, ClientOptionsSchema } from '@soederpop/luca'
|
|
356
|
-
import type { ContainerContext } from '@soederpop/luca'
|
|
357
370
|
|
|
358
371
|
declare module '@soederpop/luca/client' {
|
|
359
372
|
interface AvailableClients {
|
|
@@ -383,19 +396,18 @@ export class {{PascalName}} extends RestClient<{{PascalName}}State, {{PascalName
|
|
|
383
396
|
static override shortcut = 'clients.{{camelName}}' as const
|
|
384
397
|
static override stateSchema = {{PascalName}}StateSchema
|
|
385
398
|
static override optionsSchema = {{PascalName}}OptionsSchema
|
|
386
|
-
static
|
|
399
|
+
static { Client.register(this, '{{camelName}}') }
|
|
387
400
|
|
|
388
|
-
|
|
389
|
-
|
|
401
|
+
async afterInitialize() {
|
|
402
|
+
// Setup logic goes here — not in the constructor
|
|
390
403
|
}
|
|
391
404
|
}
|
|
392
405
|
|
|
393
|
-
export default
|
|
406
|
+
export default {{PascalName}}` }
|
|
394
407
|
],
|
|
395
408
|
full: `import { z } from 'zod'
|
|
396
|
-
import {
|
|
409
|
+
import { Client, RestClient } from '@soederpop/luca/client'
|
|
397
410
|
import { ClientStateSchema, ClientOptionsSchema } from '@soederpop/luca'
|
|
398
|
-
import type { ContainerContext } from '@soederpop/luca'
|
|
399
411
|
|
|
400
412
|
declare module '@soederpop/luca/client' {
|
|
401
413
|
interface AvailableClients {
|
|
@@ -425,14 +437,14 @@ export class {{PascalName}} extends RestClient<{{PascalName}}State, {{PascalName
|
|
|
425
437
|
static override shortcut = 'clients.{{camelName}}' as const
|
|
426
438
|
static override stateSchema = {{PascalName}}StateSchema
|
|
427
439
|
static override optionsSchema = {{PascalName}}OptionsSchema
|
|
428
|
-
static
|
|
440
|
+
static { Client.register(this, '{{camelName}}') }
|
|
429
441
|
|
|
430
|
-
|
|
431
|
-
|
|
442
|
+
async afterInitialize() {
|
|
443
|
+
// Setup logic goes here — not in the constructor
|
|
432
444
|
}
|
|
433
445
|
}
|
|
434
446
|
|
|
435
|
-
export default
|
|
447
|
+
export default {{PascalName}}`,
|
|
436
448
|
tutorial: `# Building a Client
|
|
437
449
|
|
|
438
450
|
A client is a container-managed connection to an external service. Clients handle network communication — HTTP APIs, WebSocket connections, GraphQL endpoints. They extend \`RestClient\` (for HTTP), \`WebSocketClient\` (for WS), or the base \`Client\` class.
|
|
@@ -446,9 +458,8 @@ When to build a client:
|
|
|
446
458
|
|
|
447
459
|
\`\`\`ts
|
|
448
460
|
import { z } from 'zod'
|
|
449
|
-
import { Client,
|
|
461
|
+
import { Client, RestClient } from '@soederpop/luca/client'
|
|
450
462
|
import { ClientStateSchema, ClientOptionsSchema, ClientEventsSchema } from '@soederpop/luca'
|
|
451
|
-
import type { ContainerContext } from '@soederpop/luca'
|
|
452
463
|
\`\`\`
|
|
453
464
|
|
|
454
465
|
Use \`RestClient\` for HTTP APIs (most common). It gives you \`get\`, \`post\`, \`put\`, \`patch\`, \`delete\` methods that handle JSON, headers, and error wrapping.
|
|
@@ -471,6 +482,8 @@ export type {{PascalName}}Options = z.infer<typeof {{PascalName}}OptionsSchema>
|
|
|
471
482
|
|
|
472
483
|
## Class
|
|
473
484
|
|
|
485
|
+
Running \`luca introspect\` captures JSDoc blocks and Zod schemas and includes them in the description whenever somebody calls \`container.clients.describe('{{camelName}}')\` or \`luca describe {{camelName}}\`.
|
|
486
|
+
|
|
474
487
|
\`\`\`ts
|
|
475
488
|
/**
|
|
476
489
|
* {{description}}
|
|
@@ -486,14 +499,14 @@ export class {{PascalName}} extends RestClient<{{PascalName}}State, {{PascalName
|
|
|
486
499
|
static override shortcut = 'clients.{{camelName}}' as const
|
|
487
500
|
static override stateSchema = {{PascalName}}StateSchema
|
|
488
501
|
static override optionsSchema = {{PascalName}}OptionsSchema
|
|
489
|
-
static
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
502
|
+
static { Client.register(this, '{{camelName}}') }
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Called after the client is initialized. Use this for any setup logic
|
|
506
|
+
* instead of overriding the constructor.
|
|
507
|
+
*/
|
|
508
|
+
async afterInitialize() {
|
|
509
|
+
// Set up default headers, configure auth, etc.
|
|
497
510
|
}
|
|
498
511
|
|
|
499
512
|
// Add API methods here. Each wraps an endpoint.
|
|
@@ -504,6 +517,8 @@ export class {{PascalName}} extends RestClient<{{PascalName}}State, {{PascalName
|
|
|
504
517
|
}
|
|
505
518
|
\`\`\`
|
|
506
519
|
|
|
520
|
+
**Important**: You almost never need to override the constructor. Use \`afterInitialize()\` for setup logic — it runs after the client is fully wired into the container. Set \`baseURL\` via the options schema default instead of constructor manipulation.
|
|
521
|
+
|
|
507
522
|
## Module Augmentation
|
|
508
523
|
|
|
509
524
|
\`\`\`ts
|
|
@@ -516,17 +531,22 @@ declare module '@soederpop/luca/client' {
|
|
|
516
531
|
|
|
517
532
|
## Registration
|
|
518
533
|
|
|
534
|
+
Registration happens inside the class body using a static block. The default export is just the class itself.
|
|
535
|
+
|
|
519
536
|
\`\`\`ts
|
|
520
|
-
|
|
537
|
+
// Inside the class:
|
|
538
|
+
static { Client.register(this, '{{camelName}}') }
|
|
539
|
+
|
|
540
|
+
// At module level:
|
|
541
|
+
export default {{PascalName}}
|
|
521
542
|
\`\`\`
|
|
522
543
|
|
|
523
544
|
## Complete Example
|
|
524
545
|
|
|
525
546
|
\`\`\`ts
|
|
526
547
|
import { z } from 'zod'
|
|
527
|
-
import {
|
|
548
|
+
import { Client, RestClient } from '@soederpop/luca/client'
|
|
528
549
|
import { ClientStateSchema, ClientOptionsSchema } from '@soederpop/luca'
|
|
529
|
-
import type { ContainerContext } from '@soederpop/luca'
|
|
530
550
|
|
|
531
551
|
declare module '@soederpop/luca/client' {
|
|
532
552
|
interface AvailableClients {
|
|
@@ -556,31 +576,32 @@ export class {{PascalName}} extends RestClient<{{PascalName}}State, {{PascalName
|
|
|
556
576
|
static override shortcut = 'clients.{{camelName}}' as const
|
|
557
577
|
static override stateSchema = {{PascalName}}StateSchema
|
|
558
578
|
static override optionsSchema = {{PascalName}}OptionsSchema
|
|
559
|
-
static
|
|
579
|
+
static { Client.register(this, '{{camelName}}') }
|
|
560
580
|
|
|
561
|
-
|
|
562
|
-
|
|
581
|
+
async afterInitialize() {
|
|
582
|
+
// Setup logic goes here — not in the constructor
|
|
563
583
|
}
|
|
564
584
|
}
|
|
565
585
|
|
|
566
|
-
export default
|
|
586
|
+
export default {{PascalName}}
|
|
567
587
|
\`\`\`
|
|
568
588
|
|
|
569
589
|
## Conventions
|
|
570
590
|
|
|
571
591
|
- **Extend RestClient for HTTP**: It gives you typed HTTP methods. Only use base \`Client\` if you need a non-HTTP protocol.
|
|
572
|
-
- **Set baseURL
|
|
592
|
+
- **Set baseURL via options schema**: Use a Zod \`.default()\` on the \`baseURL\` field rather than overriding the constructor.
|
|
593
|
+
- **Use \`afterInitialize()\`**: For any setup logic (auth, default headers, etc.) instead of overriding the constructor.
|
|
573
594
|
- **Wrap endpoints as methods**: Each API endpoint gets a method. Keep them thin — just map to HTTP calls.
|
|
574
|
-
- **JSDoc everything**: Every public method needs \`@param\`, \`@returns\`, \`@example\`.
|
|
575
|
-
- **Auth in options**: Pass API keys, tokens via options schema. Check them in
|
|
595
|
+
- **JSDoc everything**: Every public method needs \`@param\`, \`@returns\`, \`@example\`. Run \`luca introspect\` after changes to update generated docs.
|
|
596
|
+
- **Auth in options**: Pass API keys, tokens via options schema. Check them in \`afterInitialize()\` or a setup method.
|
|
576
597
|
`,
|
|
577
598
|
},
|
|
578
599
|
server: {
|
|
579
600
|
sections: [
|
|
580
601
|
{ heading: "Imports", code: `import { z } from 'zod'
|
|
581
|
-
import { Server
|
|
602
|
+
import { Server } from '@soederpop/luca'
|
|
582
603
|
import { ServerStateSchema, ServerOptionsSchema, ServerEventsSchema } from '@soederpop/luca'
|
|
583
|
-
import type {
|
|
604
|
+
import type { NodeContainer } from '@soederpop/luca'
|
|
584
605
|
import type { ServersInterface } from '@soederpop/luca'` },
|
|
585
606
|
{ heading: "Schemas", code: `export const {{PascalName}}StateSchema = ServerStateSchema.extend({
|
|
586
607
|
// Add your state fields here.
|
|
@@ -614,7 +635,7 @@ export class {{PascalName}} extends Server<{{PascalName}}State, {{PascalName}}Op
|
|
|
614
635
|
static override stateSchema = {{PascalName}}StateSchema
|
|
615
636
|
static override optionsSchema = {{PascalName}}OptionsSchema
|
|
616
637
|
static override eventsSchema = {{PascalName}}EventsSchema
|
|
617
|
-
static
|
|
638
|
+
static { Server.register(this, '{{camelName}}') }
|
|
618
639
|
|
|
619
640
|
static override attach(container: NodeContainer & ServersInterface) {
|
|
620
641
|
return container
|
|
@@ -651,11 +672,15 @@ export class {{PascalName}} extends Server<{{PascalName}}State, {{PascalName}}Op
|
|
|
651
672
|
{{camelName}}: typeof {{PascalName}}
|
|
652
673
|
}
|
|
653
674
|
}` },
|
|
654
|
-
{ heading: "Registration", code:
|
|
675
|
+
{ heading: "Registration", code: `// Inside the class:
|
|
676
|
+
static { Server.register(this, '{{camelName}}') }
|
|
677
|
+
|
|
678
|
+
// At module level:
|
|
679
|
+
export default {{PascalName}}` },
|
|
655
680
|
{ heading: "Complete Example", code: `import { z } from 'zod'
|
|
656
|
-
import { Server
|
|
681
|
+
import { Server } from '@soederpop/luca'
|
|
657
682
|
import { ServerStateSchema, ServerOptionsSchema, ServerEventsSchema } from '@soederpop/luca'
|
|
658
|
-
import type {
|
|
683
|
+
import type { NodeContainer } from '@soederpop/luca'
|
|
659
684
|
import type { ServersInterface } from '@soederpop/luca'
|
|
660
685
|
|
|
661
686
|
declare module '@soederpop/luca' {
|
|
@@ -688,7 +713,7 @@ export class {{PascalName}} extends Server<{{PascalName}}State, {{PascalName}}Op
|
|
|
688
713
|
static override stateSchema = {{PascalName}}StateSchema
|
|
689
714
|
static override optionsSchema = {{PascalName}}OptionsSchema
|
|
690
715
|
static override eventsSchema = {{PascalName}}EventsSchema
|
|
691
|
-
static
|
|
716
|
+
static { Server.register(this, '{{camelName}}') }
|
|
692
717
|
|
|
693
718
|
static override attach(container: NodeContainer & ServersInterface) {
|
|
694
719
|
return container
|
|
@@ -717,12 +742,12 @@ export class {{PascalName}} extends Server<{{PascalName}}State, {{PascalName}}Op
|
|
|
717
742
|
}
|
|
718
743
|
}
|
|
719
744
|
|
|
720
|
-
export default
|
|
745
|
+
export default {{PascalName}}` }
|
|
721
746
|
],
|
|
722
747
|
full: `import { z } from 'zod'
|
|
723
|
-
import { Server
|
|
748
|
+
import { Server } from '@soederpop/luca'
|
|
724
749
|
import { ServerStateSchema, ServerOptionsSchema, ServerEventsSchema } from '@soederpop/luca'
|
|
725
|
-
import type {
|
|
750
|
+
import type { NodeContainer } from '@soederpop/luca'
|
|
726
751
|
import type { ServersInterface } from '@soederpop/luca'
|
|
727
752
|
|
|
728
753
|
declare module '@soederpop/luca' {
|
|
@@ -755,7 +780,7 @@ export class {{PascalName}} extends Server<{{PascalName}}State, {{PascalName}}Op
|
|
|
755
780
|
static override stateSchema = {{PascalName}}StateSchema
|
|
756
781
|
static override optionsSchema = {{PascalName}}OptionsSchema
|
|
757
782
|
static override eventsSchema = {{PascalName}}EventsSchema
|
|
758
|
-
static
|
|
783
|
+
static { Server.register(this, '{{camelName}}') }
|
|
759
784
|
|
|
760
785
|
static override attach(container: NodeContainer & ServersInterface) {
|
|
761
786
|
return container
|
|
@@ -784,7 +809,7 @@ export class {{PascalName}} extends Server<{{PascalName}}State, {{PascalName}}Op
|
|
|
784
809
|
}
|
|
785
810
|
}
|
|
786
811
|
|
|
787
|
-
export default
|
|
812
|
+
export default {{PascalName}}`,
|
|
788
813
|
tutorial: `# Building a Server
|
|
789
814
|
|
|
790
815
|
A server is a container-managed listener — something that accepts connections and handles requests. Servers manage their own lifecycle (configure, start, stop) and expose observable state.
|
|
@@ -798,9 +823,9 @@ When to build a server:
|
|
|
798
823
|
|
|
799
824
|
\`\`\`ts
|
|
800
825
|
import { z } from 'zod'
|
|
801
|
-
import { Server
|
|
826
|
+
import { Server } from '@soederpop/luca'
|
|
802
827
|
import { ServerStateSchema, ServerOptionsSchema, ServerEventsSchema } from '@soederpop/luca'
|
|
803
|
-
import type {
|
|
828
|
+
import type { NodeContainer } from '@soederpop/luca'
|
|
804
829
|
import type { ServersInterface } from '@soederpop/luca'
|
|
805
830
|
\`\`\`
|
|
806
831
|
|
|
@@ -827,6 +852,8 @@ export const {{PascalName}}EventsSchema = ServerEventsSchema.extend({
|
|
|
827
852
|
|
|
828
853
|
## Class
|
|
829
854
|
|
|
855
|
+
Running \`luca introspect\` captures JSDoc blocks and Zod schemas and includes them in the description whenever somebody calls \`container.servers.describe('{{camelName}}')\` or \`luca describe {{camelName}}\`.
|
|
856
|
+
|
|
830
857
|
\`\`\`ts
|
|
831
858
|
/**
|
|
832
859
|
* {{description}}
|
|
@@ -844,7 +871,7 @@ export class {{PascalName}} extends Server<{{PascalName}}State, {{PascalName}}Op
|
|
|
844
871
|
static override stateSchema = {{PascalName}}StateSchema
|
|
845
872
|
static override optionsSchema = {{PascalName}}OptionsSchema
|
|
846
873
|
static override eventsSchema = {{PascalName}}EventsSchema
|
|
847
|
-
static
|
|
874
|
+
static { Server.register(this, '{{camelName}}') }
|
|
848
875
|
|
|
849
876
|
static override attach(container: NodeContainer & ServersInterface) {
|
|
850
877
|
return container
|
|
@@ -890,17 +917,23 @@ declare module '@soederpop/luca' {
|
|
|
890
917
|
|
|
891
918
|
## Registration
|
|
892
919
|
|
|
920
|
+
Registration happens inside the class body using a static block. The default export is just the class itself.
|
|
921
|
+
|
|
893
922
|
\`\`\`ts
|
|
894
|
-
|
|
923
|
+
// Inside the class:
|
|
924
|
+
static { Server.register(this, '{{camelName}}') }
|
|
925
|
+
|
|
926
|
+
// At module level:
|
|
927
|
+
export default {{PascalName}}
|
|
895
928
|
\`\`\`
|
|
896
929
|
|
|
897
930
|
## Complete Example
|
|
898
931
|
|
|
899
932
|
\`\`\`ts
|
|
900
933
|
import { z } from 'zod'
|
|
901
|
-
import { Server
|
|
934
|
+
import { Server } from '@soederpop/luca'
|
|
902
935
|
import { ServerStateSchema, ServerOptionsSchema, ServerEventsSchema } from '@soederpop/luca'
|
|
903
|
-
import type {
|
|
936
|
+
import type { NodeContainer } from '@soederpop/luca'
|
|
904
937
|
import type { ServersInterface } from '@soederpop/luca'
|
|
905
938
|
|
|
906
939
|
declare module '@soederpop/luca' {
|
|
@@ -933,7 +966,7 @@ export class {{PascalName}} extends Server<{{PascalName}}State, {{PascalName}}Op
|
|
|
933
966
|
static override stateSchema = {{PascalName}}StateSchema
|
|
934
967
|
static override optionsSchema = {{PascalName}}OptionsSchema
|
|
935
968
|
static override eventsSchema = {{PascalName}}EventsSchema
|
|
936
|
-
static
|
|
969
|
+
static { Server.register(this, '{{camelName}}') }
|
|
937
970
|
|
|
938
971
|
static override attach(container: NodeContainer & ServersInterface) {
|
|
939
972
|
return container
|
|
@@ -962,100 +995,101 @@ export class {{PascalName}} extends Server<{{PascalName}}State, {{PascalName}}Op
|
|
|
962
995
|
}
|
|
963
996
|
}
|
|
964
997
|
|
|
965
|
-
export default
|
|
998
|
+
export default {{PascalName}}
|
|
966
999
|
\`\`\`
|
|
967
1000
|
|
|
968
1001
|
## Conventions
|
|
969
1002
|
|
|
970
1003
|
- **Lifecycle**: Implement \`configure()\`, \`start()\`, and \`stop()\`. Check guards (\`isConfigured\`, \`isListening\`, \`isStopped\`) at the top of each.
|
|
1004
|
+
- **Use \`afterInitialize()\`**: For any setup logic instead of overriding the constructor. Lifecycle methods (\`configure\`, \`start\`, \`stop\`) handle the server's runtime phases.
|
|
971
1005
|
- **State tracking**: Set \`configured\`, \`listening\`, \`stopped\`, and \`port\` on the state. This powers the introspection system.
|
|
972
1006
|
- **attach() is static**: It runs when the container first loads the server class. Use it for container-level setup if needed.
|
|
973
1007
|
- **Port from options**: Accept port via options schema and respect it in \`start()\`. Allow override via start options.
|
|
974
|
-
- **JSDoc everything**: Every public method needs \`@param\`, \`@returns\`, \`@example\`.
|
|
1008
|
+
- **JSDoc everything**: Every public method needs \`@param\`, \`@returns\`, \`@example\`. Run \`luca introspect\` after changes to update generated docs.
|
|
975
1009
|
`,
|
|
976
1010
|
},
|
|
977
1011
|
command: {
|
|
978
1012
|
sections: [
|
|
979
1013
|
{ heading: "Imports", code: `import { z } from 'zod'
|
|
980
|
-
import { commands, CommandOptionsSchema } from '@soederpop/luca'
|
|
981
1014
|
import type { ContainerContext } from '@soederpop/luca'` },
|
|
982
|
-
{ heading: "
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
//
|
|
1015
|
+
{ heading: "Positional Arguments", code: `// luca {{kebabName}} ./src => options.target === './src'
|
|
1016
|
+
export const positionals = ['target']` },
|
|
1017
|
+
{ heading: "Args Schema", code: `export const argsSchema = z.object({
|
|
1018
|
+
// Positional: first arg after command name (via positionals array above)
|
|
1019
|
+
// target: z.string().optional().describe('The target to operate on'),
|
|
1020
|
+
|
|
1021
|
+
// Flags: passed as --flag on the CLI
|
|
1022
|
+
// verbose: z.boolean().default(false).describe('Enable verbose output'),
|
|
1023
|
+
// output: z.string().optional().describe('Output file path'),
|
|
986
1024
|
})` },
|
|
1025
|
+
{ heading: "Description", code: `export const description = '{{description}}'` },
|
|
987
1026
|
{ heading: "Handler", code: `export default async function {{camelName}}(options: z.infer<typeof argsSchema>, context: ContainerContext) {
|
|
988
|
-
const container = context
|
|
1027
|
+
const { container } = context
|
|
989
1028
|
const fs = container.feature('fs')
|
|
990
|
-
const args = container.argv._ as string[]
|
|
991
1029
|
|
|
992
|
-
//
|
|
993
|
-
// options
|
|
1030
|
+
// options.target is set from the first positional arg (via positionals export)
|
|
1031
|
+
// options.verbose, options.output, etc. come from --flags
|
|
994
1032
|
|
|
995
1033
|
// Your implementation here
|
|
996
|
-
}` },
|
|
997
|
-
{ heading: "Registration", code: `commands.registerHandler('{{camelName}}', {
|
|
998
|
-
description: '{{description}}',
|
|
999
|
-
argsSchema,
|
|
1000
|
-
handler: {{camelName}},
|
|
1001
|
-
})` },
|
|
1002
|
-
{ heading: "Module Augmentation", code: `declare module '@soederpop/luca' {
|
|
1003
|
-
interface AvailableCommands {
|
|
1004
|
-
{{camelName}}: ReturnType<typeof commands.registerHandler>
|
|
1005
|
-
}
|
|
1006
1034
|
}` },
|
|
1007
1035
|
{ heading: "Complete Example", code: `import { z } from 'zod'
|
|
1008
|
-
import { commands, CommandOptionsSchema } from '@soederpop/luca'
|
|
1009
1036
|
import type { ContainerContext } from '@soederpop/luca'
|
|
1010
1037
|
|
|
1011
|
-
|
|
1012
|
-
interface AvailableCommands {
|
|
1013
|
-
{{camelName}}: ReturnType<typeof commands.registerHandler>
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1038
|
+
export const description = '{{description}}'
|
|
1016
1039
|
|
|
1017
|
-
|
|
1040
|
+
// Map positional args to named options: luca {{kebabName}} myTarget => options.target === 'myTarget'
|
|
1041
|
+
export const positionals = ['target']
|
|
1042
|
+
|
|
1043
|
+
export const argsSchema = z.object({
|
|
1044
|
+
target: z.string().optional().describe('The target to operate on'),
|
|
1045
|
+
})
|
|
1018
1046
|
|
|
1019
1047
|
export default async function {{camelName}}(options: z.infer<typeof argsSchema>, context: ContainerContext) {
|
|
1020
|
-
const container = context
|
|
1048
|
+
const { container } = context
|
|
1021
1049
|
const fs = container.feature('fs')
|
|
1022
1050
|
|
|
1023
|
-
console.log('{{
|
|
1024
|
-
}
|
|
1051
|
+
console.log('{{kebabName}} running...', options.target)
|
|
1052
|
+
}` },
|
|
1053
|
+
{ heading: "Container Properties", code: `export default async function {{camelName}}(options: z.infer<typeof argsSchema>, context: ContainerContext) {
|
|
1054
|
+
const { container } = context
|
|
1055
|
+
|
|
1056
|
+
// Current working directory
|
|
1057
|
+
container.cwd // '/path/to/project'
|
|
1058
|
+
|
|
1059
|
+
// Path utilities (scoped to cwd)
|
|
1060
|
+
container.paths.resolve('src') // '/path/to/project/src'
|
|
1061
|
+
container.paths.join('a', 'b') // '/path/to/project/a/b'
|
|
1062
|
+
container.paths.relative('src') // 'src'
|
|
1063
|
+
|
|
1064
|
+
// Package manifest (parsed package.json)
|
|
1065
|
+
container.manifest.name // 'my-project'
|
|
1066
|
+
container.manifest.version // '1.0.0'
|
|
1025
1067
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
handler: {{camelName}},
|
|
1030
|
-
})` }
|
|
1068
|
+
// Raw CLI arguments (from minimist) — prefer positionals export for positional args
|
|
1069
|
+
container.argv // { _: ['{{kebabName}}', ...], verbose: true, ... }
|
|
1070
|
+
}` }
|
|
1031
1071
|
],
|
|
1032
1072
|
full: `import { z } from 'zod'
|
|
1033
|
-
import { commands, CommandOptionsSchema } from '@soederpop/luca'
|
|
1034
1073
|
import type { ContainerContext } from '@soederpop/luca'
|
|
1035
1074
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
}
|
|
1075
|
+
export const description = '{{description}}'
|
|
1076
|
+
|
|
1077
|
+
// Map positional args to named options: luca {{kebabName}} myTarget => options.target === 'myTarget'
|
|
1078
|
+
export const positionals = ['target']
|
|
1041
1079
|
|
|
1042
|
-
export const argsSchema =
|
|
1080
|
+
export const argsSchema = z.object({
|
|
1081
|
+
target: z.string().optional().describe('The target to operate on'),
|
|
1082
|
+
})
|
|
1043
1083
|
|
|
1044
1084
|
export default async function {{camelName}}(options: z.infer<typeof argsSchema>, context: ContainerContext) {
|
|
1045
|
-
const container = context
|
|
1085
|
+
const { container } = context
|
|
1046
1086
|
const fs = container.feature('fs')
|
|
1047
1087
|
|
|
1048
|
-
console.log('{{
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
commands.registerHandler('{{camelName}}', {
|
|
1052
|
-
description: '{{description}}',
|
|
1053
|
-
argsSchema,
|
|
1054
|
-
handler: {{camelName}},
|
|
1055
|
-
})`,
|
|
1088
|
+
console.log('{{kebabName}} running...', options.target)
|
|
1089
|
+
}`,
|
|
1056
1090
|
tutorial: `# Building a Command
|
|
1057
1091
|
|
|
1058
|
-
A command extends the \`luca\` CLI. Commands live in a project's \`commands/\` folder and are automatically discovered. They
|
|
1092
|
+
A command extends the \`luca\` CLI. Commands live in a project's \`commands/\` folder and are automatically discovered. They are Helper subclasses under the hood — the framework grafts your module exports into a Command class at runtime.
|
|
1059
1093
|
|
|
1060
1094
|
When to build a command:
|
|
1061
1095
|
- You need a CLI task for a project (build scripts, generators, automation)
|
|
@@ -1066,60 +1100,54 @@ When to build a command:
|
|
|
1066
1100
|
|
|
1067
1101
|
\`\`\`ts
|
|
1068
1102
|
import { z } from 'zod'
|
|
1069
|
-
import { commands, CommandOptionsSchema } from '@soederpop/luca'
|
|
1070
1103
|
import type { ContainerContext } from '@soederpop/luca'
|
|
1071
1104
|
\`\`\`
|
|
1072
1105
|
|
|
1073
|
-
##
|
|
1106
|
+
## Positional Arguments
|
|
1074
1107
|
|
|
1075
|
-
|
|
1108
|
+
Export a \`positionals\` array to map CLI positional args into named options fields. The first positional (\`_[0]\`) is always the command name — \`positionals\` maps \`_[1]\`, \`_[2]\`, etc.
|
|
1076
1109
|
|
|
1077
1110
|
\`\`\`ts
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
// Example: verbose: z.boolean().default(false).describe('Enable verbose output'),
|
|
1081
|
-
// Example: output: z.string().optional().describe('Output file path'),
|
|
1082
|
-
})
|
|
1111
|
+
// luca {{kebabName}} ./src => options.target === './src'
|
|
1112
|
+
export const positionals = ['target']
|
|
1083
1113
|
\`\`\`
|
|
1084
1114
|
|
|
1085
|
-
##
|
|
1115
|
+
## Args Schema
|
|
1086
1116
|
|
|
1087
|
-
|
|
1117
|
+
Define your command's arguments and flags with Zod. Each field becomes a \`--flag\` on the CLI. Fields named in \`positionals\` also accept positional args.
|
|
1088
1118
|
|
|
1089
1119
|
\`\`\`ts
|
|
1090
|
-
export
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
const args = container.argv._ as string[]
|
|
1120
|
+
export const argsSchema = z.object({
|
|
1121
|
+
// Positional: first arg after command name (via positionals array above)
|
|
1122
|
+
// target: z.string().optional().describe('The target to operate on'),
|
|
1094
1123
|
|
|
1095
|
-
//
|
|
1096
|
-
//
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
}
|
|
1124
|
+
// Flags: passed as --flag on the CLI
|
|
1125
|
+
// verbose: z.boolean().default(false).describe('Enable verbose output'),
|
|
1126
|
+
// output: z.string().optional().describe('Output file path'),
|
|
1127
|
+
})
|
|
1100
1128
|
\`\`\`
|
|
1101
1129
|
|
|
1102
|
-
##
|
|
1130
|
+
## Description
|
|
1103
1131
|
|
|
1104
|
-
|
|
1132
|
+
Export a description string for \`luca --help\` display:
|
|
1105
1133
|
|
|
1106
1134
|
\`\`\`ts
|
|
1107
|
-
|
|
1108
|
-
description: '{{description}}',
|
|
1109
|
-
argsSchema,
|
|
1110
|
-
handler: {{camelName}},
|
|
1111
|
-
})
|
|
1135
|
+
export const description = '{{description}}'
|
|
1112
1136
|
\`\`\`
|
|
1113
1137
|
|
|
1114
|
-
##
|
|
1138
|
+
## Handler
|
|
1115
1139
|
|
|
1116
|
-
|
|
1140
|
+
Export a default async function. It receives parsed options and the container context. Use the container for all I/O. Positional args declared in the \`positionals\` export are available as named fields on \`options\`.
|
|
1117
1141
|
|
|
1118
1142
|
\`\`\`ts
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1143
|
+
export default async function {{camelName}}(options: z.infer<typeof argsSchema>, context: ContainerContext) {
|
|
1144
|
+
const { container } = context
|
|
1145
|
+
const fs = container.feature('fs')
|
|
1146
|
+
|
|
1147
|
+
// options.target is set from the first positional arg (via positionals export)
|
|
1148
|
+
// options.verbose, options.output, etc. come from --flags
|
|
1149
|
+
|
|
1150
|
+
// Your implementation here
|
|
1123
1151
|
}
|
|
1124
1152
|
\`\`\`
|
|
1125
1153
|
|
|
@@ -1127,58 +1155,78 @@ declare module '@soederpop/luca' {
|
|
|
1127
1155
|
|
|
1128
1156
|
\`\`\`ts
|
|
1129
1157
|
import { z } from 'zod'
|
|
1130
|
-
import { commands, CommandOptionsSchema } from '@soederpop/luca'
|
|
1131
1158
|
import type { ContainerContext } from '@soederpop/luca'
|
|
1132
1159
|
|
|
1133
|
-
|
|
1134
|
-
interface AvailableCommands {
|
|
1135
|
-
{{camelName}}: ReturnType<typeof commands.registerHandler>
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1160
|
+
export const description = '{{description}}'
|
|
1138
1161
|
|
|
1139
|
-
|
|
1162
|
+
// Map positional args to named options: luca {{kebabName}} myTarget => options.target === 'myTarget'
|
|
1163
|
+
export const positionals = ['target']
|
|
1164
|
+
|
|
1165
|
+
export const argsSchema = z.object({
|
|
1166
|
+
target: z.string().optional().describe('The target to operate on'),
|
|
1167
|
+
})
|
|
1140
1168
|
|
|
1141
1169
|
export default async function {{camelName}}(options: z.infer<typeof argsSchema>, context: ContainerContext) {
|
|
1142
|
-
const container = context
|
|
1170
|
+
const { container } = context
|
|
1143
1171
|
const fs = container.feature('fs')
|
|
1144
1172
|
|
|
1145
|
-
console.log('{{
|
|
1173
|
+
console.log('{{kebabName}} running...', options.target)
|
|
1146
1174
|
}
|
|
1175
|
+
\`\`\`
|
|
1147
1176
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1177
|
+
## Container Properties
|
|
1178
|
+
|
|
1179
|
+
The \`context.container\` object provides useful properties beyond features:
|
|
1180
|
+
|
|
1181
|
+
\`\`\`ts
|
|
1182
|
+
export default async function {{camelName}}(options: z.infer<typeof argsSchema>, context: ContainerContext) {
|
|
1183
|
+
const { container } = context
|
|
1184
|
+
|
|
1185
|
+
// Current working directory
|
|
1186
|
+
container.cwd // '/path/to/project'
|
|
1187
|
+
|
|
1188
|
+
// Path utilities (scoped to cwd)
|
|
1189
|
+
container.paths.resolve('src') // '/path/to/project/src'
|
|
1190
|
+
container.paths.join('a', 'b') // '/path/to/project/a/b'
|
|
1191
|
+
container.paths.relative('src') // 'src'
|
|
1192
|
+
|
|
1193
|
+
// Package manifest (parsed package.json)
|
|
1194
|
+
container.manifest.name // 'my-project'
|
|
1195
|
+
container.manifest.version // '1.0.0'
|
|
1196
|
+
|
|
1197
|
+
// Raw CLI arguments (from minimist) — prefer positionals export for positional args
|
|
1198
|
+
container.argv // { _: ['{{kebabName}}', ...], verbose: true, ... }
|
|
1199
|
+
}
|
|
1153
1200
|
\`\`\`
|
|
1154
1201
|
|
|
1155
1202
|
## Conventions
|
|
1156
1203
|
|
|
1157
|
-
- **File location**: \`commands/{{
|
|
1158
|
-
- **Naming**:
|
|
1204
|
+
- **File location**: \`commands/{{kebabName}}.ts\` in the project root. The \`luca\` CLI discovers these automatically.
|
|
1205
|
+
- **Naming**: kebab-case for filename. \`luca {{kebabName}}\` maps to \`commands/{{kebabName}}.ts\`.
|
|
1159
1206
|
- **Use the container**: Never import \`fs\`, \`path\`, \`child_process\` directly. Use \`container.feature('fs')\`, \`container.paths\`, \`container.feature('proc')\`.
|
|
1160
|
-
- **Positional args**:
|
|
1207
|
+
- **Positional args**: Export \`positionals = ['name1', 'name2']\` to map CLI positional args into named options fields. For raw access, use \`container.argv._\` where \`_[0]\` is the command name.
|
|
1161
1208
|
- **Exit codes**: Return nothing for success. Throw for errors — the CLI catches and reports them.
|
|
1209
|
+
- **Help text**: Use \`.describe()\` on every schema field — it powers \`luca {{kebabName}} --help\`.
|
|
1162
1210
|
`,
|
|
1163
1211
|
},
|
|
1164
1212
|
endpoint: {
|
|
1165
1213
|
sections: [
|
|
1166
|
-
{ heading: "Imports", code: `import { z } from 'zod'
|
|
1167
|
-
import type { EndpointContext } from '@soederpop/luca'` },
|
|
1168
1214
|
{ heading: "Required Exports", code: `export const path = '/api/{{camelName}}'
|
|
1169
1215
|
export const description = '{{description}}'
|
|
1170
1216
|
export const tags = ['{{camelName}}']` },
|
|
1171
|
-
{ heading: "Handler Functions", code: `export async function get(params: any, ctx:
|
|
1217
|
+
{ heading: "Handler Functions", code: `export async function get(params: any, ctx: any) {
|
|
1172
1218
|
const fs = ctx.container.feature('fs')
|
|
1173
1219
|
// Your logic here
|
|
1174
1220
|
return { message: 'ok' }
|
|
1175
1221
|
}
|
|
1176
1222
|
|
|
1177
|
-
export async function post(params:
|
|
1223
|
+
export async function post(params: any, ctx: any) {
|
|
1178
1224
|
// Create something
|
|
1179
1225
|
return { created: true }
|
|
1180
1226
|
}` },
|
|
1181
|
-
{ heading: "Validation Schemas", code: `
|
|
1227
|
+
{ heading: "Validation Schemas", code: `import { z } from 'zod'
|
|
1228
|
+
|
|
1229
|
+
export const getSchema = z.object({
|
|
1182
1230
|
q: z.string().optional().describe('Search query'),
|
|
1183
1231
|
limit: z.number().default(20).describe('Max results'),
|
|
1184
1232
|
})
|
|
@@ -1192,82 +1240,66 @@ export const rateLimit = { maxRequests: 100, windowSeconds: 60 }
|
|
|
1192
1240
|
|
|
1193
1241
|
// Per-method rate limit
|
|
1194
1242
|
export const postRateLimit = { maxRequests: 10, windowSeconds: 1 }` },
|
|
1195
|
-
{ heading: "Delete Handler", code:
|
|
1243
|
+
{ heading: "Delete Handler", code: `// Use a local name, then re-export as \`delete\`
|
|
1244
|
+
const del = async (params: any, ctx: any) => {
|
|
1196
1245
|
return { deleted: true }
|
|
1197
1246
|
}
|
|
1198
1247
|
export { del as delete }` },
|
|
1199
|
-
{ heading: "Complete Example", code: `
|
|
1200
|
-
import type { EndpointContext } from '@soederpop/luca'
|
|
1201
|
-
|
|
1202
|
-
export const path = '/api/{{camelName}}'
|
|
1248
|
+
{ heading: "Complete Example", code: `export const path = '/api/{{camelName}}'
|
|
1203
1249
|
export const description = '{{description}}'
|
|
1204
1250
|
export const tags = ['{{camelName}}']
|
|
1205
1251
|
|
|
1206
|
-
export
|
|
1207
|
-
q: z.string().optional().describe('Search query'),
|
|
1208
|
-
})
|
|
1209
|
-
|
|
1210
|
-
export async function get(params: z.infer<typeof getSchema>, ctx: EndpointContext) {
|
|
1252
|
+
export async function get(params: any, ctx: any) {
|
|
1211
1253
|
return { items: [], total: 0 }
|
|
1212
1254
|
}
|
|
1213
1255
|
|
|
1214
|
-
export
|
|
1215
|
-
name: z.string().min(1).describe('Item name'),
|
|
1216
|
-
})
|
|
1217
|
-
|
|
1218
|
-
export async function post(params: z.infer<typeof postSchema>, ctx: EndpointContext) {
|
|
1256
|
+
export async function post(params: any, ctx: any) {
|
|
1219
1257
|
return { item: { id: '1', ...params }, message: 'Created' }
|
|
1220
|
-
}
|
|
1221
|
-
{ heading: "Dynamic Route Example", code: `// endpoints/{{camelName}}/[id].ts
|
|
1222
|
-
import { z } from 'zod'
|
|
1223
|
-
import type { EndpointContext } from '@soederpop/luca'
|
|
1258
|
+
}
|
|
1224
1259
|
|
|
1260
|
+
const del = async (params: any, ctx: any) => {
|
|
1261
|
+
const { id } = ctx.params
|
|
1262
|
+
return { message: \`Deleted \${id}\` }
|
|
1263
|
+
}
|
|
1264
|
+
export { del as delete }` },
|
|
1265
|
+
{ heading: "Dynamic Route Example", code: `// endpoints/{{camelName}}/[id].ts
|
|
1225
1266
|
export const path = '/api/{{camelName}}/:id'
|
|
1226
1267
|
export const description = 'Get, update, or delete a specific item'
|
|
1227
1268
|
export const tags = ['{{camelName}}']
|
|
1228
1269
|
|
|
1229
|
-
export async function get(params: any, ctx:
|
|
1270
|
+
export async function get(params: any, ctx: any) {
|
|
1230
1271
|
const { id } = ctx.params
|
|
1231
1272
|
return { item: { id } }
|
|
1232
1273
|
}
|
|
1233
1274
|
|
|
1234
|
-
export
|
|
1235
|
-
name: z.string().min(1).optional().describe('Updated name'),
|
|
1236
|
-
})
|
|
1237
|
-
|
|
1238
|
-
export async function put(params: z.infer<typeof putSchema>, ctx: EndpointContext) {
|
|
1275
|
+
export async function put(params: any, ctx: any) {
|
|
1239
1276
|
const { id } = ctx.params
|
|
1240
1277
|
return { item: { id, ...params }, message: 'Updated' }
|
|
1241
1278
|
}
|
|
1242
1279
|
|
|
1243
|
-
const del = async (params: any, ctx:
|
|
1280
|
+
const del = async (params: any, ctx: any) => {
|
|
1244
1281
|
const { id } = ctx.params
|
|
1245
1282
|
return { message: \`Deleted \${id}\` }
|
|
1246
1283
|
}
|
|
1247
1284
|
export { del as delete }` }
|
|
1248
1285
|
],
|
|
1249
|
-
full: `
|
|
1250
|
-
import type { EndpointContext } from '@soederpop/luca'
|
|
1251
|
-
|
|
1252
|
-
export const path = '/api/{{camelName}}'
|
|
1286
|
+
full: `export const path = '/api/{{camelName}}'
|
|
1253
1287
|
export const description = '{{description}}'
|
|
1254
1288
|
export const tags = ['{{camelName}}']
|
|
1255
1289
|
|
|
1256
|
-
export
|
|
1257
|
-
q: z.string().optional().describe('Search query'),
|
|
1258
|
-
})
|
|
1259
|
-
|
|
1260
|
-
export async function get(params: z.infer<typeof getSchema>, ctx: EndpointContext) {
|
|
1290
|
+
export async function get(params: any, ctx: any) {
|
|
1261
1291
|
return { items: [], total: 0 }
|
|
1262
1292
|
}
|
|
1263
1293
|
|
|
1264
|
-
export
|
|
1265
|
-
name: z.string().min(1).describe('Item name'),
|
|
1266
|
-
})
|
|
1267
|
-
|
|
1268
|
-
export async function post(params: z.infer<typeof postSchema>, ctx: EndpointContext) {
|
|
1294
|
+
export async function post(params: any, ctx: any) {
|
|
1269
1295
|
return { item: { id: '1', ...params }, message: 'Created' }
|
|
1270
|
-
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
const del = async (params: any, ctx: any) => {
|
|
1299
|
+
const { id } = ctx.params
|
|
1300
|
+
return { message: \`Deleted \${id}\` }
|
|
1301
|
+
}
|
|
1302
|
+
export { del as delete }`,
|
|
1271
1303
|
tutorial: `# Building an Endpoint
|
|
1272
1304
|
|
|
1273
1305
|
An endpoint is a route handler that \`luca serve\` auto-discovers and mounts on an Express server. Endpoints live in \`endpoints/\` and follow a file-based routing convention — each file becomes an API route with automatic validation, OpenAPI spec generation, and rate limiting.
|
|
@@ -1288,12 +1320,14 @@ Run \`luca serve\` and they're automatically discovered and mounted.
|
|
|
1288
1320
|
|
|
1289
1321
|
## Imports
|
|
1290
1322
|
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
import
|
|
1294
|
-
\`\`\`
|
|
1323
|
+
Endpoints are lightweight — just exports and handler functions. No imports are required.
|
|
1324
|
+
|
|
1325
|
+
If your project has \`@soederpop/luca\` as an npm dependency, you can import \`z\` from \`zod\` and \`EndpointContext\` from \`@soederpop/luca\` for type safety. Otherwise, use \`any\` types — the framework handles validation and context injection for you.
|
|
1295
1326
|
|
|
1296
|
-
|
|
1327
|
+
Access framework capabilities through the \`ctx\` parameter:
|
|
1328
|
+
- \`ctx.container.feature('fs')\` for file operations
|
|
1329
|
+
- \`ctx.container.feature('yaml')\` for YAML parsing
|
|
1330
|
+
- \`ctx.container.feature('sqlite')\` for database access
|
|
1297
1331
|
|
|
1298
1332
|
## Required Exports
|
|
1299
1333
|
|
|
@@ -1307,16 +1341,16 @@ export const tags = ['{{camelName}}']
|
|
|
1307
1341
|
|
|
1308
1342
|
## Handler Functions
|
|
1309
1343
|
|
|
1310
|
-
Export named functions for each HTTP method you support. Each receives validated parameters and
|
|
1344
|
+
Export named functions for each HTTP method you support. Each receives validated parameters and a context object:
|
|
1311
1345
|
|
|
1312
1346
|
\`\`\`ts
|
|
1313
|
-
export async function get(params: any, ctx:
|
|
1347
|
+
export async function get(params: any, ctx: any) {
|
|
1314
1348
|
const fs = ctx.container.feature('fs')
|
|
1315
1349
|
// Your logic here
|
|
1316
1350
|
return { message: 'ok' }
|
|
1317
1351
|
}
|
|
1318
1352
|
|
|
1319
|
-
export async function post(params:
|
|
1353
|
+
export async function post(params: any, ctx: any) {
|
|
1320
1354
|
// Create something
|
|
1321
1355
|
return { created: true }
|
|
1322
1356
|
}
|
|
@@ -1334,9 +1368,11 @@ Return any object — it's automatically JSON-serialized as the response.
|
|
|
1334
1368
|
|
|
1335
1369
|
## Validation Schemas
|
|
1336
1370
|
|
|
1337
|
-
|
|
1371
|
+
If \`zod\` is available (via \`@soederpop/luca\` dependency or \`node_modules\`), export Zod schemas to validate parameters for each method. Name them \`{method}Schema\`:
|
|
1338
1372
|
|
|
1339
1373
|
\`\`\`ts
|
|
1374
|
+
import { z } from 'zod'
|
|
1375
|
+
|
|
1340
1376
|
export const getSchema = z.object({
|
|
1341
1377
|
q: z.string().optional().describe('Search query'),
|
|
1342
1378
|
limit: z.number().default(20).describe('Max results'),
|
|
@@ -1348,7 +1384,7 @@ export const postSchema = z.object({
|
|
|
1348
1384
|
})
|
|
1349
1385
|
\`\`\`
|
|
1350
1386
|
|
|
1351
|
-
Invalid requests automatically return 400 with Zod error details. Schemas also feed the auto-generated OpenAPI spec.
|
|
1387
|
+
Invalid requests automatically return 400 with Zod error details. Schemas also feed the auto-generated OpenAPI spec. If zod is not available, skip schema exports — the endpoint still works, you just lose automatic validation.
|
|
1352
1388
|
|
|
1353
1389
|
## Rate Limiting
|
|
1354
1390
|
|
|
@@ -1364,42 +1400,40 @@ export const postRateLimit = { maxRequests: 10, windowSeconds: 1 }
|
|
|
1364
1400
|
|
|
1365
1401
|
## Delete Handler
|
|
1366
1402
|
|
|
1367
|
-
\`delete\` is a reserved word in JS. Use
|
|
1403
|
+
\`delete\` is a reserved word in JS, so you can't use it as a function name directly. Use a named export alias:
|
|
1368
1404
|
|
|
1369
1405
|
\`\`\`ts
|
|
1370
|
-
|
|
1406
|
+
// Use a local name, then re-export as \`delete\`
|
|
1407
|
+
const del = async (params: any, ctx: any) => {
|
|
1371
1408
|
return { deleted: true }
|
|
1372
1409
|
}
|
|
1373
1410
|
export { del as delete }
|
|
1374
1411
|
\`\`\`
|
|
1375
1412
|
|
|
1413
|
+
You can also export \`deleteSchema\` and \`deleteRateLimit\` for validation and rate limiting on DELETE.
|
|
1414
|
+
|
|
1376
1415
|
## Complete Example
|
|
1377
1416
|
|
|
1378
|
-
A CRUD endpoint for a resource:
|
|
1417
|
+
A CRUD endpoint for a resource (no external imports needed):
|
|
1379
1418
|
|
|
1380
1419
|
\`\`\`ts
|
|
1381
|
-
import { z } from 'zod'
|
|
1382
|
-
import type { EndpointContext } from '@soederpop/luca'
|
|
1383
|
-
|
|
1384
1420
|
export const path = '/api/{{camelName}}'
|
|
1385
1421
|
export const description = '{{description}}'
|
|
1386
1422
|
export const tags = ['{{camelName}}']
|
|
1387
1423
|
|
|
1388
|
-
export
|
|
1389
|
-
q: z.string().optional().describe('Search query'),
|
|
1390
|
-
})
|
|
1391
|
-
|
|
1392
|
-
export async function get(params: z.infer<typeof getSchema>, ctx: EndpointContext) {
|
|
1424
|
+
export async function get(params: any, ctx: any) {
|
|
1393
1425
|
return { items: [], total: 0 }
|
|
1394
1426
|
}
|
|
1395
1427
|
|
|
1396
|
-
export
|
|
1397
|
-
name: z.string().min(1).describe('Item name'),
|
|
1398
|
-
})
|
|
1399
|
-
|
|
1400
|
-
export async function post(params: z.infer<typeof postSchema>, ctx: EndpointContext) {
|
|
1428
|
+
export async function post(params: any, ctx: any) {
|
|
1401
1429
|
return { item: { id: '1', ...params }, message: 'Created' }
|
|
1402
1430
|
}
|
|
1431
|
+
|
|
1432
|
+
const del = async (params: any, ctx: any) => {
|
|
1433
|
+
const { id } = ctx.params
|
|
1434
|
+
return { message: \`Deleted \${id}\` }
|
|
1435
|
+
}
|
|
1436
|
+
export { del as delete }
|
|
1403
1437
|
\`\`\`
|
|
1404
1438
|
|
|
1405
1439
|
## Dynamic Route Example
|
|
@@ -1408,28 +1442,21 @@ For routes with URL parameters, create a nested file:
|
|
|
1408
1442
|
|
|
1409
1443
|
\`\`\`ts
|
|
1410
1444
|
// endpoints/{{camelName}}/[id].ts
|
|
1411
|
-
import { z } from 'zod'
|
|
1412
|
-
import type { EndpointContext } from '@soederpop/luca'
|
|
1413
|
-
|
|
1414
1445
|
export const path = '/api/{{camelName}}/:id'
|
|
1415
1446
|
export const description = 'Get, update, or delete a specific item'
|
|
1416
1447
|
export const tags = ['{{camelName}}']
|
|
1417
1448
|
|
|
1418
|
-
export async function get(params: any, ctx:
|
|
1449
|
+
export async function get(params: any, ctx: any) {
|
|
1419
1450
|
const { id } = ctx.params
|
|
1420
1451
|
return { item: { id } }
|
|
1421
1452
|
}
|
|
1422
1453
|
|
|
1423
|
-
export
|
|
1424
|
-
name: z.string().min(1).optional().describe('Updated name'),
|
|
1425
|
-
})
|
|
1426
|
-
|
|
1427
|
-
export async function put(params: z.infer<typeof putSchema>, ctx: EndpointContext) {
|
|
1454
|
+
export async function put(params: any, ctx: any) {
|
|
1428
1455
|
const { id } = ctx.params
|
|
1429
1456
|
return { item: { id, ...params }, message: 'Updated' }
|
|
1430
1457
|
}
|
|
1431
1458
|
|
|
1432
|
-
const del = async (params: any, ctx:
|
|
1459
|
+
const del = async (params: any, ctx: any) => {
|
|
1433
1460
|
const { id } = ctx.params
|
|
1434
1461
|
return { message: \`Deleted \${id}\` }
|
|
1435
1462
|
}
|
|
@@ -1444,10 +1471,185 @@ export { del as delete }
|
|
|
1444
1471
|
- **Use the container**: Access features via \`ctx.container.feature('fs')\`, not Node.js imports.
|
|
1445
1472
|
- **Return objects**: Handler return values are JSON-serialized. Use \`ctx.response\` only for streaming or custom status codes.
|
|
1446
1473
|
- **OpenAPI for free**: Your \`path\`, \`description\`, \`tags\`, and schemas automatically generate an OpenAPI spec at \`/openapi.json\`.
|
|
1474
|
+
`,
|
|
1475
|
+
},
|
|
1476
|
+
selector: {
|
|
1477
|
+
sections: [
|
|
1478
|
+
{ heading: "Imports", code: `import { z } from 'zod'
|
|
1479
|
+
import type { ContainerContext } from '@soederpop/luca'` },
|
|
1480
|
+
{ heading: "Args Schema", code: `export const argsSchema = z.object({
|
|
1481
|
+
// Add your input arguments here.
|
|
1482
|
+
// Example: field: z.string().optional().describe('Specific field to return'),
|
|
1483
|
+
})` },
|
|
1484
|
+
{ heading: "Description", code: `export const description = '{{description}}'` },
|
|
1485
|
+
{ heading: "Caching", code: `export function cacheKey(args: z.infer<typeof argsSchema>, context: ContainerContext) {
|
|
1486
|
+
return context.container.git.currentCommitSha
|
|
1487
|
+
}` },
|
|
1488
|
+
{ heading: "Caching", code: `export const cacheable = false` },
|
|
1489
|
+
{ heading: "Handler", code: `export async function run(args: z.infer<typeof argsSchema>, context: ContainerContext) {
|
|
1490
|
+
const { container } = context
|
|
1491
|
+
// Query and return your data
|
|
1492
|
+
return { /* your data */ }
|
|
1493
|
+
}` },
|
|
1494
|
+
{ heading: "Complete Example", code: `import { z } from 'zod'
|
|
1495
|
+
import type { ContainerContext } from '@soederpop/luca'
|
|
1496
|
+
|
|
1497
|
+
export const description = '{{description}}'
|
|
1498
|
+
|
|
1499
|
+
export const argsSchema = z.object({})
|
|
1500
|
+
|
|
1501
|
+
export async function run(args: z.infer<typeof argsSchema>, context: ContainerContext) {
|
|
1502
|
+
const { container } = context
|
|
1503
|
+
|
|
1504
|
+
// Return your data here
|
|
1505
|
+
return {}
|
|
1506
|
+
}` }
|
|
1507
|
+
],
|
|
1508
|
+
full: `import { z } from 'zod'
|
|
1509
|
+
import type { ContainerContext } from '@soederpop/luca'
|
|
1510
|
+
|
|
1511
|
+
export const description = '{{description}}'
|
|
1512
|
+
|
|
1513
|
+
export const argsSchema = z.object({})
|
|
1514
|
+
|
|
1515
|
+
export async function run(args: z.infer<typeof argsSchema>, context: ContainerContext) {
|
|
1516
|
+
const { container } = context
|
|
1517
|
+
|
|
1518
|
+
// Return your data here
|
|
1519
|
+
return {}
|
|
1520
|
+
}`,
|
|
1521
|
+
tutorial: `# Building a Selector
|
|
1522
|
+
|
|
1523
|
+
A selector returns data. Where commands perform actions, selectors query and return structured results with built-in caching. Selectors live in a project's \`selectors/\` folder and are automatically discovered.
|
|
1524
|
+
|
|
1525
|
+
When to build a selector:
|
|
1526
|
+
- You need to query project data (package info, file listings, config values)
|
|
1527
|
+
- The result benefits from caching (keyed by git SHA or custom key)
|
|
1528
|
+
- You want the data available via \`container.select('name')\` or \`luca select name\`
|
|
1529
|
+
|
|
1530
|
+
## Imports
|
|
1531
|
+
|
|
1532
|
+
\`\`\`ts
|
|
1533
|
+
import { z } from 'zod'
|
|
1534
|
+
import type { ContainerContext } from '@soederpop/luca'
|
|
1535
|
+
\`\`\`
|
|
1536
|
+
|
|
1537
|
+
## Args Schema
|
|
1538
|
+
|
|
1539
|
+
Define the selector's input arguments with Zod.
|
|
1540
|
+
|
|
1541
|
+
\`\`\`ts
|
|
1542
|
+
export const argsSchema = z.object({
|
|
1543
|
+
// Add your input arguments here.
|
|
1544
|
+
// Example: field: z.string().optional().describe('Specific field to return'),
|
|
1545
|
+
})
|
|
1546
|
+
\`\`\`
|
|
1547
|
+
|
|
1548
|
+
## Description
|
|
1549
|
+
|
|
1550
|
+
Export a description string for discoverability:
|
|
1551
|
+
|
|
1552
|
+
\`\`\`ts
|
|
1553
|
+
export const description = '{{description}}'
|
|
1554
|
+
\`\`\`
|
|
1555
|
+
|
|
1556
|
+
## Caching
|
|
1557
|
+
|
|
1558
|
+
Selectors cache by default. The default cache key is \`hashObject({ selectorName, args, gitSha })\` — same args + same commit = cache hit.
|
|
1559
|
+
|
|
1560
|
+
To customize the cache key:
|
|
1561
|
+
|
|
1562
|
+
\`\`\`ts
|
|
1563
|
+
export function cacheKey(args: z.infer<typeof argsSchema>, context: ContainerContext) {
|
|
1564
|
+
return context.container.git.currentCommitSha
|
|
1565
|
+
}
|
|
1566
|
+
\`\`\`
|
|
1567
|
+
|
|
1568
|
+
To disable caching:
|
|
1569
|
+
|
|
1570
|
+
\`\`\`ts
|
|
1571
|
+
export const cacheable = false
|
|
1572
|
+
\`\`\`
|
|
1573
|
+
|
|
1574
|
+
## Handler
|
|
1575
|
+
|
|
1576
|
+
Export a \`run\` function that returns data. It receives parsed args and the container context.
|
|
1577
|
+
|
|
1578
|
+
\`\`\`ts
|
|
1579
|
+
export async function run(args: z.infer<typeof argsSchema>, context: ContainerContext) {
|
|
1580
|
+
const { container } = context
|
|
1581
|
+
// Query and return your data
|
|
1582
|
+
return { /* your data */ }
|
|
1583
|
+
}
|
|
1584
|
+
\`\`\`
|
|
1585
|
+
|
|
1586
|
+
## Complete Example
|
|
1587
|
+
|
|
1588
|
+
\`\`\`ts
|
|
1589
|
+
import { z } from 'zod'
|
|
1590
|
+
import type { ContainerContext } from '@soederpop/luca'
|
|
1591
|
+
|
|
1592
|
+
export const description = '{{description}}'
|
|
1593
|
+
|
|
1594
|
+
export const argsSchema = z.object({})
|
|
1595
|
+
|
|
1596
|
+
export async function run(args: z.infer<typeof argsSchema>, context: ContainerContext) {
|
|
1597
|
+
const { container } = context
|
|
1598
|
+
|
|
1599
|
+
// Return your data here
|
|
1600
|
+
return {}
|
|
1601
|
+
}
|
|
1602
|
+
\`\`\`
|
|
1603
|
+
|
|
1604
|
+
## Conventions
|
|
1605
|
+
|
|
1606
|
+
- **File location**: \`selectors/{{kebabName}}.ts\` in the project root. Discovered automatically.
|
|
1607
|
+
- **Naming**: kebab-case for filename. \`luca select {{kebabName}}\` maps to \`selectors/{{kebabName}}.ts\`.
|
|
1608
|
+
- **Use the container**: Never import \`fs\`, \`path\` directly. Use \`container.feature('fs')\`, \`container.paths\`.
|
|
1609
|
+
- **Return data**: The \`run\` function must return the data. It gets wrapped in \`{ data, cached, cacheKey }\` by the framework.
|
|
1610
|
+
- **Caching**: On by default. Override \`cacheKey()\` for custom invalidation, or set \`cacheable = false\` to skip.
|
|
1611
|
+
- **CLI**: \`luca select {{kebabName}}\` runs the selector and prints JSON. Use \`--json\` for data only, \`--no-cache\` to force fresh.
|
|
1447
1612
|
`,
|
|
1448
1613
|
}
|
|
1449
1614
|
}
|
|
1450
1615
|
|
|
1616
|
+
export const assistantFiles: Record<string, string> = {
|
|
1617
|
+
"CORE.md": `# Luca Assistant Example
|
|
1618
|
+
|
|
1619
|
+
You are currently an example / template "Assistant" provided by the Luca framework. ( You'll probably have no idea what that is, don't worry, it doesn't matter ).
|
|
1620
|
+
|
|
1621
|
+
You are what gets scaffolded when a user writes the \`luca scaffold assistant\` command.
|
|
1622
|
+
|
|
1623
|
+
In luca, an Assistant is backed by a folder which has a few components:
|
|
1624
|
+
|
|
1625
|
+
- CORE.md -- this is a markdown file that will get injected into the system prompt of a chat completion call
|
|
1626
|
+
- tools.ts -- this file is expected to export functions, and a schemas object whose keys are the names of the functions that get exported, and whose values are zod v4 schemas that describe the parameters
|
|
1627
|
+
- hooks.ts -- this file is expexted to export functions, whose names match the events emitted by the luca assistant helper
|
|
1628
|
+
|
|
1629
|
+
Currently, the user is chatting with you from the \`luca chat\` CLI.
|
|
1630
|
+
|
|
1631
|
+
You should tell them what each of these files is and how to edit them.
|
|
1632
|
+
|
|
1633
|
+
It is also important for them to know that the luca \`container\` is globally available for them in the context of the \`tools.ts\` and \`hooks.ts\` files.
|
|
1634
|
+
|
|
1635
|
+
`,
|
|
1636
|
+
"tools.ts": `import { z } from 'zod'
|
|
1637
|
+
|
|
1638
|
+
export const schemas = {
|
|
1639
|
+
README: z.object({}).describe('CALL THIS README FUNCTION AS EARLY AS POSSIBLE')
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
export function README(options: z.infer<typeof schemas.README>) {
|
|
1643
|
+
return 'YO YO'
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
`,
|
|
1647
|
+
"hooks.ts": `export function started() {
|
|
1648
|
+
console.log('Assistant started!')
|
|
1649
|
+
}
|
|
1650
|
+
`
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1451
1653
|
export const mcpReadme = `# Luca Development Guide
|
|
1452
1654
|
|
|
1453
1655
|
You are working in a **luca project**. The luca container provides all capabilities your code needs. Do not install npm packages or import Node.js builtins directly.
|