@ossy/app 1.40.3 → 3.0.2
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/README.md +6 -0
- package/cli/get-platform-files.task.js +4 -6
- package/cli/manifest-plugin.js +111 -36
- package/package.json +23 -14
- package/runtime/merge-shell-slots.js +148 -0
- package/runtime/page-runtime.js +32 -16
- package/runtime/resolve-app-slots.js +48 -11
- package/src/en.translations.json +8 -0
- package/src/manifest/build-actions-schema.js +1 -36
- package/src/manifest/build-capabilities.js +1 -190
- package/src/manifest/build-manifest-summary.js +1 -121
- package/src/manifest/discover-package-definitions.js +1 -100
- package/src/manifest/resolve-page-layout.js +51 -0
- package/src/manifest/serialize-package-definition.js +1 -30
- package/src/shell/App.jsx +13 -7
- package/src/shell/AppSettings.jsx +6 -0
- package/src/shell/DevPagesPanel.jsx +1 -1
- package/src/shell/HeaderAuthActions.jsx +49 -0
- package/src/shell/LanguageList.jsx +83 -0
- package/src/shell/LanguageSelect.jsx +73 -0
- package/src/shell/ThemeEditor.jsx +2 -2
- package/src/shell/ThemeSelect.jsx +101 -0
- package/src/shell/buildSidebarNav.js +86 -0
- package/src/shell/index.js +7 -0
- package/src/shell/languageCode.js +31 -0
- package/src/shell/resolvePackageHomePageId.js +14 -0
- package/src/shell/useCompactShellLayout.js +24 -0
- package/src/shell-registry/blank.layout.jsx +8 -0
- package/src/shell-registry/default.layout.jsx +101 -0
- package/src/shell-registry/footer-default.component.jsx +39 -0
- package/src/shell-registry/head-default.component.jsx +17 -0
- package/src/shell-registry/header-default.component.jsx +50 -0
- package/src/shell-registry/logo-logomark.component.jsx +8 -0
- package/src/shell-registry/logo-logotype.component.jsx +15 -0
- package/src/shell-registry/minimal.layout.jsx +58 -0
- package/src/shell-registry/sidebar-default.component.jsx +281 -0
- package/src/sv.translations.json +8 -0
- package/src/manifest/action-id-to-tool-name.js +0 -29
- package/src/manifest/build-action-input-schema.js +0 -220
- package/src/manifest/template-to-json-schema.js +0 -103
package/runtime/page-runtime.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createElement } from 'react'
|
|
2
2
|
import { pageIdToDocumentTitleKey, resolveMessage } from '@ossy/locale'
|
|
3
|
+
import { Slot } from '@ossy/design-system'
|
|
3
4
|
import { App } from '../src/shell/App.jsx'
|
|
4
|
-
import { resolvePageSlots } from './resolve-app-slots.js'
|
|
5
|
+
import { resolvePageSlots, CONTENT_SLOT_NAME } from './resolve-app-slots.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Dynamically imports each component bundle listed in `entries` and returns a
|
|
@@ -11,14 +12,17 @@ import { resolvePageSlots } from './resolve-app-slots.js'
|
|
|
11
12
|
* skipped so a broken component never prevents the page from rendering.
|
|
12
13
|
*
|
|
13
14
|
* @param {Array<{ id: string, entry: string }>} entries
|
|
15
|
+
* @param {(entry: string) => string} [resolveEntry] — map manifest `/static/…` paths to
|
|
16
|
+
* absolute `file://` URLs on the server; omit in the browser (paths resolve via HTTP).
|
|
14
17
|
* @returns {Promise<Record<string, import('react').ComponentType>>}
|
|
15
18
|
*/
|
|
16
|
-
export async function loadComponents (entries = []) {
|
|
19
|
+
export async function loadComponents (entries = [], resolveEntry) {
|
|
17
20
|
const map = {}
|
|
21
|
+
const resolve = typeof resolveEntry === 'function' ? resolveEntry : (entry) => entry
|
|
18
22
|
await Promise.all(
|
|
19
23
|
entries.map(async ({ id, entry }) => {
|
|
20
24
|
try {
|
|
21
|
-
const mod = await import(entry)
|
|
25
|
+
const mod = await import(resolve(entry))
|
|
22
26
|
if (mod && mod.default) map[id] = mod.default
|
|
23
27
|
} catch {
|
|
24
28
|
// silently skip broken component bundles
|
|
@@ -29,10 +33,10 @@ export async function loadComponents (entries = []) {
|
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
/**
|
|
32
|
-
* Dynamically imports the
|
|
36
|
+
* Dynamically imports the layout bundle for the current page. Layout components cannot
|
|
33
37
|
* be serialized into the bootstrap JSON (functions are stripped), so the server
|
|
34
|
-
* passes `layoutEntry` and the client loads the same module here.
|
|
35
|
-
*
|
|
38
|
+
* passes `layoutEntry` and the client loads the same module here. Each page may
|
|
39
|
+
* reference a different layout id (resolved at build time per ADR 0012).
|
|
36
40
|
*
|
|
37
41
|
* @param {string | null | undefined} layoutEntry
|
|
38
42
|
* @returns {Promise<import('react').ComponentType | null>}
|
|
@@ -76,20 +80,22 @@ function resolveDocumentTitle (metadata, props) {
|
|
|
76
80
|
return metadata.title || props.documentTitle || ''
|
|
77
81
|
}
|
|
78
82
|
|
|
79
|
-
|
|
83
|
+
/** Page body is registered on `app:content`; layouts only gate shell chrome around it. */
|
|
84
|
+
function buildTree ({ Layout, metadata, props }) {
|
|
80
85
|
const lang = props.language || props.defaultLanguage || 'en'
|
|
81
|
-
const
|
|
82
|
-
|
|
86
|
+
const contentEl = Layout
|
|
87
|
+
? createElement(Layout, { shellSlots: props.layoutSlots ?? {}, ...props })
|
|
88
|
+
: createElement(Slot, { view: CONTENT_SLOT_NAME })
|
|
83
89
|
return createElement(
|
|
84
90
|
'html',
|
|
85
|
-
{ lang },
|
|
91
|
+
{ lang, style: { height: '100%' } },
|
|
86
92
|
createElement(
|
|
87
93
|
'head',
|
|
88
94
|
null,
|
|
89
95
|
createElement('meta', { charSet: 'utf-8' }),
|
|
90
96
|
createElement('title', null, resolveDocumentTitle(metadata, props)),
|
|
91
97
|
),
|
|
92
|
-
createElement('body',
|
|
98
|
+
createElement('body', { style: { height: '100%', margin: 0 } }, createElement(App, props, contentEl)),
|
|
93
99
|
)
|
|
94
100
|
}
|
|
95
101
|
|
|
@@ -127,20 +133,26 @@ export function createPageEntry (pageModule, options = {}) {
|
|
|
127
133
|
import('node:stream'),
|
|
128
134
|
])
|
|
129
135
|
|
|
130
|
-
const {
|
|
131
|
-
|
|
136
|
+
const {
|
|
137
|
+
Layout = null,
|
|
138
|
+
componentEntries = [],
|
|
139
|
+
layoutSlots = {},
|
|
140
|
+
resolveComponentEntry,
|
|
141
|
+
...pageProps
|
|
142
|
+
} = props
|
|
143
|
+
const componentsById = await loadComponents(componentEntries, resolveComponentEntry)
|
|
132
144
|
const PageContent = (slotProps = {}) => createElement(Component, { ...pageProps, ...slotProps })
|
|
133
145
|
const components = resolvePageSlots({
|
|
134
146
|
layoutSlots,
|
|
135
147
|
componentsById,
|
|
136
148
|
pageComponent: PageContent,
|
|
137
149
|
})
|
|
138
|
-
const tree = buildTree({
|
|
150
|
+
const tree = buildTree({ Layout, metadata, props: { ...pageProps, components, layoutSlots } })
|
|
139
151
|
const bootstrapUrl = toBootstrapUrl(entryUrl)
|
|
140
152
|
const bootstrapModules = bootstrapUrl ? [bootstrapUrl] : []
|
|
141
153
|
// Layout is a React component — JSON.stringify drops functions, so hydration
|
|
142
154
|
// would render without the shell unless we pass the serializable entry URL.
|
|
143
|
-
const { Layout: _layout, ...bootstrapProps } = props
|
|
155
|
+
const { Layout: _layout, resolveComponentEntry: _resolveComponentEntry, ...bootstrapProps } = props
|
|
144
156
|
const bootstrapScriptContent =
|
|
145
157
|
'window.__OSSY__=' + escapeBootstrapJson(JSON.stringify(bootstrapProps ?? {}))
|
|
146
158
|
|
|
@@ -182,7 +194,11 @@ export function createPageEntry (pageModule, options = {}) {
|
|
|
182
194
|
componentsById,
|
|
183
195
|
pageComponent: PageContent,
|
|
184
196
|
})
|
|
185
|
-
hydrateRoot(document, buildTree({
|
|
197
|
+
hydrateRoot(document, buildTree({
|
|
198
|
+
Layout,
|
|
199
|
+
metadata,
|
|
200
|
+
props: { ...pageProps, components, layoutSlots },
|
|
201
|
+
}))
|
|
186
202
|
}).catch((err) => {
|
|
187
203
|
if (typeof console !== 'undefined' && typeof console.error === 'function') {
|
|
188
204
|
console.error('[@ossy/app][page-runtime] Hydration failed:', err)
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
import { createElement } from 'react'
|
|
2
|
+
import { coerceShellSlotSpec, shellSlotViewId } from './merge-shell-slots.js'
|
|
3
|
+
|
|
1
4
|
/** Canonical app chrome slot names (namespaced). App layout maps chrome only — not content. */
|
|
2
5
|
export const APP_SLOT_NAMES = [
|
|
6
|
+
'app:head',
|
|
3
7
|
'app:header',
|
|
4
8
|
'app:sidebar',
|
|
5
9
|
'app:toolbar',
|
|
6
10
|
'app:notifications',
|
|
7
11
|
'app:system-messages',
|
|
12
|
+
'app:footer',
|
|
8
13
|
]
|
|
9
14
|
|
|
10
15
|
/** Platform-owned slot filled with the current route page component. */
|
|
@@ -14,14 +19,29 @@ const APP_SLOT_PREFIX = 'app'
|
|
|
14
19
|
|
|
15
20
|
/** Bare app region → namespaced key (legacy fallback for app components only). */
|
|
16
21
|
const BARE_APP_REGION = {
|
|
22
|
+
head: 'app:head',
|
|
17
23
|
header: 'app:header',
|
|
18
24
|
sidebar: 'app:sidebar',
|
|
19
25
|
toolbar: 'app:toolbar',
|
|
20
26
|
notifications: 'app:notifications',
|
|
21
27
|
'system-messages': 'app:system-messages',
|
|
28
|
+
footer: 'app:footer',
|
|
22
29
|
content: CONTENT_SLOT_NAME,
|
|
23
30
|
}
|
|
24
31
|
|
|
32
|
+
/**
|
|
33
|
+
* @param {import('react').ComponentType<any>} Component
|
|
34
|
+
* @param {Record<string, unknown>} defaultProps
|
|
35
|
+
* @returns {import('react').ComponentType<any>}
|
|
36
|
+
*/
|
|
37
|
+
function withShellSlotProps (Component, defaultProps) {
|
|
38
|
+
function ShellSlotTarget (props) {
|
|
39
|
+
return createElement(Component, { ...defaultProps, ...props })
|
|
40
|
+
}
|
|
41
|
+
ShellSlotTarget.displayName = `ShellSlot(${Component.displayName || Component.name || 'Component'})`
|
|
42
|
+
return ShellSlotTarget
|
|
43
|
+
}
|
|
44
|
+
|
|
25
45
|
/**
|
|
26
46
|
* Normalize a slot map key to the canonical namespaced form when it is a bare app region.
|
|
27
47
|
*
|
|
@@ -34,10 +54,10 @@ export function normalizeAppSlotName (slotName) {
|
|
|
34
54
|
}
|
|
35
55
|
|
|
36
56
|
/**
|
|
37
|
-
* Build `Record<slotName, Component>` from
|
|
38
|
-
* (`slotName →
|
|
57
|
+
* Build `Record<slotName, Component>` from merged shell slot specs
|
|
58
|
+
* (`slotName → component id + default props`) and components loaded by `metadata.id`.
|
|
39
59
|
*
|
|
40
|
-
* @param {Record<string, string> | null | undefined} layoutSlotsMap
|
|
60
|
+
* @param {Record<string, string | null | import('./merge-shell-slots.js').ShellSlotSpec | undefined> | null | undefined} layoutSlotsMap
|
|
41
61
|
* @param {Record<string, import('react').ComponentType>} componentsById
|
|
42
62
|
* @returns {Record<string, import('react').ComponentType>}
|
|
43
63
|
*/
|
|
@@ -46,16 +66,33 @@ export function resolveAppSlots (layoutSlotsMap, componentsById) {
|
|
|
46
66
|
const resolved = {}
|
|
47
67
|
|
|
48
68
|
const map = layoutSlotsMap && typeof layoutSlotsMap === 'object' ? layoutSlotsMap : {}
|
|
49
|
-
for (const [rawSlot,
|
|
69
|
+
for (const [rawSlot, rawSpec] of Object.entries(map)) {
|
|
50
70
|
const slotName = normalizeAppSlotName(rawSlot)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
71
|
+
if (!slotName || slotName === CONTENT_SLOT_NAME) continue
|
|
72
|
+
|
|
73
|
+
const spec = coerceShellSlotSpec(rawSpec) ?? (
|
|
74
|
+
rawSpec && typeof rawSpec === 'object' && !Array.isArray(rawSpec) ? rawSpec : undefined
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
if (spec?.view === null) {
|
|
78
|
+
resolved[slotName] = null
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const componentId = shellSlotViewId(spec ?? rawSpec)
|
|
83
|
+
if (!componentId) continue
|
|
84
|
+
|
|
85
|
+
const Component = componentsById[componentId]
|
|
86
|
+
if (!Component) continue
|
|
87
|
+
|
|
88
|
+
const defaultProps = spec?.props ?? {}
|
|
89
|
+
resolved[slotName] = Object.keys(defaultProps).length > 0
|
|
90
|
+
? withShellSlotProps(Component, defaultProps)
|
|
91
|
+
: Component
|
|
55
92
|
}
|
|
56
93
|
|
|
57
94
|
for (const slotName of APP_SLOT_NAMES) {
|
|
58
|
-
if (resolved
|
|
95
|
+
if (slotName in resolved) continue
|
|
59
96
|
if (componentsById[slotName]) {
|
|
60
97
|
resolved[slotName] = componentsById[slotName]
|
|
61
98
|
continue
|
|
@@ -69,10 +106,10 @@ export function resolveAppSlots (layoutSlotsMap, componentsById) {
|
|
|
69
106
|
|
|
70
107
|
/**
|
|
71
108
|
* Full provider slot map for a page request: app chrome and page content.
|
|
72
|
-
* Feature components register at canonical ADR 0006 ids (`@ossy/…/view|form
|
|
109
|
+
* Feature components register at canonical ADR 0006 ids (`@ossy/…/view|form|…`).
|
|
73
110
|
*
|
|
74
111
|
* @param {{
|
|
75
|
-
* layoutSlots?: Record<string, string> | null,
|
|
112
|
+
* layoutSlots?: Record<string, string | null | import('./merge-shell-slots.js').ShellSlotSpec> | null,
|
|
76
113
|
* componentsById?: Record<string, import('react').ComponentType>,
|
|
77
114
|
* pageComponent?: import('react').ComponentType | null,
|
|
78
115
|
* }} options
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"app.shell.sidebar.expand": "Expand sidebar",
|
|
3
|
+
"app.shell.sidebar.collapse": "Collapse sidebar",
|
|
4
|
+
"app.shell.header.languagePicker": "Language",
|
|
5
|
+
"app.shell.footer.languages": "Languages",
|
|
6
|
+
"app.shell.header.themeSwitch": "{theme} theme. Switch theme.",
|
|
7
|
+
"app.shell.header.profile": "Profile"
|
|
8
|
+
}
|
|
@@ -1,36 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { taskIdFromActionId } from '@ossy/schema'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* @param {Array<{ id: string, access?: string, package?: string }>} actions
|
|
6
|
-
* @param {{ schemas?: object[], tasks?: object[], taskCatalog?: object[], taskGraphEdges?: object[], generatedAt?: string }} [options]
|
|
7
|
-
* @returns {{ version: number, generatedAt: string, actions: Array<object> }}
|
|
8
|
-
*/
|
|
9
|
-
export function buildActionsSchema (actions = [], options = {}) {
|
|
10
|
-
const capabilities = buildCapabilities(
|
|
11
|
-
{
|
|
12
|
-
actions,
|
|
13
|
-
tasks: options.tasks || actions.flatMap(a => {
|
|
14
|
-
try {
|
|
15
|
-
return [{ id: taskIdFromActionId(a.id) }]
|
|
16
|
-
} catch {
|
|
17
|
-
return []
|
|
18
|
-
}
|
|
19
|
-
}),
|
|
20
|
-
schemas: options.schemas || [],
|
|
21
|
-
taskCatalog: options.taskCatalog || [],
|
|
22
|
-
taskGraphEdges: options.taskGraphEdges || [],
|
|
23
|
-
},
|
|
24
|
-
{ generatedAt: options.generatedAt },
|
|
25
|
-
)
|
|
26
|
-
return capabilitiesToActionsSchema(capabilities, actions)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export {
|
|
30
|
-
buildCapabilities,
|
|
31
|
-
buildCapabilitiesGraph,
|
|
32
|
-
capabilitiesTaskTopologyResource,
|
|
33
|
-
capabilitiesToActionsSchema,
|
|
34
|
-
projectTaskForCapabilities,
|
|
35
|
-
TASK_TOPOLOGY_RESOURCE_URI,
|
|
36
|
-
} from './build-capabilities.js'
|
|
1
|
+
export * from '@ossy/manifest/build-actions-schema'
|
|
@@ -1,190 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { buildActionInputSchema } from './build-action-input-schema.js'
|
|
3
|
-
import { taskIdFromActionId } from '@ossy/schema'
|
|
4
|
-
|
|
5
|
-
const MCP_ACCESS = new Set(['workspace', 'public'])
|
|
6
|
-
|
|
7
|
-
/** MCP resource URI for read-only task topology (tasks + graph edges). */
|
|
8
|
-
export const TASK_TOPOLOGY_RESOURCE_URI = 'ossy://capabilities/task-topology'
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* @param {object} task
|
|
12
|
-
*/
|
|
13
|
-
export function projectTaskForCapabilities (task) {
|
|
14
|
-
return {
|
|
15
|
-
id: task.id,
|
|
16
|
-
package: task.package ?? null,
|
|
17
|
-
moduleId: task.moduleId ?? null,
|
|
18
|
-
triggers: task.triggers ?? [],
|
|
19
|
-
schedule: task.schedule ?? null,
|
|
20
|
-
inputSchemaIds: task.inputSchemaIds ?? [],
|
|
21
|
-
actionIds: task.actionIds ?? [],
|
|
22
|
-
primaryActionId: task.primaryActionId ?? null,
|
|
23
|
-
outputs: task.outputs ?? [],
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Structured trigger → task edges for agent planning (ADR 0011).
|
|
29
|
-
*
|
|
30
|
-
* @param {object[]} taskCatalog
|
|
31
|
-
*/
|
|
32
|
-
export function buildCapabilitiesGraph (taskCatalog = []) {
|
|
33
|
-
/** @type {Array<{ kind: string, from: object, to: { taskId: string }, label?: string }>} */
|
|
34
|
-
const edges = []
|
|
35
|
-
|
|
36
|
-
for (const task of taskCatalog) {
|
|
37
|
-
for (const trigger of task.triggers ?? []) {
|
|
38
|
-
if (trigger.kind === 'on_action' && trigger.action) {
|
|
39
|
-
edges.push({
|
|
40
|
-
kind: 'on_action',
|
|
41
|
-
from: { kind: 'action', actionId: trigger.action },
|
|
42
|
-
to: { taskId: task.id },
|
|
43
|
-
label: trigger.action,
|
|
44
|
-
})
|
|
45
|
-
}
|
|
46
|
-
if (trigger.kind === 'on_event' && trigger.type) {
|
|
47
|
-
edges.push({
|
|
48
|
-
kind: 'on_event',
|
|
49
|
-
from: {
|
|
50
|
-
kind: 'trigger',
|
|
51
|
-
type: trigger.type,
|
|
52
|
-
...(trigger.event ? { event: trigger.event } : {}),
|
|
53
|
-
},
|
|
54
|
-
to: { taskId: task.id },
|
|
55
|
-
label: [trigger.type, trigger.event].filter(Boolean).join(' · '),
|
|
56
|
-
})
|
|
57
|
-
}
|
|
58
|
-
if (trigger.kind === 'on_schedule' && trigger.schedule) {
|
|
59
|
-
edges.push({
|
|
60
|
-
kind: 'on_schedule',
|
|
61
|
-
from: { kind: 'schedule', schedule: trigger.schedule },
|
|
62
|
-
to: { taskId: task.id },
|
|
63
|
-
label: trigger.schedule,
|
|
64
|
-
})
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
if (task.primaryActionId) {
|
|
68
|
-
edges.push({
|
|
69
|
-
kind: 'primary',
|
|
70
|
-
from: { kind: 'action', actionId: task.primaryActionId },
|
|
71
|
-
to: { taskId: task.id },
|
|
72
|
-
label: 'invoke',
|
|
73
|
-
})
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return { edges }
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Build agent capabilities (MCP tools + resource template catalog) from manifest data.
|
|
82
|
-
*
|
|
83
|
-
* @param {{
|
|
84
|
-
* actions?: Array<{ id: string, access?: string, package?: string }>,
|
|
85
|
-
* tasks?: Array<{ id: string }>,
|
|
86
|
-
* schemas?: Array<object>,
|
|
87
|
-
* taskCatalog?: object[],
|
|
88
|
-
* taskGraphEdges?: object[],
|
|
89
|
-
* }} manifest
|
|
90
|
-
* @param {{ generatedAt?: string }} [options]
|
|
91
|
-
*/
|
|
92
|
-
export function buildCapabilities (manifest, options = {}) {
|
|
93
|
-
const actions = manifest.actions || []
|
|
94
|
-
const tasks = manifest.tasks || []
|
|
95
|
-
const schemas = manifest.schemas || []
|
|
96
|
-
const taskCatalog = manifest.taskCatalog || []
|
|
97
|
-
const taskIds = new Set(tasks.map(t => t.id))
|
|
98
|
-
|
|
99
|
-
const tools = actions
|
|
100
|
-
.filter(action => {
|
|
101
|
-
try {
|
|
102
|
-
return taskIds.has(taskIdFromActionId(action.id))
|
|
103
|
-
} catch {
|
|
104
|
-
return false
|
|
105
|
-
}
|
|
106
|
-
})
|
|
107
|
-
.filter(action => MCP_ACCESS.has(action.access || 'authenticated'))
|
|
108
|
-
.map(action => ({
|
|
109
|
-
actionId: action.id,
|
|
110
|
-
name: actionIdToToolName(action.id),
|
|
111
|
-
title: humanizeActionId(action.id),
|
|
112
|
-
description: `Invoke ${action.id}`,
|
|
113
|
-
access: action.access || 'authenticated',
|
|
114
|
-
package: action.package,
|
|
115
|
-
channels: ['sdk', 'api', 'mcp'],
|
|
116
|
-
inputSchema: buildActionInputSchema(action.id, { schemas }),
|
|
117
|
-
}))
|
|
118
|
-
.sort((a, b) => a.actionId.localeCompare(b.actionId))
|
|
119
|
-
|
|
120
|
-
const projectedTasks = taskCatalog.map(projectTaskForCapabilities)
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
version: 2,
|
|
124
|
-
generatedAt: options.generatedAt || new Date().toISOString(),
|
|
125
|
-
tools,
|
|
126
|
-
schemas: schemas.map(t => ({
|
|
127
|
-
id: t.id,
|
|
128
|
-
name: t.name,
|
|
129
|
-
package: t.package,
|
|
130
|
-
fields: t.fields,
|
|
131
|
-
})),
|
|
132
|
-
tasks: projectedTasks,
|
|
133
|
-
graph: buildCapabilitiesGraph(taskCatalog),
|
|
134
|
-
/** Legacy string-edge projection kept for UI parity — prefer `graph.edges`. */
|
|
135
|
-
taskGraphEdges: manifest.taskGraphEdges ?? [],
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Payload exposed via MCP resource {@link TASK_TOPOLOGY_RESOURCE_URI}.
|
|
141
|
-
*
|
|
142
|
-
* @param {ReturnType<typeof buildCapabilities>} capabilities
|
|
143
|
-
*/
|
|
144
|
-
export function capabilitiesTaskTopologyResource (capabilities) {
|
|
145
|
-
return {
|
|
146
|
-
version: capabilities.version,
|
|
147
|
-
generatedAt: capabilities.generatedAt,
|
|
148
|
-
tasks: capabilities.tasks ?? [],
|
|
149
|
-
graph: capabilities.graph ?? { edges: [] },
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Legacy actions.schema.json shape for package detail / docs.
|
|
155
|
-
*
|
|
156
|
-
* @param {ReturnType<typeof buildCapabilities>} capabilities
|
|
157
|
-
* @param {Array<{ id: string, access?: string, package?: string }>} [allActions]
|
|
158
|
-
*/
|
|
159
|
-
export function capabilitiesToActionsSchema (capabilities, allActions) {
|
|
160
|
-
const toolById = Object.fromEntries(capabilities.tools.map(t => [t.actionId, t]))
|
|
161
|
-
const actions = (allActions || capabilities.tools.map(t => ({
|
|
162
|
-
id: t.actionId,
|
|
163
|
-
access: t.access,
|
|
164
|
-
package: t.package,
|
|
165
|
-
})))
|
|
166
|
-
|
|
167
|
-
return {
|
|
168
|
-
version: capabilities.version,
|
|
169
|
-
generatedAt: capabilities.generatedAt,
|
|
170
|
-
actions: actions.map(action => {
|
|
171
|
-
const tool = toolById[action.id]
|
|
172
|
-
const inputSchema = tool?.inputSchema
|
|
173
|
-
?? buildActionInputSchema(action.id, {
|
|
174
|
-
schemas: capabilities.schemas,
|
|
175
|
-
})
|
|
176
|
-
const hasKnownEnvelope = inputSchema?.properties && !inputSchema.properties.payload
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
id: action.id,
|
|
180
|
-
access: action.access ?? 'authenticated',
|
|
181
|
-
package: action.package,
|
|
182
|
-
channels: tool ? tool.channels : ['sdk', 'api'],
|
|
183
|
-
...(tool?.title ? { title: tool.title } : {}),
|
|
184
|
-
...(tool?.description ? { description: tool.description } : {}),
|
|
185
|
-
...(tool?.name ? { mcpTool: tool.name } : {}),
|
|
186
|
-
...(hasKnownEnvelope ? { inputSchema } : {}),
|
|
187
|
-
}
|
|
188
|
-
}).sort((a, b) => a.id.localeCompare(b.id)),
|
|
189
|
-
}
|
|
190
|
-
}
|
|
1
|
+
export * from '@ossy/manifest/build-capabilities'
|
|
@@ -1,121 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* @param {string} packageName npm package name (e.g. `@ossy/booking`).
|
|
3
|
-
* @returns {string} URL slug (e.g. `booking`).
|
|
4
|
-
*/
|
|
5
|
-
export function packageNameToSlug (packageName) {
|
|
6
|
-
const slash = packageName.indexOf('/')
|
|
7
|
-
return slash === -1 ? packageName : packageName.slice(slash + 1)
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* @param {string} slug URL slug (e.g. `booking`).
|
|
12
|
-
* @returns {string} Scoped package name (e.g. `@ossy/booking`).
|
|
13
|
-
*/
|
|
14
|
-
export function slugToPackageName (slug) {
|
|
15
|
-
return `@ossy/${slug}`
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* @param {object} manifest Loaded manifest from `@ossy/platform` `loadManifest`.
|
|
20
|
-
* @returns {{ packages: Array<object> }} Grouped, JSON-serializable package summary.
|
|
21
|
-
*/
|
|
22
|
-
export function buildManifestSummary (manifest) {
|
|
23
|
-
/** @type {Map<string, object>} */
|
|
24
|
-
const groups = new Map()
|
|
25
|
-
|
|
26
|
-
const ensure = (pkg) => {
|
|
27
|
-
if (!groups.has(pkg)) {
|
|
28
|
-
groups.set(pkg, {
|
|
29
|
-
package: pkg,
|
|
30
|
-
slug: packageNameToSlug(pkg),
|
|
31
|
-
pages: [],
|
|
32
|
-
apis: [],
|
|
33
|
-
actions: [],
|
|
34
|
-
components: [],
|
|
35
|
-
schemas: [],
|
|
36
|
-
tasks: [],
|
|
37
|
-
integrations: [],
|
|
38
|
-
emails: [],
|
|
39
|
-
aggregates: [],
|
|
40
|
-
})
|
|
41
|
-
}
|
|
42
|
-
return groups.get(pkg)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const add = (pkg, key, item) => {
|
|
46
|
-
ensure(pkg || '@ossy/app')[key].push(item)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
for (const page of manifest.pages || []) {
|
|
50
|
-
add(page.package, 'pages', {
|
|
51
|
-
id: page.id,
|
|
52
|
-
path: page.path,
|
|
53
|
-
...(page.title ? { title: page.title } : {}),
|
|
54
|
-
})
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
for (const api of manifest.apis || []) {
|
|
58
|
-
add(api.package, 'apis', { id: api.id, path: api.path })
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
for (const action of manifest.actions || []) {
|
|
62
|
-
add(action.package, 'actions', {
|
|
63
|
-
id: action.id,
|
|
64
|
-
...(action.access ? { access: action.access } : {}),
|
|
65
|
-
})
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
for (const component of manifest.components || []) {
|
|
69
|
-
add(component.package, 'components', { id: component.id })
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
for (const template of manifest.schemas || []) {
|
|
73
|
-
add(template.package, 'schemas', {
|
|
74
|
-
id: template.id,
|
|
75
|
-
...(template.title ? { title: template.title } : {}),
|
|
76
|
-
})
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
for (const task of manifest.taskCatalog || manifest.tasks || []) {
|
|
80
|
-
add(task.package, 'tasks', {
|
|
81
|
-
id: task.id,
|
|
82
|
-
...(task.moduleId ? { moduleId: task.moduleId } : {}),
|
|
83
|
-
...(task.triggers ? { triggers: task.triggers } : {}),
|
|
84
|
-
...(task.inputSchemaIds ? { inputSchemaIds: task.inputSchemaIds } : {}),
|
|
85
|
-
...(task.primaryActionId ? { primaryActionId: task.primaryActionId } : {}),
|
|
86
|
-
})
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
for (const integration of manifest.integrations || []) {
|
|
90
|
-
add(integration.package, 'integrations', {
|
|
91
|
-
id: integration.id,
|
|
92
|
-
...(integration.credentials?.length ? { credentials: integration.credentials } : {}),
|
|
93
|
-
})
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
for (const email of manifest.emails || []) {
|
|
97
|
-
add(email.package, 'emails', { id: email.id })
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
for (const aggregate of manifest.aggregates || []) {
|
|
101
|
-
add(aggregate.package, 'aggregates', { id: aggregate.id })
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const packages = Array.from(groups.values())
|
|
105
|
-
.sort((a, b) => a.package.localeCompare(b.package))
|
|
106
|
-
|
|
107
|
-
for (const pkg of packages) {
|
|
108
|
-
for (const key of Object.keys(pkg)) {
|
|
109
|
-
if (Array.isArray(pkg[key])) {
|
|
110
|
-
pkg[key].sort((a, b) => a.id.localeCompare(b.id))
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const definitions =
|
|
116
|
-
manifest.definitions && typeof manifest.definitions === 'object' && !Array.isArray(manifest.definitions)
|
|
117
|
-
? manifest.definitions
|
|
118
|
-
: {}
|
|
119
|
-
|
|
120
|
-
return { packages, definitions }
|
|
121
|
-
}
|
|
1
|
+
export * from '@ossy/manifest/build-manifest-summary'
|
|
@@ -1,100 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
import { pathToFileURL } from 'node:url'
|
|
4
|
-
|
|
5
|
-
import { packageNameToSlug } from './build-manifest-summary.js'
|
|
6
|
-
import { serializePackageDefinition } from './serialize-package-definition.js'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* @param {string} projectRoot
|
|
10
|
-
* @returns {string[]}
|
|
11
|
-
*/
|
|
12
|
-
function collectNodeModulesDirs (projectRoot) {
|
|
13
|
-
const dirs = []
|
|
14
|
-
let current = projectRoot
|
|
15
|
-
while (true) {
|
|
16
|
-
const nm = path.join(current, 'node_modules')
|
|
17
|
-
if (fs.existsSync(nm) && fs.statSync(nm).isDirectory()) dirs.push(nm)
|
|
18
|
-
const parent = path.dirname(current)
|
|
19
|
-
if (parent === current) break
|
|
20
|
-
current = parent
|
|
21
|
-
}
|
|
22
|
-
return dirs
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Load `Definition.js` from disk only — never import the package main entry,
|
|
27
|
-
* which may pull CLI side effects (e.g. `@ossy/cli` runs on import).
|
|
28
|
-
*
|
|
29
|
-
* @param {string} pkgDir
|
|
30
|
-
* @param {object} pkg
|
|
31
|
-
* @returns {Promise<object | null>}
|
|
32
|
-
*/
|
|
33
|
-
async function loadPackageDefinition (pkgDir, pkg) {
|
|
34
|
-
if (!pkg.ossy?.src) return null
|
|
35
|
-
|
|
36
|
-
const defPath = path.join(pkgDir, pkg.ossy.src, 'Definition.js')
|
|
37
|
-
if (!fs.existsSync(defPath)) return null
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
const mod = await import(pathToFileURL(defPath).href + `?t=${Date.now()}`)
|
|
41
|
-
if (mod?.Definition && typeof mod.Definition === 'object') {
|
|
42
|
-
return mod.Definition
|
|
43
|
-
}
|
|
44
|
-
} catch {
|
|
45
|
-
// skip packages without a readable Definition
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return null
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Discover `Definition.js` from installed `@ossy/*` packages.
|
|
53
|
-
*
|
|
54
|
-
* @param {string} projectRoot Absolute path to the consuming app root.
|
|
55
|
-
* @returns {Promise<Record<string, object>>} Map keyed by package slug.
|
|
56
|
-
*/
|
|
57
|
-
export async function discoverPackageDefinitions (projectRoot) {
|
|
58
|
-
const nmDirs = collectNodeModulesDirs(projectRoot)
|
|
59
|
-
/** @type {Record<string, object>} */
|
|
60
|
-
const definitions = {}
|
|
61
|
-
const seen = new Set()
|
|
62
|
-
|
|
63
|
-
const tryPackageDir = async (pkgDir) => {
|
|
64
|
-
const real = fs.realpathSync(pkgDir)
|
|
65
|
-
if (seen.has(real)) return
|
|
66
|
-
seen.add(real)
|
|
67
|
-
|
|
68
|
-
const pkgJsonPath = path.join(pkgDir, 'package.json')
|
|
69
|
-
if (!fs.existsSync(pkgJsonPath)) return
|
|
70
|
-
|
|
71
|
-
let pkg
|
|
72
|
-
try {
|
|
73
|
-
pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'))
|
|
74
|
-
} catch {
|
|
75
|
-
return
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const name = typeof pkg.name === 'string' ? pkg.name.trim() : ''
|
|
79
|
-
if (!name.startsWith('@ossy/')) return
|
|
80
|
-
|
|
81
|
-
const slug = packageNameToSlug(name)
|
|
82
|
-
if (!slug || definitions[slug]) return
|
|
83
|
-
|
|
84
|
-
const raw = await loadPackageDefinition(real, pkg)
|
|
85
|
-
const serialized = serializePackageDefinition(raw)
|
|
86
|
-
if (serialized) definitions[slug] = serialized
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
for (const nmDir of nmDirs) {
|
|
90
|
-
const scopeDir = path.join(nmDir, '@ossy')
|
|
91
|
-
if (!fs.existsSync(scopeDir) || !fs.statSync(scopeDir).isDirectory()) continue
|
|
92
|
-
|
|
93
|
-
for (const entry of fs.readdirSync(scopeDir, { withFileTypes: true })) {
|
|
94
|
-
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue
|
|
95
|
-
await tryPackageDir(path.join(scopeDir, entry.name))
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return definitions
|
|
100
|
-
}
|
|
1
|
+
export * from '@ossy/manifest/discover-package-definitions'
|