Skip to content

Commit b4d0aba

Browse files
committed
feat(data-inspector): example source inspects the environment, opt-out only
The built-in example source is now always registered unless explicitly opted out (option exampleSource: false; CLI --no-example; agent env DEVFRAME_DATA_INSPECTOR_EXAMPLE=0) — the agent and file-serving CLI no longer suppress it automatically. Beyond the playground branch it now answers real questions: the devframe context (registered RPC functions, services, storage dirs), OS info, and live process stats via query-time getters, with matching suggested queries.
1 parent 46e5bd8 commit b4d0aba

8 files changed

Lines changed: 130 additions & 42 deletions

File tree

docs/plugins/data-inspector.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Package: `@devframes/plugin-data-inspector` · framework: **Vue + Vite**
1616
- **Filters** — exclude functions, `_`-prefixed, or `$`-prefixed properties from results and skeleton alike.
1717
- **Saved queries** — recipes (`query` + optional title/description + the filters they were authored with), id-keyed, in two scopes: **workspace** (committable, shared with the team) and **project** (per-checkout).
1818

19-
A built-in example source demonstrates all of it on first run; disable it with `createDataInspectorDevframe({ exampleSource: false })` once your own sources are registered.
19+
A built-in **example source** is always registered alongside your own: it exposes the devframe context (registered RPC functions, services, storage dirs), OS info, and live process stats — with query-time getters that change on every re-run — plus a small playground branch exercising every viewer capability. Opt out with `createDataInspectorDevframe({ exampleSource: false })` (CLI: `--no-example`, agent: `DEVFRAME_DATA_INSPECTOR_EXAMPLE=0`).
2020

2121
## Providing data sources
2222

plugins/data-inspector/src/agent/index.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ export interface ExposeDataInspectorOptions {
6262
discoveryFile?: boolean
6363
/** Suppress the stderr banner. */
6464
silent?: boolean
65+
/**
66+
* Register the built-in example source (devframe context, OS, live
67+
* process stats of the target). Default `true`; set `false` (or
68+
* `DEVFRAME_DATA_INSPECTOR_EXAMPLE=0` on the `--import` path) to expose
69+
* only the target's own registrations.
70+
*/
71+
exampleSource?: boolean
6572
}
6673

6774
export interface DataInspectorAgent {
@@ -94,9 +101,7 @@ export async function exposeDataInspector(options: ExposeDataInspectorOptions =
94101
}
95102

96103
const context: DevframeNodeContext = await createHostContext({ cwd, mode: 'dev', host })
97-
// Attach exists to inspect the target's own objects; the example source
98-
// would only be noise next to them.
99-
setupDataInspector(context, { exampleSource: false })
104+
setupDataInspector(context, { exampleSource: options.exampleSource })
100105

101106
const useAuth = options.auth ?? true
102107
const token = useAuth ? (options.token ?? randomToken()) : undefined
@@ -148,6 +153,7 @@ if (process.env.DEVFRAME_DATA_INSPECTOR === '1' || process.env.DEVFRAME_DATA_INS
148153
port: process.env.DEVFRAME_DATA_INSPECTOR_PORT ? Number(process.env.DEVFRAME_DATA_INSPECTOR_PORT) : undefined,
149154
auth: process.env.DEVFRAME_DATA_INSPECTOR_AUTH !== '0',
150155
token: process.env.DEVFRAME_DATA_INSPECTOR_TOKEN,
156+
exampleSource: process.env.DEVFRAME_DATA_INSPECTOR_EXAMPLE !== '0',
151157
}).catch((error) => {
152158
console.error('[data-inspector] agent failed to start:', error)
153159
})

