@quazardous/qdadm 2.13.0 → 2.15.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.
Files changed (50) hide show
  1. package/README.md +1 -0
  2. package/package.json +6 -4
  3. package/src/chain/index.ts +5 -0
  4. package/src/components/InfoBanner.vue +3 -3
  5. package/src/components/base/QdButton.vue +20 -0
  6. package/src/components/base/QdMessage.vue +17 -0
  7. package/src/components/dialogs/MultiStepDialog.vue +5 -5
  8. package/src/components/dialogs/SimpleDialog.vue +4 -4
  9. package/src/components/dialogs/UnsavedChangesDialog.vue +2 -2
  10. package/src/components/display/CopyableId.vue +2 -2
  11. package/src/components/edit/FormActions.vue +4 -4
  12. package/src/components/edit/FormPage.vue +7 -7
  13. package/src/components/edit/LookupPickerDialog.vue +3 -3
  14. package/src/components/editors/JsonEditorFoldable.vue +8 -8
  15. package/src/components/editors/JsonStructuredField.vue +3 -3
  16. package/src/components/editors/KeyValueEditor.vue +3 -3
  17. package/src/components/editors/LanguageEditor.vue +5 -5
  18. package/src/components/editors/ScopeEditor.vue +3 -3
  19. package/src/components/index.ts +4 -0
  20. package/src/components/layout/AppLayout.vue +2 -2
  21. package/src/components/layout/PageLayout.vue +2 -2
  22. package/src/components/layout/defaults/DefaultUserInfo.vue +2 -2
  23. package/src/components/lists/ActionButtons.vue +2 -2
  24. package/src/components/lists/ListPage.vue +4 -4
  25. package/src/components/pages/LoginPage.vue +2 -2
  26. package/src/components/show/ShowPage.vue +6 -6
  27. package/src/composables/useEntityItemShowPage.ts +8 -0
  28. package/src/composables/useListPage.ts +23 -0
  29. package/src/composables/useListPage.types.ts +3 -0
  30. package/src/composables/useLiveEntity.ts +115 -0
  31. package/src/deferred/index.ts +5 -0
  32. package/src/entity/EntityManager.cache.ts +20 -0
  33. package/src/entity/EntityManager.interface.ts +3 -0
  34. package/src/entity/EntityManager.ts +29 -1
  35. package/src/entity/EntityManager.types.ts +32 -0
  36. package/src/gen/vite-plugin.ts +4 -0
  37. package/src/index.ts +10 -0
  38. package/src/kernel/Kernel.registries.ts +31 -5
  39. package/src/kernel/Kernel.ts +3 -0
  40. package/src/kernel/Kernel.types.ts +31 -0
  41. package/src/kernel/LiveEntityRouter.ts +189 -0
  42. package/src/kernel/SSEBridge.ts +3 -0
  43. package/src/query/index.ts +7 -0
  44. package/src/security/pages/RoleList.vue +3 -3
  45. package/src/types/index.ts +2 -0
  46. package/src/utils/pluralize.ts +472 -0
  47. package/src/vite/qdadmDebugPlugin.ts +178 -2
  48. package/src/vite/qdadmPlugin.ts +5 -19
  49. package/src/vite-env.d.ts +0 -16
  50. package/src/types/pluralize.d.ts +0 -19
@@ -34,9 +34,22 @@ interface PendingRequest {
34
34
  timeout: ReturnType<typeof setTimeout>
35
35
  }
36
36
 
