@bluelibs/runner-dev 4.0.0
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/AI.md +411 -0
- package/README.md +1103 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +58 -0
- package/dist/cli.js.map +1 -0
- package/dist/client/documentation.d.ts +8 -0
- package/dist/client/documentation.js +144 -0
- package/dist/client/documentation.js.map +1 -0
- package/dist/components/Documentation/Documentation.d.ts +8 -0
- package/dist/components/Documentation/Documentation.js +283 -0
- package/dist/components/Documentation/Documentation.js.map +1 -0
- package/dist/components/Documentation/components/DiagnosticsPanel.d.ts +7 -0
- package/dist/components/Documentation/components/DiagnosticsPanel.js +126 -0
- package/dist/components/Documentation/components/DiagnosticsPanel.js.map +1 -0
- package/dist/components/Documentation/components/EventCard.d.ts +8 -0
- package/dist/components/Documentation/components/EventCard.js +164 -0
- package/dist/components/Documentation/components/EventCard.js.map +1 -0
- package/dist/components/Documentation/components/HookCard.d.ts +8 -0
- package/dist/components/Documentation/components/HookCard.js +111 -0
- package/dist/components/Documentation/components/HookCard.js.map +1 -0
- package/dist/components/Documentation/components/MiddlewareCard.d.ts +8 -0
- package/dist/components/Documentation/components/MiddlewareCard.js +159 -0
- package/dist/components/Documentation/components/MiddlewareCard.js.map +1 -0
- package/dist/components/Documentation/components/ResourceCard.d.ts +8 -0
- package/dist/components/Documentation/components/ResourceCard.js +94 -0
- package/dist/components/Documentation/components/ResourceCard.js.map +1 -0
- package/dist/components/Documentation/components/Sidebar.d.ts +13 -0
- package/dist/components/Documentation/components/Sidebar.js +129 -0
- package/dist/components/Documentation/components/Sidebar.js.map +1 -0
- package/dist/components/Documentation/components/TagCard.d.ts +7 -0
- package/dist/components/Documentation/components/TagCard.js +75 -0
- package/dist/components/Documentation/components/TagCard.js.map +1 -0
- package/dist/components/Documentation/components/TaskCard.d.ts +8 -0
- package/dist/components/Documentation/components/TaskCard.js +77 -0
- package/dist/components/Documentation/components/TaskCard.js.map +1 -0
- package/dist/components/Documentation/index.d.ts +2 -0
- package/dist/components/Documentation/index.js +6 -0
- package/dist/components/Documentation/index.js.map +1 -0
- package/dist/components/Documentation/utils/formatting.d.ts +8 -0
- package/dist/components/Documentation/utils/formatting.js +84 -0
- package/dist/components/Documentation/utils/formatting.js.map +1 -0
- package/dist/components/ExampleComponent.d.ts +10 -0
- package/dist/components/ExampleComponent.js +48 -0
- package/dist/components/ExampleComponent.js.map +1 -0
- package/dist/generated/resolvers-types.d.ts +1523 -0
- package/dist/generated/resolvers-types.js +3 -0
- package/dist/generated/resolvers-types.js.map +1 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +3 -0
- package/dist/main.js.map +1 -0
- package/dist/mcp/env.d.ts +5 -0
- package/dist/mcp/env.js +42 -0
- package/dist/mcp/env.js.map +1 -0
- package/dist/mcp/format.d.ts +50 -0
- package/dist/mcp/format.js +249 -0
- package/dist/mcp/format.js.map +1 -0
- package/dist/mcp/help.d.ts +20 -0
- package/dist/mcp/help.js +121 -0
- package/dist/mcp/help.js.map +1 -0
- package/dist/mcp/http.d.ts +6 -0
- package/dist/mcp/http.js +47 -0
- package/dist/mcp/http.js.map +1 -0
- package/dist/mcp/projectOverview.d.ts +2 -0
- package/dist/mcp/projectOverview.js +210 -0
- package/dist/mcp/projectOverview.js.map +1 -0
- package/dist/mcp/schema.d.ts +1 -0
- package/dist/mcp/schema.js +17 -0
- package/dist/mcp/schema.js.map +1 -0
- package/dist/mcp/tools/graphql.introspect.d.ts +2 -0
- package/dist/mcp/tools/graphql.introspect.js +19 -0
- package/dist/mcp/tools/graphql.introspect.js.map +1 -0
- package/dist/mcp/tools/graphql.mutation.d.ts +2 -0
- package/dist/mcp/tools/graphql.mutation.js +43 -0
- package/dist/mcp/tools/graphql.mutation.js.map +1 -0
- package/dist/mcp/tools/graphql.ping.d.ts +2 -0
- package/dist/mcp/tools/graphql.ping.js +23 -0
- package/dist/mcp/tools/graphql.ping.js.map +1 -0
- package/dist/mcp/tools/graphql.query.d.ts +2 -0
- package/dist/mcp/tools/graphql.query.js +42 -0
- package/dist/mcp/tools/graphql.query.js.map +1 -0
- package/dist/mcp/tools/graphql.schemaSdl.d.ts +2 -0
- package/dist/mcp/tools/graphql.schemaSdl.js +15 -0
- package/dist/mcp/tools/graphql.schemaSdl.js.map +1 -0
- package/dist/mcp/tools/help.read.d.ts +2 -0
- package/dist/mcp/tools/help.read.js +67 -0
- package/dist/mcp/tools/help.read.js.map +1 -0
- package/dist/mcp/tools/help.runner.d.ts +2 -0
- package/dist/mcp/tools/help.runner.js +55 -0
- package/dist/mcp/tools/help.runner.js.map +1 -0
- package/dist/mcp/tools/help.runnerDev.d.ts +2 -0
- package/dist/mcp/tools/help.runnerDev.js +56 -0
- package/dist/mcp/tools/help.runnerDev.js.map +1 -0
- package/dist/mcp.d.ts +1 -0
- package/dist/mcp.js +75 -0
- package/dist/mcp.js.map +1 -0
- package/dist/resources/dev.resource.d.ts +5 -0
- package/dist/resources/dev.resource.js +26 -0
- package/dist/resources/dev.resource.js.map +1 -0
- package/dist/resources/docs.generator.resource.d.ts +17 -0
- package/dist/resources/docs.generator.resource.js +230 -0
- package/dist/resources/docs.generator.resource.js.map +1 -0
- package/dist/resources/graphql-accumulator.resource.d.ts +7 -0
- package/dist/resources/graphql-accumulator.resource.js +41 -0
- package/dist/resources/graphql-accumulator.resource.js.map +1 -0
- package/dist/resources/introspector.resource.d.ts +129 -0
- package/dist/resources/introspector.resource.js +266 -0
- package/dist/resources/introspector.resource.js.map +1 -0
- package/dist/resources/introspector.tools.d.ts +47 -0
- package/dist/resources/introspector.tools.js +505 -0
- package/dist/resources/introspector.tools.js.map +1 -0
- package/dist/resources/live.resource.d.ts +80 -0
- package/dist/resources/live.resource.js +231 -0
- package/dist/resources/live.resource.js.map +1 -0
- package/dist/resources/server.resource.d.ts +38 -0
- package/dist/resources/server.resource.js +106 -0
- package/dist/resources/server.resource.js.map +1 -0
- package/dist/resources/swap.resource.d.ts +43 -0
- package/dist/resources/swap.resource.js +251 -0
- package/dist/resources/swap.resource.js.map +1 -0
- package/dist/resources/swap.tools.d.ts +31 -0
- package/dist/resources/swap.tools.js +207 -0
- package/dist/resources/swap.tools.js.map +1 -0
- package/dist/resources/telemetry.chain.d.ts +13 -0
- package/dist/resources/telemetry.chain.js +32 -0
- package/dist/resources/telemetry.chain.js.map +1 -0
- package/dist/resources/telemetry.resource.d.ts +1 -0
- package/dist/resources/telemetry.resource.js +90 -0
- package/dist/resources/telemetry.resource.js.map +1 -0
- package/dist/schema/context.d.ts +11 -0
- package/dist/schema/context.js +3 -0
- package/dist/schema/context.js.map +1 -0
- package/dist/schema/index.d.ts +7 -0
- package/dist/schema/index.js +72 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/model.d.ts +97 -0
- package/dist/schema/model.js +5 -0
- package/dist/schema/model.js.map +1 -0
- package/dist/schema/mutation.d.ts +3 -0
- package/dist/schema/mutation.js +112 -0
- package/dist/schema/mutation.js.map +1 -0
- package/dist/schema/query.d.ts +3 -0
- package/dist/schema/query.js +295 -0
- package/dist/schema/query.js.map +1 -0
- package/dist/schema/types/AllType.d.ts +3 -0
- package/dist/schema/types/AllType.js +149 -0
- package/dist/schema/types/AllType.js.map +1 -0
- package/dist/schema/types/BaseElementCommon.d.ts +11 -0
- package/dist/schema/types/BaseElementCommon.js +61 -0
- package/dist/schema/types/BaseElementCommon.js.map +1 -0
- package/dist/schema/types/DiagnosticsType.d.ts +2 -0
- package/dist/schema/types/DiagnosticsType.js +15 -0
- package/dist/schema/types/DiagnosticsType.js.map +1 -0
- package/dist/schema/types/EventType.d.ts +4 -0
- package/dist/schema/types/EventType.js +97 -0
- package/dist/schema/types/EventType.js.map +1 -0
- package/dist/schema/types/HookType.d.ts +4 -0
- package/dist/schema/types/HookType.js +123 -0
- package/dist/schema/types/HookType.js.map +1 -0
- package/dist/schema/types/LiveType.d.ts +33 -0
- package/dist/schema/types/LiveType.js +553 -0
- package/dist/schema/types/LiveType.js.map +1 -0
- package/dist/schema/types/MetaType.d.ts +3 -0
- package/dist/schema/types/MetaType.js +31 -0
- package/dist/schema/types/MetaType.js.map +1 -0
- package/dist/schema/types/MiddlewareType.d.ts +4 -0
- package/dist/schema/types/MiddlewareType.js +26 -0
- package/dist/schema/types/MiddlewareType.js.map +1 -0
- package/dist/schema/types/ResourceType.d.ts +2 -0
- package/dist/schema/types/ResourceType.js +145 -0
- package/dist/schema/types/ResourceType.js.map +1 -0
- package/dist/schema/types/RunTypes.d.ts +7 -0
- package/dist/schema/types/RunTypes.js +95 -0
- package/dist/schema/types/RunTypes.js.map +1 -0
- package/dist/schema/types/SwapType.d.ts +5 -0
- package/dist/schema/types/SwapType.js +42 -0
- package/dist/schema/types/SwapType.js.map +1 -0
- package/dist/schema/types/TagType.d.ts +6 -0
- package/dist/schema/types/TagType.js +48 -0
- package/dist/schema/types/TagType.js.map +1 -0
- package/dist/schema/types/TaskLikeCommon.d.ts +7 -0
- package/dist/schema/types/TaskLikeCommon.js +86 -0
- package/dist/schema/types/TaskLikeCommon.js.map +1 -0
- package/dist/schema/types/TaskType.d.ts +11 -0
- package/dist/schema/types/TaskType.js +188 -0
- package/dist/schema/types/TaskType.js.map +1 -0
- package/dist/schema/types/index.d.ts +13 -0
- package/dist/schema/types/index.js +44 -0
- package/dist/schema/types/index.js.map +1 -0
- package/dist/schema/types/middleware/UsageTypes.d.ts +3 -0
- package/dist/schema/types/middleware/UsageTypes.js +23 -0
- package/dist/schema/types/middleware/UsageTypes.js.map +1 -0
- package/dist/schema/types/middleware/common.d.ts +6 -0
- package/dist/schema/types/middleware/common.js +156 -0
- package/dist/schema/types/middleware/common.js.map +1 -0
- package/dist/schema/utils.d.ts +12 -0
- package/dist/schema/utils.js +35 -0
- package/dist/schema/utils.js.map +1 -0
- package/dist/ui/index.html +20 -0
- package/dist/ui/static/index-D-NS5aw1.js +40 -0
- package/dist/utils/json-schema-to-readable.d.ts +1 -0
- package/dist/utils/json-schema-to-readable.js +256 -0
- package/dist/utils/json-schema-to-readable.js.map +1 -0
- package/dist/utils/path.d.ts +16 -0
- package/dist/utils/path.js +101 -0
- package/dist/utils/path.js.map +1 -0
- package/dist/utils/react-ssr.d.ts +9 -0
- package/dist/utils/react-ssr.js +36 -0
- package/dist/utils/react-ssr.js.map +1 -0
- package/dist/utils/zod.d.ts +6 -0
- package/dist/utils/zod.js +39 -0
- package/dist/utils/zod.js.map +1 -0
- package/package.json +83 -0
package/README.md
ADDED
|
@@ -0,0 +1,1103 @@
|
|
|
1
|
+
## Welcome
|
|
2
|
+
|
|
3
|
+
Runner Dev Tools provide introspection, live telemetry, and a GraphQL API to explore and query your running Runner app.
|
|
4
|
+
|
|
5
|
+
The way it works, is that this is a resource that opens a graphql server which opens your application to introspection.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @bluelibs/runner-dev
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { dev } from "@bluelibs/runner-dev";
|
|
15
|
+
|
|
16
|
+
const app = resource({
|
|
17
|
+
register: [
|
|
18
|
+
// your resources,
|
|
19
|
+
dev, // if you are fine with defaults or
|
|
20
|
+
dev.with({
|
|
21
|
+
port: 1337, // default,
|
|
22
|
+
maxEntries: 10000, // how many logs to keep in the store.
|
|
23
|
+
}),
|
|
24
|
+
],
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## What you get
|
|
29
|
+
|
|
30
|
+
- Introspector: programmatic API to inspect tasks, hooks, resources, events, middleware, and diagnostics (including file paths, contents)
|
|
31
|
+
- Live: in-memory logs and event emissions
|
|
32
|
+
- GraphQL server: deep graph navigation over your app’s topology and live data
|
|
33
|
+
- MCP server: allow your AI to do introspection for you.
|
|
34
|
+
|
|
35
|
+
## Quickstart
|
|
36
|
+
|
|
37
|
+
Register the Dev resources in your Runner root:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { resource } from "@bluelibs/runner";
|
|
41
|
+
import { dev } from "@bluelibs/runner-dev";
|
|
42
|
+
|
|
43
|
+
const register = [
|
|
44
|
+
// rest of your app elements
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
if (process.env.DEV_MODE) {
|
|
48
|
+
register.push(dev);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const app = resource({
|
|
52
|
+
id: "app",
|
|
53
|
+
register: [
|
|
54
|
+
// You can omit .with() if you are fine with defaults.
|
|
55
|
+
dev.with({
|
|
56
|
+
port: 1337, // default
|
|
57
|
+
maxEntries: 1000, // default
|
|
58
|
+
}),
|
|
59
|
+
// rest of your app.
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Add it as an MCP Server:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"mcpServers": {
|
|
69
|
+
"mcp-graphql": {
|
|
70
|
+
"description": "MCP Server for Active Running Context App",
|
|
71
|
+
"command": "npx",
|
|
72
|
+
"args": ["runner-dev", "mcp"],
|
|
73
|
+
"env": {
|
|
74
|
+
"ENDPOINT": "http://localhost:1337/graphql",
|
|
75
|
+
"ALLOW_MUTATIONS": "true"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Then start your app as usual. The Dev GraphQL server will be available at http://localhost:1337/graphql.
|
|
83
|
+
|
|
84
|
+
### CLI usage (MCP server)
|
|
85
|
+
|
|
86
|
+
After installing, you can start the MCP server from this package via stdio.
|
|
87
|
+
|
|
88
|
+
Using npx:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
ENDPOINT=http://localhost:1337/graphql npx -y @bluelibs/runner-dev mcp
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Optional environment variables:
|
|
95
|
+
|
|
96
|
+
- `ALLOW_MUTATIONS=true` to enable `graphql.mutation`
|
|
97
|
+
- `HEADERS='{"Authorization":"Bearer token"}'` to pass extra headers
|
|
98
|
+
|
|
99
|
+
Available tools once connected:
|
|
100
|
+
|
|
101
|
+
- `graphql.query` — run read-only queries
|
|
102
|
+
- `graphql.mutation` — run mutations (requires `ALLOW_MUTATIONS=true`)
|
|
103
|
+
- `graphql.introspect` — fetch schema
|
|
104
|
+
- `graphql.ping` — reachability check
|
|
105
|
+
- `project.overview` — dynamic Markdown overview aggregated from the API
|
|
106
|
+
|
|
107
|
+
## Programmatic Introspection (without GraphQL)
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { resource } from "@bluelibs/runner";
|
|
111
|
+
import type { Introspector } from "@bluelibs/runner-dev/dist/resources/introspector.resource";
|
|
112
|
+
import { introspector } from "@bluelibs/runner-dev/dist/resources/introspector.resource";
|
|
113
|
+
|
|
114
|
+
export const probe = resource({
|
|
115
|
+
id: "probe",
|
|
116
|
+
dependencies: { introspector },
|
|
117
|
+
async init(_c, { introspector }: { introspector: Introspector }) {
|
|
118
|
+
const tasks = introspector.getTasks(); // Task[]
|
|
119
|
+
const hooks = introspector.getHooks(); // Hook[]
|
|
120
|
+
const resources = introspector.getResources(); // Resource[]
|
|
121
|
+
const events = introspector.getEvents(); // Event[]
|
|
122
|
+
const middlewares = introspector.getMiddlewares(); // Middleware[]
|
|
123
|
+
|
|
124
|
+
const deps = introspector.getDependencies(tasks[0]); // tasks/hooks/resources/emitters
|
|
125
|
+
|
|
126
|
+
// Diagnostics
|
|
127
|
+
const diagnostics = introspector.getDiagnostics();
|
|
128
|
+
// Or granular helpers
|
|
129
|
+
// introspector.getOrphanEvents();
|
|
130
|
+
// introspector.getUnemittedEvents();
|
|
131
|
+
// introspector.getUnusedMiddleware();
|
|
132
|
+
// introspector.getMissingFiles();
|
|
133
|
+
// introspector.getOverrideConflicts();
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## GraphQL API Highlights
|
|
139
|
+
|
|
140
|
+
All arrays are non-null lists with non-null items, and ids are complemented by resolved fields for deep traversal.
|
|
141
|
+
|
|
142
|
+
- Common
|
|
143
|
+
|
|
144
|
+
- `BaseElement`: `id: ID!`, `meta: Meta`, `filePath: String`, `markdownDescription: String!`, `fileContents(startLine: Int, endLine: Int): String`
|
|
145
|
+
- `Meta`: `title: String`, `description: String`, `tags: [MetaTagUsage!]!`
|
|
146
|
+
- `MetaTagUsage`: `id: ID!`, `config: String`
|
|
147
|
+
|
|
148
|
+
- Query root
|
|
149
|
+
|
|
150
|
+
- `all(idIncludes: ID): [BaseElement!]!`
|
|
151
|
+
- `event(id: ID!): Event`, `events(filter: EventFilterInput): [Event!]!`
|
|
152
|
+
- `task(id: ID!): Task`, `tasks(idIncludes: ID): [Task!]!`
|
|
153
|
+
- `hooks(idIncludes: ID): [Hook!]!`
|
|
154
|
+
- `middleware(id: ID!): Middleware`, `middlewares(idIncludes: ID): [Middleware!]!`
|
|
155
|
+
- `taskMiddlewares(idIncludes: ID): [TaskMiddleware!]!`
|
|
156
|
+
- `resourceMiddlewares(idIncludes: ID): [ResourceMiddleware!]!`
|
|
157
|
+
- `resource(id: ID!): Resource`, `resources(idIncludes: ID): [Resource!]!`
|
|
158
|
+
- `root: Resource`
|
|
159
|
+
- `swappedTasks: [SwappedTask!]!`
|
|
160
|
+
- `live: Live!`
|
|
161
|
+
- `diagnostics: [Diagnostic!]!`
|
|
162
|
+
|
|
163
|
+
- All
|
|
164
|
+
|
|
165
|
+
- `id`, `meta`, `filePath`, `fileContents`, `markdownDescription`
|
|
166
|
+
|
|
167
|
+
- Tasks/Hooks
|
|
168
|
+
|
|
169
|
+
- Shared: `emits: [String!]!`, `emitsResolved: [Event!]!`
|
|
170
|
+
- Shared: `dependsOn: [String!]!`
|
|
171
|
+
- Shared: `middleware: [String!]!`, `middlewareResolved: [TaskMiddleware!]!`, `middlewareResolvedDetailed: [TaskMiddlewareUsage!]!`
|
|
172
|
+
- Shared: `overriddenBy: String`, `registeredBy: String`, `registeredByResolved: Resource`
|
|
173
|
+
- Task-specific: `inputSchema: String`, `inputSchemaReadable: String`, `dependsOnResolved { tasks { id } resources { id } emitters { id } }`
|
|
174
|
+
- Hook-specific: `event: String!`, `hookOrder: Int`
|
|
175
|
+
|
|
176
|
+
- Resources
|
|
177
|
+
|
|
178
|
+
- `config: String`, `context: String`, `configSchema: String`, `configSchemaReadable: String`
|
|
179
|
+
- `middleware`, `middlewareResolved: [ResourceMiddleware!]!`, `middlewareResolvedDetailed: [TaskMiddlewareUsage!]!`
|
|
180
|
+
- `overrides`, `overridesResolved`
|
|
181
|
+
- `registers`, `registersResolved`
|
|
182
|
+
- `usedBy: [Task!]!`
|
|
183
|
+
- `emits: [Event!]!` (inferred from task/hook emissions)
|
|
184
|
+
- `dependsOn: [String!]!`, `dependsOnResolved: [Resource!]!`
|
|
185
|
+
- `registeredBy: String`, `registeredByResolved: Resource`
|
|
186
|
+
|
|
187
|
+
- Events
|
|
188
|
+
|
|
189
|
+
- `emittedBy: [String!]!`, `emittedByResolved: [Hook!]!`
|
|
190
|
+
- `listenedToBy: [String!]!`, `listenedToByResolved: [Hook!]!`
|
|
191
|
+
- `payloadSchema: String`, `payloadSchemaReadable: String`
|
|
192
|
+
- `registeredBy: String`, `registeredByResolved: Resource`
|
|
193
|
+
|
|
194
|
+
- TaskMiddleware
|
|
195
|
+
|
|
196
|
+
- `global: GlobalMiddleware`
|
|
197
|
+
- `usedBy: [Task!]!`, `usedByDetailed: [MiddlewareTaskUsage!]!`
|
|
198
|
+
- `emits: [Event!]!`
|
|
199
|
+
- `overriddenBy: String`, `registeredBy: String`, `registeredByResolved: Resource`
|
|
200
|
+
|
|
201
|
+
- ResourceMiddleware
|
|
202
|
+
|
|
203
|
+
- `global: GlobalMiddleware`
|
|
204
|
+
- `usedBy: [Resource!]!`, `usedByDetailed: [MiddlewareResourceUsage!]!`
|
|
205
|
+
- `emits: [Event!]!`
|
|
206
|
+
- `overriddenBy: String`, `registeredBy: String`, `registeredByResolved: Resource`
|
|
207
|
+
|
|
208
|
+
- Live
|
|
209
|
+
|
|
210
|
+
- `logs(afterTimestamp: Float, last: Int, filter: LogFilterInput): [LogEntry!]!`
|
|
211
|
+
- `LogEntry { timestampMs, level, message, data, correlationId }`
|
|
212
|
+
- `LogFilterInput { levels: [LogLevelEnum!], messageIncludes: String, correlationIds: [String!] }`
|
|
213
|
+
- `emissions(afterTimestamp: Float, last: Int, filter: EmissionFilterInput): [EmissionEntry!]!`
|
|
214
|
+
- `EmissionEntry { timestampMs, eventId, emitterId, payload, correlationId }`
|
|
215
|
+
- `EmissionFilterInput { eventIds: [String!], emitterIds: [String!] }`
|
|
216
|
+
- `errors(afterTimestamp: Float, last: Int, filter: ErrorFilterInput): [ErrorEntry!]!`
|
|
217
|
+
- `ErrorEntry { timestampMs, sourceId, sourceKind, message, stack, data, correlationId }`
|
|
218
|
+
- `ErrorFilterInput { sourceKinds: [SourceKindEnum!], sourceIds: [ID!], messageIncludes: String }`
|
|
219
|
+
- `runs(afterTimestamp: Float, last: Int, filter: RunFilterInput): [RunRecord!]!`
|
|
220
|
+
- `RunRecord { timestampMs, nodeId, nodeKind, durationMs, ok, error, parentId, rootId, correlationId }`
|
|
221
|
+
- `RunFilterInput { nodeKinds: [NodeKindEnum!], nodeIds: [String!], ok: Boolean, parentIds: [String!], rootIds: [String!] }`
|
|
222
|
+
|
|
223
|
+
Enums: `LogLevelEnum` = `trace|debug|info|warn|error|fatal|log`, `SourceKindEnum` = `TASK|HOOK|RESOURCE|MIDDLEWARE|INTERNAL`, `NodeKindEnum` = `TASK|HOOK`.
|
|
224
|
+
|
|
225
|
+
- Diagnostics
|
|
226
|
+
|
|
227
|
+
- `Diagnostic { severity: String!, code: String!, message: String!, nodeId: ID, nodeKind: String }`
|
|
228
|
+
- Exposed via `diagnostics: [Diagnostic!]!` on the root query; diagnostics are computed from the in-memory introspected graph with safe filesystem checks.
|
|
229
|
+
- Example codes: `ORPHAN_EVENT`, `UNEMITTED_EVENT`, `UNUSED_MIDDLEWARE`, `MISSING_FILE`, `OVERRIDE_CONFLICT`.
|
|
230
|
+
|
|
231
|
+
### Example Queries
|
|
232
|
+
|
|
233
|
+
- Explore tasks and dependencies deeply
|
|
234
|
+
|
|
235
|
+
```graphql
|
|
236
|
+
query {
|
|
237
|
+
tasks {
|
|
238
|
+
id
|
|
239
|
+
filePath
|
|
240
|
+
emits
|
|
241
|
+
emitsResolved {
|
|
242
|
+
id
|
|
243
|
+
}
|
|
244
|
+
dependsOn
|
|
245
|
+
middleware
|
|
246
|
+
middlewareResolved {
|
|
247
|
+
id
|
|
248
|
+
}
|
|
249
|
+
dependsOnResolved {
|
|
250
|
+
tasks {
|
|
251
|
+
id
|
|
252
|
+
}
|
|
253
|
+
resources {
|
|
254
|
+
id
|
|
255
|
+
}
|
|
256
|
+
emitters {
|
|
257
|
+
id
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
- Diagnostics
|
|
265
|
+
|
|
266
|
+
```graphql
|
|
267
|
+
query {
|
|
268
|
+
diagnostics {
|
|
269
|
+
severity
|
|
270
|
+
code
|
|
271
|
+
message
|
|
272
|
+
nodeId
|
|
273
|
+
nodeKind
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
- Traverse from middleware to dependents, then back to their middleware
|
|
279
|
+
|
|
280
|
+
```graphql
|
|
281
|
+
query {
|
|
282
|
+
middlewares {
|
|
283
|
+
id
|
|
284
|
+
usedByTasksResolved {
|
|
285
|
+
id
|
|
286
|
+
middlewareResolved {
|
|
287
|
+
id
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
usedByResourcesResolved {
|
|
291
|
+
id
|
|
292
|
+
}
|
|
293
|
+
emits {
|
|
294
|
+
id
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
- Events and hooks
|
|
301
|
+
|
|
302
|
+
```graphql
|
|
303
|
+
query {
|
|
304
|
+
events {
|
|
305
|
+
id
|
|
306
|
+
emittedBy
|
|
307
|
+
emittedByResolved {
|
|
308
|
+
id
|
|
309
|
+
}
|
|
310
|
+
listenedToBy
|
|
311
|
+
listenedToByResolved {
|
|
312
|
+
id
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## Live Telemetry
|
|
319
|
+
|
|
320
|
+
The `live` resource records:
|
|
321
|
+
|
|
322
|
+
- Logs emitted via `globals.events.log`
|
|
323
|
+
- All event emissions (via an internal global `on: "*"` hook)
|
|
324
|
+
|
|
325
|
+
GraphQL (basic):
|
|
326
|
+
|
|
327
|
+
```graphql
|
|
328
|
+
query {
|
|
329
|
+
live {
|
|
330
|
+
logs(afterTimestamp: 0) {
|
|
331
|
+
timestampMs
|
|
332
|
+
level
|
|
333
|
+
message
|
|
334
|
+
data # stringified JSON if object, otherwise null
|
|
335
|
+
}
|
|
336
|
+
emissions(afterTimestamp: 0) {
|
|
337
|
+
timestampMs
|
|
338
|
+
eventId
|
|
339
|
+
emitterId
|
|
340
|
+
payload # stringified JSON if object, otherwise null
|
|
341
|
+
}
|
|
342
|
+
errors(afterTimestamp: 0) {
|
|
343
|
+
timestampMs
|
|
344
|
+
sourceId
|
|
345
|
+
sourceKind
|
|
346
|
+
message
|
|
347
|
+
}
|
|
348
|
+
runs(afterTimestamp: 0) {
|
|
349
|
+
timestampMs
|
|
350
|
+
nodeId
|
|
351
|
+
nodeKind
|
|
352
|
+
durationMs
|
|
353
|
+
ok
|
|
354
|
+
parentId
|
|
355
|
+
rootId
|
|
356
|
+
correlationId
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Filter by timestamp (ms) to retrieve only recent entries.
|
|
363
|
+
|
|
364
|
+
GraphQL (with filters and last):
|
|
365
|
+
|
|
366
|
+
```graphql
|
|
367
|
+
query {
|
|
368
|
+
live {
|
|
369
|
+
logs(
|
|
370
|
+
last: 100
|
|
371
|
+
filter: { levels: [debug, error], messageIncludes: "probe" }
|
|
372
|
+
) {
|
|
373
|
+
timestampMs
|
|
374
|
+
level
|
|
375
|
+
message
|
|
376
|
+
correlationId
|
|
377
|
+
}
|
|
378
|
+
emissions(
|
|
379
|
+
last: 50
|
|
380
|
+
filter: { eventIds: ["evt.hello"], emitterIds: ["task.id"] }
|
|
381
|
+
) {
|
|
382
|
+
eventId
|
|
383
|
+
emitterId
|
|
384
|
+
}
|
|
385
|
+
errors(
|
|
386
|
+
last: 10
|
|
387
|
+
filter: { sourceKinds: [TASK, RESOURCE], messageIncludes: "boom" }
|
|
388
|
+
) {
|
|
389
|
+
sourceKind
|
|
390
|
+
message
|
|
391
|
+
}
|
|
392
|
+
runs(afterTimestamp: 0, last: 5, filter: { ok: true, nodeKinds: [TASK] }) {
|
|
393
|
+
nodeId
|
|
394
|
+
durationMs
|
|
395
|
+
ok
|
|
396
|
+
correlationId
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Live system health
|
|
403
|
+
|
|
404
|
+
- **memory: `MemoryStats!`**
|
|
405
|
+
- Fields: `heapUsed` (bytes), `heapTotal` (bytes), `rss` (bytes)
|
|
406
|
+
- **cpu: `CpuStats!`**
|
|
407
|
+
- Fields: `usage` (0..1 event loop utilization), `loadAverage` (1‑minute load avg)
|
|
408
|
+
- **eventLoop(reset: Boolean): `EventLoopStats!`**
|
|
409
|
+
- Fields: `lag` (ms, avg delay via `monitorEventLoopDelay`)
|
|
410
|
+
- Args: `reset` optionally clears the histogram after reading
|
|
411
|
+
- **gc(windowMs: Float): `GcStats!`**
|
|
412
|
+
- Fields: `collections` (count), `duration` (ms)
|
|
413
|
+
- Args: `windowMs` returns stats only within the last window; omitted = totals since process start
|
|
414
|
+
|
|
415
|
+
Example query:
|
|
416
|
+
|
|
417
|
+
```graphql
|
|
418
|
+
query SystemHealth {
|
|
419
|
+
live {
|
|
420
|
+
memory {
|
|
421
|
+
heapUsed
|
|
422
|
+
heapTotal
|
|
423
|
+
rss
|
|
424
|
+
}
|
|
425
|
+
cpu {
|
|
426
|
+
usage
|
|
427
|
+
loadAverage
|
|
428
|
+
}
|
|
429
|
+
eventLoop(reset: true) {
|
|
430
|
+
lag
|
|
431
|
+
}
|
|
432
|
+
gc(windowMs: 10000) {
|
|
433
|
+
collections
|
|
434
|
+
duration
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Notes:
|
|
441
|
+
|
|
442
|
+
- `heap*` and `rss` are bytes.
|
|
443
|
+
- `cpu.usage` is a ratio; `loadAverage` is 1‑minute OS load.
|
|
444
|
+
- `eventLoop.lag` may be 0 if `monitorEventLoopDelay` is unavailable.
|
|
445
|
+
|
|
446
|
+
### Correlation and call chains
|
|
447
|
+
|
|
448
|
+
- What is correlationId? An opaque UUID (via `crypto.randomUUID()`) created for the first task in a run chain.
|
|
449
|
+
- How is it formed?
|
|
450
|
+
- When a task starts, a middleware opens an AsyncLocalStorage scope containing:
|
|
451
|
+
- `correlationId`: a UUID for the chain
|
|
452
|
+
- `chain`: ordered array of node ids representing the call path
|
|
453
|
+
- Nested tasks and listeners reuse the same AsyncLocalStorage scope, so the same `correlationId` flows throughout the chain.
|
|
454
|
+
- What does it contain? Only a UUID string. No payload, no PII.
|
|
455
|
+
- Where is it recorded?
|
|
456
|
+
- `logs.correlationId`
|
|
457
|
+
- `emissions.correlationId`
|
|
458
|
+
- `errors.correlationId`
|
|
459
|
+
- `runs.correlationId` plus `runs.parentId` and `runs.rootId` for chain topology
|
|
460
|
+
- How to use it
|
|
461
|
+
- Read a recent run to discover a correlation id, then filter logs by it:
|
|
462
|
+
|
|
463
|
+
```graphql
|
|
464
|
+
query TraceByCorrelation($ts: Float, $cid: String!) {
|
|
465
|
+
live {
|
|
466
|
+
runs(afterTimestamp: $ts, last: 10) {
|
|
467
|
+
nodeId
|
|
468
|
+
parentId
|
|
469
|
+
rootId
|
|
470
|
+
correlationId
|
|
471
|
+
}
|
|
472
|
+
logs(last: 100, filter: { correlationIds: [$cid] }) {
|
|
473
|
+
timestampMs
|
|
474
|
+
level
|
|
475
|
+
message
|
|
476
|
+
correlationId
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
## Emitting Events (Runner-native)
|
|
483
|
+
|
|
484
|
+
- Define an event:
|
|
485
|
+
|
|
486
|
+
```ts
|
|
487
|
+
import { event } from "@bluelibs/runner";
|
|
488
|
+
|
|
489
|
+
export const userCreated = event<{ id: string; name: string }>({
|
|
490
|
+
id: "evt.user.created",
|
|
491
|
+
});
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
- Use it in a task:
|
|
495
|
+
|
|
496
|
+
```ts
|
|
497
|
+
import { task } from "@bluelibs/runner";
|
|
498
|
+
import { userCreated } from "./events";
|
|
499
|
+
|
|
500
|
+
export const createUser = task({
|
|
501
|
+
id: "task.user.create",
|
|
502
|
+
dependencies: { userCreated },
|
|
503
|
+
async run(input: { name: string }, { userCreated }) {
|
|
504
|
+
const id = crypto.randomUUID();
|
|
505
|
+
await userCreated({ id, name: input.name });
|
|
506
|
+
return { id };
|
|
507
|
+
},
|
|
508
|
+
});
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
- Emit logs:
|
|
512
|
+
|
|
513
|
+
```ts
|
|
514
|
+
import { globals, task } from "@bluelibs/runner";
|
|
515
|
+
|
|
516
|
+
export const logSomething = task({
|
|
517
|
+
id: "task.log",
|
|
518
|
+
dependencies: { logger: globals.resources.logger },
|
|
519
|
+
async run(_i, { logger }) {
|
|
520
|
+
logger.info("Hello world!");
|
|
521
|
+
},
|
|
522
|
+
});
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
## Notes on Overrides
|
|
526
|
+
|
|
527
|
+
- If a resource overrides another registerable, the overridden node remains discoverable but marked with `overriddenBy`.
|
|
528
|
+
- Only the active definition exists; we do not retain a shadow copy of the original.
|
|
529
|
+
|
|
530
|
+
## Guarantees and DX
|
|
531
|
+
|
|
532
|
+
- No `any` in APIs; strong types for nodes and relations
|
|
533
|
+
- Non-null lists with non-null items (`[T!]!`) in GraphQL
|
|
534
|
+
- Deep “resolved” fields for easy graph traversal
|
|
535
|
+
- File-aware enhancements:
|
|
536
|
+
- `filePath` everywhere
|
|
537
|
+
- `fileContents` and `markdownDescription` on `all` (computed on demand)
|
|
538
|
+
|
|
539
|
+
## Development
|
|
540
|
+
|
|
541
|
+
- Library targets `@bluelibs/runner` v4+
|
|
542
|
+
- GraphQL built with Apollo Server 5
|
|
543
|
+
- Tests cover:
|
|
544
|
+
- Node discovery, dependencies, and emissions
|
|
545
|
+
- Overrides, middleware usage, and deep traversal
|
|
546
|
+
- Live logs and emissions (with timestamp filtering)
|
|
547
|
+
- Contributions welcome!
|
|
548
|
+
|
|
549
|
+
### Type-safe GraphQL resolvers
|
|
550
|
+
|
|
551
|
+
- We generate resolver arg types from the schema using GraphQL Code Generator.
|
|
552
|
+
- Run this after any schema change:
|
|
553
|
+
|
|
554
|
+
```bash
|
|
555
|
+
npm run codegen
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
- Generated types are in `src/generated/resolvers-types.ts` and used in schema resolvers (for example `LiveLogsArgs`, `QueryEventArgs`).
|
|
559
|
+
|
|
560
|
+
## 🔥 Hot-Swapping Debugging System
|
|
561
|
+
|
|
562
|
+
**Revolutionary live debugging feature that allows AI assistants and developers to dynamically replace task run functions in live applications.**
|
|
563
|
+
|
|
564
|
+
### Overview
|
|
565
|
+
|
|
566
|
+
The hot-swapping system enables:
|
|
567
|
+
|
|
568
|
+
- **Live Function Replacement**: Replace any task's `run` function with new TypeScript/JavaScript code without restarting the application
|
|
569
|
+
- **TypeScript Compilation**: Automatic compilation and validation of swapped code
|
|
570
|
+
- **GraphQL API**: Remote swap operations via GraphQL mutations
|
|
571
|
+
- **Live Telemetry Integration**: Real-time capture of debug logs from swapped functions
|
|
572
|
+
- **Rollback Support**: Easy restoration to original functions
|
|
573
|
+
- **Type Safety**: 100% type-safe implementation with comprehensive error handling
|
|
574
|
+
|
|
575
|
+
### Quick Setup
|
|
576
|
+
|
|
577
|
+
Add the swap manager to your app:
|
|
578
|
+
|
|
579
|
+
```ts
|
|
580
|
+
import { resource } from "@bluelibs/runner";
|
|
581
|
+
import { resources as dev } from "@bluelibs/runner-dev";
|
|
582
|
+
|
|
583
|
+
export const app = resource({
|
|
584
|
+
id: "app",
|
|
585
|
+
register: [
|
|
586
|
+
// Core dev resources
|
|
587
|
+
dev.live,
|
|
588
|
+
dev.introspector,
|
|
589
|
+
|
|
590
|
+
// Add the swap manager for hot-swapping
|
|
591
|
+
dev.swapManager,
|
|
592
|
+
|
|
593
|
+
// GraphQL server with swap mutations
|
|
594
|
+
dev.server.with({ port: 1337 }),
|
|
595
|
+
],
|
|
596
|
+
});
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
### GraphQL API
|
|
600
|
+
|
|
601
|
+
#### Queries
|
|
602
|
+
|
|
603
|
+
**Get currently swapped tasks:**
|
|
604
|
+
|
|
605
|
+
```graphql
|
|
606
|
+
query {
|
|
607
|
+
swappedTasks {
|
|
608
|
+
taskId
|
|
609
|
+
swappedAt
|
|
610
|
+
originalCode
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
#### Mutations
|
|
616
|
+
|
|
617
|
+
**Swap a task's run function:**
|
|
618
|
+
|
|
619
|
+
```graphql
|
|
620
|
+
mutation SwapTask($taskId: ID!, $runCode: String!) {
|
|
621
|
+
swapTask(taskId: $taskId, runCode: $runCode) {
|
|
622
|
+
success
|
|
623
|
+
error
|
|
624
|
+
taskId
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
**Restore original function:**
|
|
630
|
+
|
|
631
|
+
```graphql
|
|
632
|
+
mutation UnswapTask($taskId: ID!) {
|
|
633
|
+
unswapTask(taskId: $taskId) {
|
|
634
|
+
success
|
|
635
|
+
error
|
|
636
|
+
taskId
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
**Restore all swapped tasks:**
|
|
642
|
+
|
|
643
|
+
```graphql
|
|
644
|
+
mutation UnswapAllTasks {
|
|
645
|
+
unswapAllTasks {
|
|
646
|
+
success
|
|
647
|
+
error
|
|
648
|
+
taskId
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
### Usage Examples
|
|
654
|
+
|
|
655
|
+
#### Basic Function Swapping
|
|
656
|
+
|
|
657
|
+
Replace a task's logic with enhanced debugging:
|
|
658
|
+
|
|
659
|
+
```graphql
|
|
660
|
+
mutation {
|
|
661
|
+
swapTask(
|
|
662
|
+
taskId: "user.create"
|
|
663
|
+
runCode: """
|
|
664
|
+
async function run(input, deps) {
|
|
665
|
+
// Add comprehensive logging
|
|
666
|
+
if (deps.emitLog) {
|
|
667
|
+
await deps.emitLog({
|
|
668
|
+
timestamp: new Date(),
|
|
669
|
+
level: "info",
|
|
670
|
+
message: "🔍 DEBUG: Creating user started",
|
|
671
|
+
data: { input }
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Enhanced validation
|
|
676
|
+
if (!input.email || !input.email.includes('@')) {
|
|
677
|
+
throw new Error('Invalid email address');
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Original logic with debugging
|
|
681
|
+
const result = {
|
|
682
|
+
id: crypto.randomUUID(),
|
|
683
|
+
email: input.email,
|
|
684
|
+
createdAt: new Date().toISOString(),
|
|
685
|
+
debugInfo: {
|
|
686
|
+
swappedAt: Date.now(),
|
|
687
|
+
inputValidated: true
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
if (deps.emitLog) {
|
|
692
|
+
await deps.emitLog({
|
|
693
|
+
timestamp: new Date(),
|
|
694
|
+
level: "info",
|
|
695
|
+
message: "🔍 DEBUG: User created successfully",
|
|
696
|
+
data: { result }
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return result;
|
|
701
|
+
}
|
|
702
|
+
"""
|
|
703
|
+
) {
|
|
704
|
+
success
|
|
705
|
+
error
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
#### TypeScript Support
|
|
711
|
+
|
|
712
|
+
The system supports full TypeScript syntax:
|
|
713
|
+
|
|
714
|
+
```graphql
|
|
715
|
+
mutation {
|
|
716
|
+
swapTask(
|
|
717
|
+
taskId: "data.processor"
|
|
718
|
+
runCode: """
|
|
719
|
+
async function run(input: { items: string[] }, deps: any): Promise<{ processed: number }> {
|
|
720
|
+
const items: string[] = input.items || [];
|
|
721
|
+
let processed: number = 0;
|
|
722
|
+
|
|
723
|
+
for (const item of items) {
|
|
724
|
+
if (typeof item === 'string' && item.length > 0) {
|
|
725
|
+
processed++;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return { processed };
|
|
730
|
+
}
|
|
731
|
+
"""
|
|
732
|
+
) {
|
|
733
|
+
success
|
|
734
|
+
error
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
#### Arrow Functions and Function Bodies
|
|
740
|
+
|
|
741
|
+
Multiple code formats are supported:
|
|
742
|
+
|
|
743
|
+
```graphql
|
|
744
|
+
# Arrow function
|
|
745
|
+
mutation {
|
|
746
|
+
swapTask(
|
|
747
|
+
taskId: "simple.task"
|
|
748
|
+
runCode: "() => ({ message: 'Hello from arrow function!' })"
|
|
749
|
+
) {
|
|
750
|
+
success
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
# Function body only
|
|
755
|
+
mutation {
|
|
756
|
+
swapTask(
|
|
757
|
+
taskId: "another.task"
|
|
758
|
+
runCode: """
|
|
759
|
+
const result = { timestamp: Date.now() };
|
|
760
|
+
return result;
|
|
761
|
+
"""
|
|
762
|
+
) {
|
|
763
|
+
success
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
### Live Telemetry Integration
|
|
769
|
+
|
|
770
|
+
Swapped functions can emit logs that are captured by the live telemetry system:
|
|
771
|
+
|
|
772
|
+
```graphql
|
|
773
|
+
# After swapping with debug logging, query the logs
|
|
774
|
+
query RecentDebugLogs {
|
|
775
|
+
live {
|
|
776
|
+
logs(last: 50, filter: { messageIncludes: "🔍 DEBUG" }) {
|
|
777
|
+
timestampMs
|
|
778
|
+
level
|
|
779
|
+
message
|
|
780
|
+
data
|
|
781
|
+
correlationId
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
### Programmatic Usage
|
|
788
|
+
|
|
789
|
+
Use the swap manager directly in your code:
|
|
790
|
+
|
|
791
|
+
```ts
|
|
792
|
+
import { resource } from "@bluelibs/runner";
|
|
793
|
+
import type { SwapManager } from "@bluelibs/runner-dev/dist/resources/swap.resource";
|
|
794
|
+
import { swapManager } from "@bluelibs/runner-dev/dist/resources/swap.resource";
|
|
795
|
+
|
|
796
|
+
export const debugger = resource({
|
|
797
|
+
id: "my.debugger",
|
|
798
|
+
dependencies: { swapManager },
|
|
799
|
+
async init(_c, { swapManager }: { swapManager: SwapManager }) {
|
|
800
|
+
// Swap a task programmatically
|
|
801
|
+
const result = await swapManager.swap("user.create", `
|
|
802
|
+
async function run(input, deps) {
|
|
803
|
+
console.log("Debug: Enhanced user creation");
|
|
804
|
+
return { id: "debug-user", ...input };
|
|
805
|
+
}
|
|
806
|
+
`);
|
|
807
|
+
|
|
808
|
+
if (result.success) {
|
|
809
|
+
console.log(`Task ${result.taskId} swapped successfully`);
|
|
810
|
+
} else {
|
|
811
|
+
console.error(`Swap failed: ${result.error}`);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Check what's currently swapped
|
|
815
|
+
const swappedTasks = swapManager.getSwappedTasks();
|
|
816
|
+
console.log(`Currently swapped: ${swappedTasks.map(t => t.taskId).join(', ')}`);
|
|
817
|
+
|
|
818
|
+
// Restore original function
|
|
819
|
+
await swapManager.unswap("user.create");
|
|
820
|
+
},
|
|
821
|
+
});
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
### Error Handling
|
|
825
|
+
|
|
826
|
+
The system provides comprehensive error handling:
|
|
827
|
+
|
|
828
|
+
```graphql
|
|
829
|
+
mutation {
|
|
830
|
+
swapTask(taskId: "nonexistent.task", runCode: "invalid javascript code {") {
|
|
831
|
+
success # false
|
|
832
|
+
error # "Task 'nonexistent.task' not found" or "Compilation failed: ..."
|
|
833
|
+
taskId # "nonexistent.task"
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
### Safety and Best Practices
|
|
839
|
+
|
|
840
|
+
#### Type Safety
|
|
841
|
+
|
|
842
|
+
- No `as any` usage throughout the implementation
|
|
843
|
+
- Full TypeScript type checking and compilation
|
|
844
|
+
- Comprehensive error validation and reporting
|
|
845
|
+
|
|
846
|
+
#### Security Considerations
|
|
847
|
+
|
|
848
|
+
- Code is executed via `eval()` in the Node.js context
|
|
849
|
+
- Intended for development/debugging environments only
|
|
850
|
+
- Swapped functions have access to the same context as original functions
|
|
851
|
+
|
|
852
|
+
#### Best Practices
|
|
853
|
+
|
|
854
|
+
- Use descriptive debug messages in swapped functions
|
|
855
|
+
- Leverage the logging system for telemetry capture
|
|
856
|
+
- Test swapped functions thoroughly before deployment
|
|
857
|
+
- Always restore original functions after debugging
|
|
858
|
+
|
|
859
|
+
#### Error Recovery
|
|
860
|
+
|
|
861
|
+
- Failed swaps don't affect the original function
|
|
862
|
+
- State tracking prevents inconsistencies
|
|
863
|
+
- Easy rollback with `unswapAllTasks` mutation
|
|
864
|
+
|
|
865
|
+
### AI Assistant Integration
|
|
866
|
+
|
|
867
|
+
This system is specifically designed for AI debugging workflows:
|
|
868
|
+
|
|
869
|
+
1. **AI analyzes application behavior** via introspection and live telemetry
|
|
870
|
+
2. **AI identifies issues** in specific tasks or functions
|
|
871
|
+
3. **AI generates enhanced debug code** with additional logging and validation
|
|
872
|
+
4. **AI swaps the function remotely** via GraphQL mutations
|
|
873
|
+
5. **AI monitors enhanced telemetry** to understand the issue
|
|
874
|
+
6. **AI restores original function** once debugging is complete
|
|
875
|
+
|
|
876
|
+
### Remote Task Execution
|
|
877
|
+
|
|
878
|
+
The system provides `invokeTask` functionality for remotely executing tasks with JSON input/output serialization, perfect for AI-driven debugging and testing.
|
|
879
|
+
|
|
880
|
+
#### Basic Task Invocation
|
|
881
|
+
|
|
882
|
+
```graphql
|
|
883
|
+
mutation {
|
|
884
|
+
invokeTask(taskId: "user.create") {
|
|
885
|
+
success
|
|
886
|
+
error
|
|
887
|
+
taskId
|
|
888
|
+
result
|
|
889
|
+
executionTimeMs
|
|
890
|
+
invocationId
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
#### Task Invocation with Input
|
|
896
|
+
|
|
897
|
+
```graphql
|
|
898
|
+
mutation {
|
|
899
|
+
invokeTask(
|
|
900
|
+
taskId: "user.create"
|
|
901
|
+
inputJson: "{\"email\": \"test@example.com\", \"name\": \"John Doe\"}"
|
|
902
|
+
) {
|
|
903
|
+
success
|
|
904
|
+
result
|
|
905
|
+
executionTimeMs
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
#### JavaScript Input Evaluation
|
|
911
|
+
|
|
912
|
+
For advanced debugging scenarios, use `evalInput: true` to evaluate JavaScript expressions instead of parsing JSON:
|
|
913
|
+
|
|
914
|
+
```graphql
|
|
915
|
+
mutation {
|
|
916
|
+
invokeTask(
|
|
917
|
+
taskId: "data.processor"
|
|
918
|
+
inputJson: """
|
|
919
|
+
{
|
|
920
|
+
timestamp: new Date("2023-01-01"),
|
|
921
|
+
data: [1, 2, 3].map(x => x * 2),
|
|
922
|
+
config: {
|
|
923
|
+
retries: Math.max(3, process.env.NODE_ENV === 'prod' ? 5 : 1),
|
|
924
|
+
timeout: 30 * 1000
|
|
925
|
+
},
|
|
926
|
+
processData: (items) => items.filter(x => x > 0)
|
|
927
|
+
}
|
|
928
|
+
"""
|
|
929
|
+
evalInput: true
|
|
930
|
+
) {
|
|
931
|
+
success
|
|
932
|
+
result
|
|
933
|
+
executionTimeMs
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
#### Pure Mode (Bypass Middleware)
|
|
939
|
+
|
|
940
|
+
Pure mode executes tasks with computed dependencies directly from the store, bypassing the middleware pipeline and authentication systems for clean testing:
|
|
941
|
+
|
|
942
|
+
```graphql
|
|
943
|
+
mutation {
|
|
944
|
+
invokeTask(
|
|
945
|
+
taskId: "user.create"
|
|
946
|
+
inputJson: "{\"email\": \"test@example.com\"}"
|
|
947
|
+
pure: true
|
|
948
|
+
) {
|
|
949
|
+
success
|
|
950
|
+
result
|
|
951
|
+
executionTimeMs
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
#### AI Debugging Workflow
|
|
957
|
+
|
|
958
|
+
1. **Swap task with enhanced debugging**:
|
|
959
|
+
|
|
960
|
+
```graphql
|
|
961
|
+
mutation {
|
|
962
|
+
swapTask(
|
|
963
|
+
taskId: "user.create"
|
|
964
|
+
runCode: """
|
|
965
|
+
async function run(input, deps) {
|
|
966
|
+
console.log('Input received:', input);
|
|
967
|
+
const result = { id: Math.random(), ...input };
|
|
968
|
+
console.log('Result generated:', result);
|
|
969
|
+
return result;
|
|
970
|
+
}
|
|
971
|
+
"""
|
|
972
|
+
) {
|
|
973
|
+
success
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
```
|
|
977
|
+
|
|
978
|
+
2. **Invoke task to test behavior**:
|
|
979
|
+
|
|
980
|
+
```graphql
|
|
981
|
+
mutation {
|
|
982
|
+
invokeTask(
|
|
983
|
+
taskId: "user.create"
|
|
984
|
+
inputJson: """
|
|
985
|
+
{
|
|
986
|
+
email: "debug@test.com",
|
|
987
|
+
createdAt: new Date(),
|
|
988
|
+
metadata: {
|
|
989
|
+
source: "ai-debug",
|
|
990
|
+
sessionId: crypto.randomUUID(),
|
|
991
|
+
testData: [1, 2, 3].map(x => x * 10)
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
"""
|
|
995
|
+
pure: true
|
|
996
|
+
# Activation of eval() for smarter inputs.
|
|
997
|
+
evalInput: true
|
|
998
|
+
) {
|
|
999
|
+
success
|
|
1000
|
+
result
|
|
1001
|
+
executionTimeMs
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
```
|
|
1005
|
+
|
|
1006
|
+
3. **Monitor live telemetry for debug output**:
|
|
1007
|
+
|
|
1008
|
+
```graphql
|
|
1009
|
+
query {
|
|
1010
|
+
live {
|
|
1011
|
+
logs(last: 10) {
|
|
1012
|
+
level
|
|
1013
|
+
message
|
|
1014
|
+
data
|
|
1015
|
+
timestampMs
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
#### JSON Serialization
|
|
1022
|
+
|
|
1023
|
+
The system automatically handles complex JavaScript types:
|
|
1024
|
+
|
|
1025
|
+
- **Primitives**: strings, numbers, booleans preserved exactly
|
|
1026
|
+
- **Objects/Arrays**: Deep serialization with proper structure
|
|
1027
|
+
- **Functions**: Converted to `[Function: name]` strings
|
|
1028
|
+
- **Undefined**: Converted to `[undefined]` strings
|
|
1029
|
+
- **Dates**: Serialized as ISO strings
|
|
1030
|
+
- **Circular References**: Safely handled to prevent errors
|
|
1031
|
+
|
|
1032
|
+
#### Input Processing Modes
|
|
1033
|
+
|
|
1034
|
+
**JSON Mode (default)**: `evalInput: false`
|
|
1035
|
+
|
|
1036
|
+
- Input is parsed as JSON using `JSON.parse()`
|
|
1037
|
+
- Safe for structured data
|
|
1038
|
+
- Limited to JSON-compatible types
|
|
1039
|
+
|
|
1040
|
+
**JavaScript Evaluation Mode**: `evalInput: true`
|
|
1041
|
+
|
|
1042
|
+
- Input is evaluated as JavaScript using `eval()`
|
|
1043
|
+
- Supports complex expressions, function calls, Date objects, calculations
|
|
1044
|
+
- Full access to JavaScript runtime and built-in objects
|
|
1045
|
+
- Perfect for AI-driven testing with dynamic inputs
|
|
1046
|
+
|
|
1047
|
+
### Arbitrary Code Evaluation
|
|
1048
|
+
|
|
1049
|
+
For advanced debugging, the system provides an `eval` mutation to execute arbitrary JavaScript/TypeScript code on the server.
|
|
1050
|
+
|
|
1051
|
+
**⚠️ Security Warning**: This feature is powerful and executes code with the same privileges as the application. It is intended for development environments only and is disabled by default in production. To enable it, set the environment variable `RUNNER_DEV_EVAL=1`.
|
|
1052
|
+
|
|
1053
|
+
#### `eval` Mutation
|
|
1054
|
+
|
|
1055
|
+
**Execute arbitrary code:**
|
|
1056
|
+
|
|
1057
|
+
```graphql
|
|
1058
|
+
mutation EvalCode($code: String!, $inputJson: String, $evalInput: Boolean) {
|
|
1059
|
+
eval(code: $code, inputJson: $inputJson, evalInput: $evalInput) {
|
|
1060
|
+
success
|
|
1061
|
+
error
|
|
1062
|
+
result # JSON string
|
|
1063
|
+
executionTimeMs
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
```
|
|
1067
|
+
|
|
1068
|
+
- `code`: The JavaScript/TypeScript code to execute.
|
|
1069
|
+
- `inputJson`: Optional input string, parsed as JSON by default.
|
|
1070
|
+
- `evalInput`: If `true`, `inputJson` is evaluated as a JavaScript expression.
|
|
1071
|
+
|
|
1072
|
+
**Example:**
|
|
1073
|
+
|
|
1074
|
+
```graphql
|
|
1075
|
+
mutation {
|
|
1076
|
+
eval(code: "return { a: 1, b: process.version }") {
|
|
1077
|
+
success
|
|
1078
|
+
result
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
```
|
|
1082
|
+
|
|
1083
|
+
### Use Cases
|
|
1084
|
+
|
|
1085
|
+
- **Production Debugging**: Add logging to specific functions without restarts
|
|
1086
|
+
- **A/B Testing**: Compare different function implementations live
|
|
1087
|
+
- **Performance Monitoring**: Inject performance measurements
|
|
1088
|
+
- **Error Investigation**: Add error handling and detailed logging
|
|
1089
|
+
- **Feature Development**: Test new logic before permanent implementation
|
|
1090
|
+
- **AI-Driven Debugging**: Enable AI assistants to debug applications autonomously
|
|
1091
|
+
|
|
1092
|
+
### Architecture
|
|
1093
|
+
|
|
1094
|
+
The hot-swapping system consists of:
|
|
1095
|
+
|
|
1096
|
+
- **SwapManager Resource** (`src/resources/swap.resource.ts`): Core swapping logic
|
|
1097
|
+
- **GraphQL Types** (`src/schema/types/SwapType.ts`): Type definitions for GraphQL API
|
|
1098
|
+
- **GraphQL Mutations** (`src/schema/mutation.ts`): Remote swap operations
|
|
1099
|
+
- **TypeScript Compiler**: Automatic code compilation and validation
|
|
1100
|
+
- **State Management**: Tracking of swapped functions and original code
|
|
1101
|
+
- **Error Handling**: Comprehensive validation and recovery mechanisms
|
|
1102
|
+
|
|
1103
|
+
The implementation maintains 100% type safety and provides extensive test coverage with both unit tests and GraphQL integration tests.
|