plugins/data-inspector/src/cli.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,11 @@ export function createDataInspectorCli() {
5151
.option('--host <host>', 'Host to bind to', { default: 'localhost' })
5252
.option('--open', 'Open the browser on start')
5353
.option('--no-open', 'Do not open the browser')
54-
.action(async (files: string[], flags: { port?: number, host: string, open?: boolean }) => {
54+
.option('--no-example', 'Skip the built-in example source')
55+
.action(async (files: string[], flags: { port?: number, host: string, open?: boolean, example?: boolean }) => {
5556
for (const file of files)
5657
registerDataSource(createFileDataSource(file))
57-
// With files given, they are the point; the example source only backs
58-
// the empty first-run.
59-
const def = createDataInspectorDevframe({ exampleSource: files.length === 0 })
58+
const def = createDataInspectorDevframe({ exampleSource: flags.example })
6059
await createDevServer(def, {
6160
host: flags.host,
6261
port: flags.port ? Number(flags.port) : undefined,

plugins/data-inspector/src/node/example-source.ts

Lines changed: 82 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import type { DevframeNodeContext } from 'devframe/types'
12
import type { DataSourceEntry } from '../registry/index'
3+
import os from 'node:os'
24
import process from 'node:process'
35

46
/** Id of the built-in example source (namespaced like every source id). */
@@ -14,22 +16,17 @@ class RequestLog {
1416
])
1517
}
1618

17-
function createExampleData(): Record<string, unknown> {
18-
const log = new RequestLog()
19+
/** A small synthetic branch exercising every viewer capability. */
20+
function createPlayground(): Record<string, unknown> {
1921
const node: Record<string, unknown> = { name: 'root' }
2022
node.self = node // circular structures render as $ref markers
21-
22-
const data: Record<string, unknown> = {
23-
server: {
24-
name: 'example',
25-
port: 5173,
26-
tags: new Set(['dev', 'local', 'example']),
27-
middlewares: [
28-
{ name: 'compression', handler: () => {} },
29-
{ name: 'static', handler: () => {} },
30-
],
31-
},
32-
requests: log,
23+
return {
24+
requests: new RequestLog(),
25+
tags: new Set(['dev', 'local', 'example']),
26+
middlewares: [
27+
{ name: 'compression', handler: () => {} },
28+
{ name: 'static', handler: () => {} },
29+
],
3330
build: {
3431
hash: 'c3f7c4e',
3532
size: 1024n * 512n,
@@ -40,9 +37,51 @@ function createExampleData(): Record<string, unknown> {
4037
})),
4138
},
4239
tree: node,
43-
process: { pid: process.pid, node: process.version, platform: process.platform },
4440
}
45-
// An own getter: reads at query time, so re-running shows the data is live.
41+
}
42+
43+
/** The devframe context, projected to its inspectable surface. */
44+
function describeContext(ctx: DevframeNodeContext): Record<string, unknown> {
45+
return {
46+
cwd: ctx.cwd,
47+
workspaceRoot: ctx.workspaceRoot,
48+
mode: ctx.mode,
49+
rpcFunctions: Array.from(ctx.rpc.definitions.keys()).sort(),
50+
services: ctx.services.keys().sort(),
51+
storage: {
52+
workspace: ctx.host.getStorageDir('workspace'),
53+
project: ctx.host.getStorageDir('project'),
54+
global: ctx.host.getStorageDir('global'),
55+
},
56+
}
57+
}
58+
59+
function createExampleData(ctx?: DevframeNodeContext): Record<string, unknown> {
60+
const data: Record<string, unknown> = {
61+
...(ctx ? { devframe: describeContext(ctx) } : {}),
62+
os: {
63+
platform: os.platform(),
64+
arch: os.arch(),
65+
release: os.release(),
66+
hostname: os.hostname(),
67+
cpus: { count: os.cpus().length, model: os.cpus()[0]?.model },
68+
memory: { totalMb: Math.round(os.totalmem() / 1024 / 1024), freeMb: Math.round(os.freemem() / 1024 / 1024) },
69+
homedir: os.homedir(),
70+
uptimeHours: Math.round(os.uptime() / 36) / 100,
71+
},
72+
process: {
73+
pid: process.pid,
74+
execPath: process.execPath,
75+
argv: process.argv,
76+
versions: process.versions,
77+
},
78+
playground: createPlayground(),
79+
}
80+
// Own getters read at query time, so re-running shows the data is live.
81+
Object.defineProperties(data.process as object, {
82+
memory: { enumerable: true, get: () => process.memoryUsage() },
83+
uptimeSeconds: { enumerable: true, get: () => Math.round(process.uptime()) },
84+
})
4685
Object.defineProperty(data, 'queriedAt', {
4786
enumerable: true,
4887
get: () => new Date().toISOString(),
@@ -51,36 +90,47 @@ function createExampleData(): Record<string, unknown> {
5190
}
5291

5392
/**
54-
* The built-in example source — a small live graph exercising everything the
55-
* viewer can show (Maps, Sets, class instances, circulars, BigInt, functions,
56-
* a query-time getter), with suggested queries as starting points. Sources
57-
* registered by your plugins and hosts appear next to it; disable it via
58-
* `createDataInspectorDevframe({ exampleSource: false })`.
93+
* The built-in example source — always registered unless opted out
94+
* (`exampleSource: false`). Lets the viewer query simple environment info
95+
* (the devframe context: registered RPC functions, services, storage dirs;
96+
* OS and live process stats) plus a small playground branch exercising
97+
* everything the viewer can show (Maps, Sets, class instances, circulars,
98+
* BigInt, functions, query-time getters).
5999
*/
60-
export function createExampleDataSource(): DataSourceEntry {
100+
export function createExampleDataSource(ctx?: DevframeNodeContext): DataSourceEntry {
61101
let data: Record<string, unknown> | undefined
62102
return {
63103
id: EXAMPLE_SOURCE_ID,
64104
title: 'Example data',
65-
description: 'A built-in playground graph. Register your own sources to inspect real objects.',
105+
description: 'Devframe context, OS and process info, plus a playground graph.',
66106
icon: 'i-ph:flask-duotone',
67-
data: () => data ??= createExampleData(),
107+
data: () => data ??= createExampleData(ctx),
68108
queries: [
109+
...(ctx
110+
? [{
111+
title: 'Registered RPC functions',
112+
description: 'Everything callable on this devframe connection',
113+
query: 'devframe.rpcFunctions',
114+
}]
115+
: []),
116+
{
117+
title: 'OS at a glance',
118+
query: 'os.({ platform, arch, cpus, memory })',
119+
},
120+
{
121+
title: 'Live process memory',
122+
description: 'Query-time getter: re-run and watch it change',
123+
query: '{ memory: process.memory, uptime: process.uptimeSeconds, at: queriedAt }',
124+
},
69125
{
70126
title: 'Busiest endpoints',
71127
description: 'Live Map entries, sorted',
72-
query: 'requests.entries.mapEntries().value.sort(hits desc)',
128+
query: 'playground.requests.entries.mapEntries().value.sort(hits desc)',
73129
},
74130
{
75131
title: 'Large modules as a table',
76132
description: 'Filter and project an array',
77-
query: 'build.modules.[sizeKb > 80].({ id, sizeKb })',
78-
},
79-
{
80-
title: 'Server config (data only)',
81-
description: 'Filters strip the middleware functions',
82-
query: 'server',
83-
excludeFunctions: true,
133+
query: 'playground.build.modules.[sizeKb > 80].({ id, sizeKb })',
84134
},
85135
{ title: 'Everything', query: '' },
86136
],

plugins/data-inspector/src/node/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ export function setupDataInspector(ctx: DevframeNodeContext, options: SetupDataI
2828
if (!ctx.services.has(DATA_SOURCES_SERVICE_ID))
2929
ctx.services.provide(DATA_SOURCES_SERVICE_ID, createDataSourcesService())
3030

31+
// Always on unless opted out: the example source doubles as an
32+
// environment inspector (this context, OS, live process stats).
3133
if ((options.exampleSource ?? true) && !getDataSource(EXAMPLE_SOURCE_ID))
32-
registerDataSource(createExampleDataSource())
34+
registerDataSource(createExampleDataSource(ctx))
3335

3436
onDataSourcesChanged(() => {
3537
ctx.rpc.broadcast(SOURCES_CHANGED_EVENT as never)

plugins/data-inspector/test/registry.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,34 @@ describe('example source', () => {
107107
expect(out.ok, `suggested query "${recipe.title}" must run`).toBe(true)
108108
}
109109
})
110+
111+
it('exposes os and live process info', async () => {
112+
const { createExampleDataSource } = await import('../src/node/example-source')
113+
const entry = createExampleDataSource()
114+
const data = await resolveSourceData(entry) as Record<string, any>
115+
expect(data.os.platform).toBeTypeOf('string')
116+
expect(data.os.cpus.count).toBeGreaterThan(0)
117+
expect(data.process.memory.heapUsed).toBeGreaterThan(0)
118+
expect(data.process.versions.node).toBeTypeOf('string')
119+
})
120+
121+
it('describes the devframe context when given one', async () => {
122+
const { createExampleDataSource } = await import('../src/node/example-source')
123+
const ctx = {
124+
cwd: '/w/app',
125+
workspaceRoot: '/w',
126+
mode: 'dev',
127+
rpc: { definitions: new Map([['a:fn', {}]]) },
128+
services: { keys: () => ['a:svc'] },
129+
host: { getStorageDir: (scope: string) => `/w/${scope}` },
130+
} as any
131+
const data = await resolveSourceData(createExampleDataSource(ctx)) as Record<string, any>
132+
expect(data.devframe).toMatchObject({
133+
cwd: '/w/app',
134+
mode: 'dev',
135+
rpcFunctions: ['a:fn'],
136+
services: ['a:svc'],
137+
storage: { workspace: '/w/workspace', project: '/w/project', global: '/w/global' },
138+
})
139+
})
110140
})

tests/__snapshots__/tsnapi/@devframes/plugin-data-inspector/agent.snapshot.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface ExposeDataInspectorOptions {
1919
token?: string;
2020
discoveryFile?: boolean;
2121
silent?: boolean;
22+
exampleSource?: boolean;
2223
}
2324
// #endregion
2425

tests/__snapshots__/tsnapi/@devframes/plugin-data-inspector/node.snapshot.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export interface SetupDataInspectorOptions {
88
// #endregion
99

1010
// #region Functions
11-
export declare function createExampleDataSource(): DataSourceEntry;
11+
export declare function createExampleDataSource(_?: DevframeNodeContext): DataSourceEntry;
1212
export declare function setupDataInspector(_: DevframeNodeContext, _?: SetupDataInspectorOptions): void;
1313
// #endregion
1414

0 commit comments

Comments
 (0)