37
+ /** Typed page-side queries (#1398) — served by the injected client. */
38
+ export type DebugQueryType =
39
+ | 'describe'
40
+ | 'dump'
41
+ | 'call'
42
+ | 'sessionInfo'
43
+ | 'routes'
44
+ | 'entityState'
45
+ | 'entityCall'
46
+ | 'storageDump'
47
+ | 'bootlog'
48
+ | 'recentSignals'
49
+
37
50
  interface DebugRequest {
38
51
  id: string
39
- type: 'describe' | 'dump' | 'call'
52
+ type: DebugQueryType
40
53
  payload?: unknown
41
54
  /** Target a specific session (server → client). */
42
55
  sessionId?: string
@@ -67,6 +80,26 @@ interface SessionState {
67
80
  meta: Record<string, unknown>
68
81
  }
69
82
 
83
+ /** Broker surface exposed to sibling plugins via `plugin.api` (#1398). */
84
+ export interface QdadmDebugPluginApi {
85
+ /** Send a typed query to the page and await its reply. */
86
+ ask: (type: DebugQueryType, payload?: unknown, sessionId?: string) => Promise<unknown>
87
+ /** Resolve a session param ('latest' or id) to a live session, if any. */
88
+ pickSession: (
89
+ sessionParam: string | null
90
+ ) => { id: string; lastSeenAt: number; meta: Record<string, unknown> } | null
91
+ listSessions: () => Array<{
92
+ id: string
93
+ firstSeenAt: number
94
+ lastSeenAt: number
95
+ ageMs: number
96
+ hasDescribe: boolean
97
+ hasSnapshot: boolean
98
+ meta: Record<string, unknown>
99
+ }>
100
+ prefix: string
101
+ }
102
+
70
103
  export interface QdadmDebugPluginOptions {
71
104
  /** URL prefix (default '/__qdadm') */
72
105
  prefix?: string
@@ -203,9 +236,21 @@ export function qdadmDebugPlugin(options: QdadmDebugPluginOptions = {}): Plugin
203
236
  return s
204
237
  }
205
238
 
239
+ /** Inter-plugin API (#1398) — consumed by @quazardous/qdmcp. */
240
+ const api: QdadmDebugPluginApi = {
241
+ ask,
242
+ pickSession: (sessionParam) => {
243
+ const s = pickSession(sessionParam)
244
+ return s ? { id: s.id, lastSeenAt: s.lastSeenAt, meta: s.meta } : null
245
+ },
246
+ listSessions,
247
+ prefix,
248
+ }
249
+
206
250
  return {
207
251
  name: 'qdadm-debug',
208
252
  apply: 'serve',
253
+ api,
209
254
 
210
255
  transformIndexHtml: {
211
256
  order: 'pre',
@@ -294,7 +339,10 @@ export function qdadmDebugPlugin(options: QdadmDebugPluginOptions = {}): Plugin
294
339
  res.setHeader('x-qdadm-session', session.id)
295
340
  return sendJson(res, 200, data)
296
341
  }
297
- return sendJson(res, 404, { error: `unknown ${req.method} ${path}` })
342
+ // Unknown subpath: fall through — sibling plugins (e.g.
343
+ // @quazardous/qdadm-mcp) mount their own endpoints under the
344
+ // same prefix (#1398).
345
+ return next()
298
346
  } catch (e) {
299
347
  return sendJson(res, 500, { error: (e as Error).message })
300
348
  }
@@ -333,6 +381,34 @@ function readBody(req: IncomingMessage): Promise<string> {
333
381
  * stays fresh. Listens for ad-hoc requests targeted at this session.
334
382
  */
335
383
  const HMR_CLIENT_SCRIPT = String.raw`
384
+ // ── Boot capture (#1398): this head module runs BEFORE the app's entry
385
+ // module, so console/window errors from the very first app statement are
386
+ // buffered — visible over MCP even when the app dies before the bridge
387
+ // exists (the class of failure a bridge-only design can never see).
388
+ const __bootAt = Date.now()
389
+ const __bootlog = { errors: [], warns: [], pageErrors: [], rejections: [] }
390
+ function __cap(arr, entry) { arr.push(entry); if (arr.length > 200) arr.shift() }
391
+ function __fmt(args) {
392
+ try {
393
+ return Array.from(args).map((a) => {
394
+ if (a instanceof Error) return a.stack || a.message
395
+ if (typeof a === 'object') { try { return JSON.stringify(a) } catch (e) { return String(a) } }
396
+ return String(a)
397
+ }).join(' ').slice(0, 2000)
398
+ } catch (e) { return '[unformattable]' }
399
+ }
400
+ const __origError = console.error.bind(console)
401
+ const __origWarn = console.warn.bind(console)
402
+ console.error = function () { __cap(__bootlog.errors, { at: Date.now(), msg: __fmt(arguments) }); __origError.apply(null, arguments) }
403
+ console.warn = function () { __cap(__bootlog.warns, { at: Date.now(), msg: __fmt(arguments) }); __origWarn.apply(null, arguments) }
404
+ window.addEventListener('error', (e) => {
405
+ __cap(__bootlog.pageErrors, { at: Date.now(), msg: String(e.message || e), source: e.filename ? e.filename + ':' + e.lineno : undefined })
406
+ })
407
+ window.addEventListener('unhandledrejection', (e) => {
408
+ let msg; try { msg = e.reason && (e.reason.stack || e.reason.message) || String(e.reason) } catch (err) { msg = '[unreadable reason]' }
409
+ __cap(__bootlog.rejections, { at: Date.now(), msg: String(msg).slice(0, 2000) })
410
+ })
411
+
336
412
  const hot = import.meta.hot
337
413
  if (!hot) {
338
414
  console.warn('[qdadm-debug] import.meta.hot unavailable in injected script')
@@ -344,6 +420,95 @@ if (!hot) {
344
420
 
345
421
  const w = /** @type {any} */ (window)
346
422
 
423
+ // Signal ring buffer — armed once __qdadm.signals is reachable.
424
+ const __signals = []
425
+ let __signalsArmed = false
426
+ function __armSignals() {
427
+ if (__signalsArmed) return
428
+ const q = w.__qdadm
429
+ if (!q || !q.signals || typeof q.signals.on !== 'function') return
430
+ try {
431
+ q.signals.on('**', (event) => { __cap(__signals, { at: Date.now(), name: event && event.name }) ; if (__signals.length > 100) __signals.shift() })
432
+ __signalsArmed = true
433
+ } catch (e) {}
434
+ }
435
+
436
+ function __sessionInfo() {
437
+ const q = w.__qdadm || {}
438
+ let app = {}
439
+ try { app = (q.kernel && q.kernel.options && q.kernel.options.app) || {} } catch (e) {}
440
+ let route = null
441
+ try { const r = q.router && q.router.currentRoute && q.router.currentRoute.value; route = r ? { name: r.name, fullPath: r.fullPath } : null } catch (e) {}
442
+ return {
443
+ sessionId, bootAt: __bootAt, now: Date.now(), url: location.href,
444
+ app: { name: app.name, version: typeof app.version === 'function' ? app.version() : app.version },
445
+ route, bridgeReady: !!bridge, signalsArmed: __signalsArmed,
446
+ bootlogCounts: { errors: __bootlog.errors.length, warns: __bootlog.warns.length, pageErrors: __bootlog.pageErrors.length, rejections: __bootlog.rejections.length },
447
+ }
448
+ }
449
+
450
+ function __routes() {
451
+ const q = w.__qdadm
452
+ if (!q || !q.router) throw new Error('router not ready')
453
+ return q.router.getRoutes().map((r) => ({
454
+ name: r.name, path: r.path,
455
+ meta: { entity: r.meta && r.meta.entity, layout: r.meta && r.meta.layout, requiresAuth: r.meta && r.meta.requiresAuth, public: r.meta && r.meta.public },
456
+ }))
457
+ }
458
+
459
+ function __manager(entity) {
460
+ const q = w.__qdadm
461
+ if (!q || !q.orchestrator) throw new Error('orchestrator not ready')
462
+ if (!q.orchestrator.isRegistered(entity)) throw new Error('entity not registered: ' + entity + ' (known: ' + q.orchestrator.getRegisteredNames().join(', ') + ')')
463
+ return q.orchestrator.get(entity)
464
+ }
465
+
466
+ function __entityState(payload) {
467
+ const q = w.__qdadm
468
+ if (!payload || !payload.entity) {
469
+ return { entities: q && q.orchestrator ? q.orchestrator.getRegisteredNames() : [] }
470
+ }
471
+ const m = __manager(payload.entity)
472
+ const safe = (fn) => { try { return fn() } catch (e) { return '[error: ' + (e && e.message) + ']' } }
473
+ const fields = {}
474
+ try {
475
+ const fs = m.fields || m._fields || {}
476
+ for (const k in fs) fields[k] = { type: fs[k] && fs[k].type, label: fs[k] && fs[k].label, required: !!(fs[k] && fs[k].required) }
477
+ } catch (e) {}
478
+ return {
479
+ entity: payload.entity,
480
+ idField: safe(() => m.idField), labelField: safe(() => m.labelField),
481
+ label: safe(() => m.label), labelPlural: safe(() => m.labelPlural),
482
+ can: { create: safe(() => m.canCreate()), read: safe(() => m.canRead()), update: safe(() => m.canUpdate()), delete: safe(() => m.canDelete()) },
483
+ fields,
484
+ storage: safe(() => { const st = m.storage || m._storage; return st ? { kind: st.constructor && st.constructor.name, storageKey: st._storageKey } : null }),
485
+ }
486
+ }
487
+
488
+ async function __entityCall(payload) {
489
+ if (!payload || !payload.entity || !payload.op) throw new Error('entityCall requires { entity, op, ... }')
490
+ const m = __manager(payload.entity)
491
+ const op = payload.op
492
+ if (op === 'list') return await m.list(payload.params || {})
493
+ if (op === 'get') return await m.get(payload.id)
494
+ if (op === 'create') return await m.create(payload.data || {})
495
+ if (op === 'update') return await m.update(payload.id, payload.data || {})
496
+ if (op === 'delete') { await m.delete(payload.id); return { deleted: payload.id } }
497
+ throw new Error('unknown op "' + op + '" (list|get|create|update|delete)')
498
+ }
499
+
500
+ function __storageDump(payload) {
501
+ if (!payload || !payload.entity) throw new Error('storageDump requires { entity }')
502
+ const m = __manager(payload.entity)
503
+ const st = m.storage || m._storage
504
+ const key = st && st._storageKey
505
+ if (!key) return { storageKey: null, note: 'storage exposes no localStorage key (' + (st && st.constructor && st.constructor.name) + ')' }
506
+ const raw = localStorage.getItem(key)
507
+ let parsed = null
508
+ try { parsed = raw ? JSON.parse(raw) : null } catch (e) { parsed = '[unparsable]' }
509
+ return { storageKey: key, count: Array.isArray(parsed) ? parsed.length : null, data: parsed }
510
+ }
511
+
347
512
  function helloIfNeeded() {
348
513
  try { hot.send('qdadm:debug:push', { type: 'hello', sessionId, meta }) } catch (e) {}
349
514
  }
@@ -365,6 +530,16 @@ if (!hot) {
365
530
  if (msg && msg.sessionId && msg.sessionId !== sessionId) return
366
531
  const id = (msg && msg.id) || ''
367
532
  try {
533
+ // Typed queries (#1398): bridge-independent — they must answer even
534
+ // when the app died before the bridge existed.
535
+ if (msg.type === 'bootlog') { hot.send('qdadm:debug:reply', { id, ok: true, data: __bootlog, sessionId }); return }
536
+ if (msg.type === 'sessionInfo') { hot.send('qdadm:debug:reply', { id, ok: true, data: __sessionInfo(), sessionId }); return }
537
+ if (msg.type === 'recentSignals') { __armSignals(); hot.send('qdadm:debug:reply', { id, ok: true, data: { armed: __signalsArmed, events: __signals }, sessionId }); return }
538
+ if (msg.type === 'routes') { hot.send('qdadm:debug:reply', { id, ok: true, data: __routes(), sessionId }); return }
539
+ if (msg.type === 'entityState') { hot.send('qdadm:debug:reply', { id, ok: true, data: __entityState(msg.payload), sessionId }); return }
540
+ if (msg.type === 'entityCall') { const data = await __entityCall(msg.payload); hot.send('qdadm:debug:reply', { id, ok: true, data, sessionId }); return }
541
+ if (msg.type === 'storageDump') { hot.send('qdadm:debug:reply', { id, ok: true, data: __storageDump(msg.payload), sessionId }); return }
542
+
368
543
  const b = w.__qdadm && w.__qdadm.debug && w.__qdadm.debug.bridge
369
544
  if (!b) throw new Error('debug bridge not ready')
370
545
  let data
@@ -385,6 +560,7 @@ if (!hot) {
385
560
  bridge = w.__qdadm && w.__qdadm.debug && w.__qdadm.debug.bridge
386
561
  if (!bridge) return
387
562
  clearInterval(attachTimer)
563
+ __armSignals()
388
564
  helloIfNeeded()
389
565
  pushDescribe()
390
566
  pushSnapshot()
@@ -23,13 +23,11 @@
23
23
  * - `optimizeDeps.exclude` keeps primevue/@primeuix/themes/qdadm out of the
24
24
  * pre-bundle so BOTH pipelines resolve the same raw modules. Excluding
25
25
  * qdadm also keeps HMR sane when the package is consumed via a symlink.
26
- * - `optimizeDeps.include` pre-bundles qdadm's CJS transitives (currently
27
- * `pluralize`), which cannot be served raw as ESM. The list lives HERE —
28
- * consumers should not have to track qdadm's internal CJS deps (skybot
29
- * feedback on #1389). Both the nested (`@quazardous/qdadm > pluralize`,
30
- * npm installs) and plain (`pluralize`, symlinked installs where the
31
- * nested path can't resolve) forms are declared; the unused one is a
32
- * no-op warning at worst.
26
+ * - qdadm has NO CJS transitives (pluralize is vendored as ESM, #1454), so
27
+ * there is nothing to `optimizeDeps.include` — which also means nothing
28
+ * for the optimizer to fail resolving in file:-linked installs where
29
+ * npm (install-links=false) never materializes qdadm's deps at the
30
+ * consumer root.
33
31
  * - When `node_modules/@quazardous/qdadm` is a symlink (file:/workspace
34
32
  * link — qdadm's own dev mode against a testbed app), its realpath is
35
33
  * appended to `server.fs.allow` so qdadm's `?raw` dynamic imports (i18n
@@ -44,9 +42,6 @@ import fs from 'node:fs'
44
42
  import path from 'node:path'
45
43
  import { searchForWorkspaceRoot, type Plugin, type UserConfig } from 'vite'
46
44
 
47
- /** qdadm's CJS transitive deps that must be pre-bundled (internal knowledge). */
48
- const CJS_TRANSITIVES = ['pluralize']
49
-
50
45
  export interface QdadmVitePluginOptions {
51
46
  /**
52
47
  * Extra packages to dedupe on top of the defaults
@@ -84,12 +79,10 @@ export function qdadmVitePlugin(options: QdadmVitePluginOptions = {}): Plugin {
84
79
  // Symlinked install (file:/workspace link)? Allow serving from the
85
80
  // link target, which typically lives outside the consumer's root.
86
81
  const fsAllow: string[] = []
87
- let symlinked = false
88
82
  try {
89
83
  const pkgDir = path.join(root, 'node_modules', '@quazardous', 'qdadm')
90
84
  const real = fs.realpathSync(pkgDir)
91
85
  if (real !== pkgDir) {
92
- symlinked = true
93
86
  fsAllow.push(searchForWorkspaceRoot(root), real)
94
87
  }
95
88
  } catch {
@@ -123,13 +116,6 @@ export function qdadmVitePlugin(options: QdadmVitePluginOptions = {}): Plugin {
123
116
  },
124
117
  optimizeDeps: {
125
118
  exclude: [...widgetExcludes, ...themesExcludes, '@quazardous/qdadm'],
126
- // One form per install mode: the nested path resolves through a
127
- // real node_modules chain (npm installs), the plain form through a
128
- // symlink realpath — declaring the unused one logs a "Failed to
129
- // resolve dependency" warning (skybot testbed, #1389).
130
- include: symlinked
131
- ? [...CJS_TRANSITIVES]
132
- : CJS_TRANSITIVES.map((dep) => `@quazardous/qdadm > ${dep}`),
133
119
  },
134
120
  ...(fsAllow.length > 0 ? { server: { fs: { allow: fsAllow } } } : {}),
135
121
  }
package/src/vite-env.d.ts CHANGED
@@ -32,19 +32,3 @@ declare module '*.sass' {
32
32
  const content: string
33
33
  export default content
34
34
  }
35
-
36
- // `pluralize` ships as plain JS without bundled `.d.ts`. The official
37
- // types live in `@types/pluralize` but pulling them in as a runtime
38
- // dep here is overkill for a single-call usage. Declare the minimum
39
- // surface we use so `strict` mode is happy without DefinitelyTyped.
40
- declare module 'pluralize' {
41
- interface PluralizeFn {
42
- (word: string, count?: number, inclusive?: boolean): string
43
- singular(word: string): string
44
- plural(word: string, count?: number, inclusive?: boolean): string
45
- isPlural(word: string): boolean
46
- isSingular(word: string): boolean
47
- }
48
- const pluralize: PluralizeFn
49
- export default pluralize
50
- }
@@ -1,19 +0,0 @@
1
- /**
2
- * Type declarations for pluralize module
3
- */
4
- declare module 'pluralize' {
5
- function pluralize(word: string, count?: number, inclusive?: boolean): string
6
-
7
- namespace pluralize {
8
- function plural(word: string): string
9
- function singular(word: string): string
10
- function isPlural(word: string): boolean
11
- function isSingular(word: string): boolean
12
- function addPluralRule(rule: string | RegExp, replacement: string): void
13
- function addSingularRule(rule: string | RegExp, replacement: string): void
14
- function addIrregularRule(single: string, plural: string): void
15
- function addUncountableRule(word: string | RegExp): void
16
- }
17
-
18
- export = pluralize
19
- }