@ossy/app 3.0.1 → 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/cli/manifest-plugin.js +11 -3
- package/package.json +17 -16
- 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/serialize-package-definition.js +1 -29
- package/src/shell/DevPagesPanel.jsx +1 -1
- package/src/shell/buildSidebarNav.js +5 -32
- package/src/shell/index.js +2 -1
- package/src/shell/resolvePackageHomePageId.js +14 -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/cli/manifest-plugin.js
CHANGED
|
@@ -3,9 +3,9 @@ import path from 'node:path'
|
|
|
3
3
|
import { pathToFileURL } from 'node:url'
|
|
4
4
|
|
|
5
5
|
import { metadataIdFromFile, defaultPageRoute } from './get-platform-files.task.js'
|
|
6
|
-
import { discoverPackageDefinitions } from '
|
|
7
|
-
import { buildActionsSchema } from '
|
|
8
|
-
import { buildCapabilities } from '
|
|
6
|
+
import { discoverPackageDefinitions } from '@ossy/manifest/discover-package-definitions'
|
|
7
|
+
import { buildActionsSchema } from '@ossy/manifest/build-actions-schema'
|
|
8
|
+
import { buildCapabilities } from '@ossy/manifest/build-capabilities'
|
|
9
9
|
import { extractTaskCatalogEntry, buildTaskCatalogEdges } from '../src/manifest/extract-task-catalog.js'
|
|
10
10
|
import {
|
|
11
11
|
Schema,
|
|
@@ -587,6 +587,14 @@ export function manifestPlugin ({ entriesByStub, srcDir, staticOutDir, configVal
|
|
|
587
587
|
this.error(`[@ossy/app][build] ${entryInfo.kind} entry missing metadata.id: ${entryInfo.sourcePath}`)
|
|
588
588
|
}
|
|
589
589
|
if (seenIds[entryInfo.kind].has(id)) {
|
|
590
|
+
// App-local components are discovered first. Allow the app to shadow
|
|
591
|
+
// the same component id from an installed package (e.g. brand logos).
|
|
592
|
+
if (entryInfo.kind === 'component' && entryInfo.packageName) {
|
|
593
|
+
console.warn(
|
|
594
|
+
`[@ossy/app][build] Skipping package component "${id}" from ${entryInfo.packageName} — app already provides it`,
|
|
595
|
+
)
|
|
596
|
+
continue
|
|
597
|
+
}
|
|
590
598
|
this.error(`[@ossy/app][build] Duplicate ${entryInfo.kind} id "${id}"`)
|
|
591
599
|
}
|
|
592
600
|
seenIds[entryInfo.kind].add(id)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/app",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"description": "",
|
|
5
5
|
"source": "./src/index.js",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -49,20 +49,21 @@
|
|
|
49
49
|
"@babel/eslint-parser": "^7.15.8",
|
|
50
50
|
"@babel/preset-react": "^7.26.3",
|
|
51
51
|
"@babel/register": "^7.25.9",
|
|
52
|
-
"@ossy/authentication": "^3.0.
|
|
53
|
-
"@ossy/design-system": "^3.0.
|
|
54
|
-
"@ossy/locale": "^3.0.
|
|
55
|
-
"@ossy/
|
|
56
|
-
"@ossy/
|
|
57
|
-
"@ossy/
|
|
58
|
-
"@ossy/
|
|
59
|
-
"@ossy/
|
|
60
|
-
"@ossy/router
|
|
61
|
-
"@ossy/
|
|
62
|
-
"@ossy/
|
|
63
|
-
"@ossy/sdk
|
|
64
|
-
"@ossy/
|
|
65
|
-
"@ossy/
|
|
52
|
+
"@ossy/authentication": "^3.0.2",
|
|
53
|
+
"@ossy/design-system": "^3.0.2",
|
|
54
|
+
"@ossy/locale": "^3.0.2",
|
|
55
|
+
"@ossy/manifest": "^3.0.2",
|
|
56
|
+
"@ossy/package-catalog": "^3.0.2",
|
|
57
|
+
"@ossy/pages": "^3.0.2",
|
|
58
|
+
"@ossy/platform": "^3.0.2",
|
|
59
|
+
"@ossy/resources": "^3.0.2",
|
|
60
|
+
"@ossy/router": "^3.0.2",
|
|
61
|
+
"@ossy/router-react": "^3.0.2",
|
|
62
|
+
"@ossy/schema": "^3.0.2",
|
|
63
|
+
"@ossy/sdk": "^3.0.2",
|
|
64
|
+
"@ossy/sdk-react": "^3.0.2",
|
|
65
|
+
"@ossy/themes": "^3.0.2",
|
|
66
|
+
"@ossy/workspaces": "^3.0.2",
|
|
66
67
|
"@rollup/plugin-alias": "^6.0.0",
|
|
67
68
|
"@rollup/plugin-babel": "^7.0.0",
|
|
68
69
|
"@rollup/plugin-commonjs": "^29.0.0",
|
|
@@ -96,5 +97,5 @@
|
|
|
96
97
|
"README.md",
|
|
97
98
|
"tsconfig.json"
|
|
98
99
|
],
|
|
99
|
-
"gitHead": "
|
|
100
|
+
"gitHead": "f0a8af3c370bbaffe4f7388c14f9345a5c6c56e5"
|
|
100
101
|
}
|
|
@@ -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'
|
|
@@ -1,29 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Pick JSON-serializable Definition fields for manifest / SSR bootstrap.
|
|
3
|
-
* Definition holds module presentation metadata only — capabilities live in manifestSummary.
|
|
4
|
-
*
|
|
5
|
-
* @param {object | null | undefined} definition
|
|
6
|
-
* @returns {object | null}
|
|
7
|
-
*/
|
|
8
|
-
export function serializePackageDefinition (definition) {
|
|
9
|
-
if (!definition || typeof definition !== 'object' || Array.isArray(definition)) return null
|
|
10
|
-
|
|
11
|
-
/** @type {Record<string, unknown>} */
|
|
12
|
-
const out = {}
|
|
13
|
-
|
|
14
|
-
if (typeof definition.id === 'string' && definition.id.trim()) out.id = definition.id.trim()
|
|
15
|
-
if (typeof definition.title === 'string' && definition.title.trim()) out.title = definition.title.trim()
|
|
16
|
-
if (typeof definition.description === 'string' && definition.description.trim()) {
|
|
17
|
-
out.description = definition.description.trim()
|
|
18
|
-
}
|
|
19
|
-
if (typeof definition.icon === 'string' && definition.icon.trim()) out.icon = definition.icon.trim()
|
|
20
|
-
if (typeof definition.navOrder === 'number' && Number.isFinite(definition.navOrder)) {
|
|
21
|
-
out.navOrder = definition.navOrder
|
|
22
|
-
}
|
|
23
|
-
if (definition.entitlementRequired === false) out.entitlementRequired = false
|
|
24
|
-
if (Array.isArray(definition.status) && definition.status.length) {
|
|
25
|
-
out.status = definition.status.filter((s) => typeof s === 'string' && s.trim()).map((s) => s.trim())
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return Object.keys(out).length ? out : null
|
|
29
|
-
}
|
|
1
|
+
export * from '@ossy/manifest/serialize-package-definition'
|
|
@@ -3,7 +3,7 @@ import { Button, Text, View } from '@ossy/design-system'
|
|
|
3
3
|
import { useRouter } from '@ossy/router-react'
|
|
4
4
|
import { useApp } from './AppContext.js'
|
|
5
5
|
import { formatPagePaths, groupPagesByFeature } from './devPagesUtils.js'
|
|
6
|
-
import { packageNameToSlug } from '
|
|
6
|
+
import { packageNameToSlug } from '@ossy/manifest/build-manifest-summary'
|
|
7
7
|
|
|
8
8
|
const monoStyle = {
|
|
9
9
|
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { slugToDisplayName } from '@ossy/package-catalog'
|
|
2
2
|
import { isServiceEntitled } from '@ossy/workspaces/entitlements'
|
|
3
3
|
import { resolveWorkspaceServices } from './resolveWorkspaceServices.js'
|
|
4
|
+
import { resolvePackageHomePageId } from './resolvePackageHomePageId.js'
|
|
5
|
+
|
|
6
|
+
export { resolvePackageHomePageId }
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* @typedef {object} SidebarShellItem
|
|
@@ -19,38 +22,8 @@ import { resolveWorkspaceServices } from './resolveWorkspaceServices.js'
|
|
|
19
22
|
*/
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* @param {Array<{ id?: string, path?: string | Record<string, string> }>} pages
|
|
26
|
-
* @param {string} slug
|
|
27
|
-
* @returns {string | null}
|
|
28
|
-
*/
|
|
29
|
-
export function resolvePackageHomePageId (pages, slug) {
|
|
30
|
-
const list = (pages || []).filter((page) => page?.id)
|
|
31
|
-
if (!list.length) return null
|
|
32
|
-
|
|
33
|
-
const conventional = list.find((page) => page.id === `${slug}/home`)
|
|
34
|
-
if (conventional) return conventional.id
|
|
35
|
-
|
|
36
|
-
const anyHome = list.find((page) => page.id === 'home' || page.id.endsWith('/home'))
|
|
37
|
-
if (anyHome) return anyHome.id
|
|
38
|
-
|
|
39
|
-
const pathLength = (page) => {
|
|
40
|
-
const path = page.path
|
|
41
|
-
if (typeof path === 'string') return path.length
|
|
42
|
-
if (path && typeof path === 'object') {
|
|
43
|
-
return Math.min(...Object.values(path).map((p) => (typeof p === 'string' ? p.length : 999)))
|
|
44
|
-
}
|
|
45
|
-
return 999
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const byPath = [...list].sort((a, b) => pathLength(a) - pathLength(b) || a.id.localeCompare(b.id))
|
|
49
|
-
return byPath[0].id
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Build entitlement-filtered sidebar navigation — one link per entitled package home.
|
|
25
|
+
* Build entitlement-filtered sidebar navigation — one link per entitled package
|
|
26
|
+
* that registers a `{slug}/home` page.
|
|
54
27
|
*
|
|
55
28
|
* @param {{
|
|
56
29
|
* manifestSummary?: { packages?: Array<{ slug?: string, package?: string, pages?: Array<{ id: string, title?: string, path?: string | Record<string, string> }> }> }
|
package/src/shell/index.js
CHANGED
|
@@ -8,7 +8,8 @@ export { WorkspaceAppSettingsSync } from './WorkspaceAppSettingsSync.jsx'
|
|
|
8
8
|
export { resolveEndpoints } from './resolveEndpoints.js'
|
|
9
9
|
export { resolveWorkspaceServices } from './resolveWorkspaceServices.js'
|
|
10
10
|
export { useCompactShellLayout } from './useCompactShellLayout.js'
|
|
11
|
-
export { buildSidebarNav
|
|
11
|
+
export { buildSidebarNav } from './buildSidebarNav.js'
|
|
12
|
+
export { resolvePackageHomePageId } from './resolvePackageHomePageId.js'
|
|
12
13
|
export { shellSlotUnset, shellSlotViewId, coerceShellSlotSpec } from '../../runtime/merge-shell-slots.js'
|
|
13
14
|
export { ThemeSelect } from './ThemeSelect.jsx'
|
|
14
15
|
export { LanguageSelect } from './LanguageSelect.jsx'
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pick the package home page for sidebar nav.
|
|
3
|
+
* Only `{slug}/home` qualifies — no fallback to other pages.
|
|
4
|
+
*
|
|
5
|
+
* @param {Array<{ id?: string }> | undefined} pages
|
|
6
|
+
* @param {string} slug
|
|
7
|
+
* @returns {string | null}
|
|
8
|
+
*/
|
|
9
|
+
export function resolvePackageHomePageId (pages, slug) {
|
|
10
|
+
if (!slug) return null
|
|
11
|
+
const homeId = `${slug}/home`
|
|
12
|
+
const match = (pages || []).find((page) => page?.id === homeId)
|
|
13
|
+
return match ? homeId : null
|
|
14
|
+
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Derive MCP tool name from platform action id.
|
|
3
|
-
*
|
|
4
|
-
* @example
|
|
5
|
-
* actionIdToToolName('@ossy/resources/actions/list') // 'ossy_resources_list'
|
|
6
|
-
*
|
|
7
|
-
* @param {string} actionId
|
|
8
|
-
* @returns {string}
|
|
9
|
-
*/
|
|
10
|
-
export function actionIdToToolName (actionId) {
|
|
11
|
-
const parts = actionId.split('/')
|
|
12
|
-
if (parts.length >= 4 && parts[0].startsWith('@') && parts[2] === 'actions') {
|
|
13
|
-
const provider = parts[0].slice(1)
|
|
14
|
-
return `${provider}_${parts[1]}_${parts[3]}`.replace(/-/g, '_')
|
|
15
|
-
}
|
|
16
|
-
return `ossy_${actionId.replace(/^@/, '').replace(/\//g, '_').replace(/-/g, '_')}`
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* @param {string} actionId
|
|
21
|
-
* @returns {string}
|
|
22
|
-
*/
|
|
23
|
-
export function humanizeActionId (actionId) {
|
|
24
|
-
const intent = actionId.split('/').pop() || actionId
|
|
25
|
-
return intent
|
|
26
|
-
.split('-')
|
|
27
|
-
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
28
|
-
.join(' ')
|
|
29
|
-
}
|
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
import { templatesContentOneOf } from './template-to-json-schema.js'
|
|
2
|
-
|
|
3
|
-
const workspaceIdProp = {
|
|
4
|
-
workspaceId: {
|
|
5
|
-
type: 'string',
|
|
6
|
-
description: 'Workspace id (defaults to request workspace header)',
|
|
7
|
-
},
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/** @type {Record<string, (ctx: { schemas: object[] }) => object>} */
|
|
11
|
-
const ENVELOPES = {
|
|
12
|
-
'@ossy/resources/actions/list': () => ({
|
|
13
|
-
type: 'object',
|
|
14
|
-
properties: {
|
|
15
|
-
...workspaceIdProp,
|
|
16
|
-
location: { type: 'string', description: 'Folder path, e.g. /test/' },
|
|
17
|
-
query: { type: 'object', description: 'Optional query filters merged with location' },
|
|
18
|
-
},
|
|
19
|
-
}),
|
|
20
|
-
|
|
21
|
-
'@ossy/resources/actions/get': () => ({
|
|
22
|
-
type: 'object',
|
|
23
|
-
required: ['resourceId'],
|
|
24
|
-
properties: {
|
|
25
|
-
...workspaceIdProp,
|
|
26
|
-
resourceId: { type: 'string' },
|
|
27
|
-
},
|
|
28
|
-
}),
|
|
29
|
-
|
|
30
|
-
'@ossy/resources/actions/search': () => ({
|
|
31
|
-
type: 'object',
|
|
32
|
-
properties: {
|
|
33
|
-
...workspaceIdProp,
|
|
34
|
-
location: { type: 'string' },
|
|
35
|
-
name: { type: 'string' },
|
|
36
|
-
type: { type: 'string', description: 'Resource template id or mime type' },
|
|
37
|
-
},
|
|
38
|
-
}),
|
|
39
|
-
|
|
40
|
-
'@ossy/resources/actions/create': ({ schemas }) => {
|
|
41
|
-
const contentSchema = templatesContentOneOf(schemas)
|
|
42
|
-
return {
|
|
43
|
-
type: 'object',
|
|
44
|
-
required: ['location', 'name', 'type'],
|
|
45
|
-
properties: {
|
|
46
|
-
...workspaceIdProp,
|
|
47
|
-
location: { type: 'string', description: 'Parent folder path, e.g. /test/' },
|
|
48
|
-
name: { type: 'string' },
|
|
49
|
-
type: {
|
|
50
|
-
type: 'string',
|
|
51
|
-
description: 'directory | mime type (binary) | registered resource template id',
|
|
52
|
-
},
|
|
53
|
-
size: {
|
|
54
|
-
type: 'number',
|
|
55
|
-
description: 'Byte size — required when type is a mime type (binary upload)',
|
|
56
|
-
},
|
|
57
|
-
...(contentSchema
|
|
58
|
-
? {
|
|
59
|
-
content: {
|
|
60
|
-
description: 'Document payload when type is a resource template id',
|
|
61
|
-
...contentSchema,
|
|
62
|
-
},
|
|
63
|
-
}
|
|
64
|
-
: {}),
|
|
65
|
-
},
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
|
|
69
|
-
'@ossy/resources/actions/update-content': ({ schemas }) => {
|
|
70
|
-
const contentSchema = templatesContentOneOf(schemas)
|
|
71
|
-
return {
|
|
72
|
-
type: 'object',
|
|
73
|
-
required: ['resourceId', 'content'],
|
|
74
|
-
properties: {
|
|
75
|
-
...workspaceIdProp,
|
|
76
|
-
resourceId: { type: 'string' },
|
|
77
|
-
...(contentSchema
|
|
78
|
-
? { content: contentSchema }
|
|
79
|
-
: { content: { type: 'object' } }),
|
|
80
|
-
},
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
|
|
84
|
-
'@ossy/resources/actions/delete': () => ({
|
|
85
|
-
type: 'object',
|
|
86
|
-
required: ['resourceId'],
|
|
87
|
-
properties: {
|
|
88
|
-
...workspaceIdProp,
|
|
89
|
-
resourceId: { type: 'string' },
|
|
90
|
-
},
|
|
91
|
-
}),
|
|
92
|
-
|
|
93
|
-
'@ossy/resources/actions/upload-named-version': () => ({
|
|
94
|
-
type: 'object',
|
|
95
|
-
required: ['resourceId', 'namedVersion', 'type', 'size'],
|
|
96
|
-
properties: {
|
|
97
|
-
...workspaceIdProp,
|
|
98
|
-
resourceId: { type: 'string' },
|
|
99
|
-
namedVersion: { type: 'string', description: 'Version label, e.g. thumbnail' },
|
|
100
|
-
type: { type: 'string', description: 'Mime type' },
|
|
101
|
-
size: { type: 'number' },
|
|
102
|
-
},
|
|
103
|
-
}),
|
|
104
|
-
|
|
105
|
-
'@ossy/resources/actions/update-name': () => ({
|
|
106
|
-
type: 'object',
|
|
107
|
-
required: ['resourceId', 'name'],
|
|
108
|
-
properties: {
|
|
109
|
-
...workspaceIdProp,
|
|
110
|
-
resourceId: { type: 'string' },
|
|
111
|
-
name: { type: 'string' },
|
|
112
|
-
},
|
|
113
|
-
}),
|
|
114
|
-
|
|
115
|
-
'@ossy/resources/actions/update-location': () => ({
|
|
116
|
-
type: 'object',
|
|
117
|
-
required: ['resourceId', 'target'],
|
|
118
|
-
properties: {
|
|
119
|
-
...workspaceIdProp,
|
|
120
|
-
resourceId: { type: 'string' },
|
|
121
|
-
target: { type: 'string', description: 'Destination folder path' },
|
|
122
|
-
},
|
|
123
|
-
}),
|
|
124
|
-
|
|
125
|
-
'@ossy/resources/actions/update-access': () => ({
|
|
126
|
-
type: 'object',
|
|
127
|
-
required: ['resourceId', 'access'],
|
|
128
|
-
properties: {
|
|
129
|
-
...workspaceIdProp,
|
|
130
|
-
resourceId: { type: 'string' },
|
|
131
|
-
access: { type: 'string', enum: ['public', 'workspace', 'restricted'] },
|
|
132
|
-
},
|
|
133
|
-
}),
|
|
134
|
-
|
|
135
|
-
'@ossy/booking/actions/get-services': () => ({
|
|
136
|
-
type: 'object',
|
|
137
|
-
properties: {
|
|
138
|
-
providerSlug: { type: 'string' },
|
|
139
|
-
workspaceId: { type: 'string' },
|
|
140
|
-
includeInactive: { type: 'boolean' },
|
|
141
|
-
includeContact: { type: 'boolean' },
|
|
142
|
-
},
|
|
143
|
-
}),
|
|
144
|
-
|
|
145
|
-
'@ossy/booking/actions/get-available-slots': () => ({
|
|
146
|
-
type: 'object',
|
|
147
|
-
properties: {
|
|
148
|
-
providerSlug: { type: 'string' },
|
|
149
|
-
workspaceId: { type: 'string' },
|
|
150
|
-
serviceId: { type: 'string' },
|
|
151
|
-
duration: { type: 'number', description: 'Slot duration in minutes' },
|
|
152
|
-
from: { type: 'number', description: 'Range start (Unix ms)' },
|
|
153
|
-
to: { type: 'number', description: 'Range end (Unix ms)' },
|
|
154
|
-
},
|
|
155
|
-
}),
|
|
156
|
-
|
|
157
|
-
'@ossy/booking/actions/create': () => ({
|
|
158
|
-
type: 'object',
|
|
159
|
-
required: ['startAt', 'duration', 'clientName', 'clientEmail'],
|
|
160
|
-
properties: {
|
|
161
|
-
providerSlug: { type: 'string' },
|
|
162
|
-
workspaceId: { type: 'string' },
|
|
163
|
-
serviceId: { type: 'string' },
|
|
164
|
-
startAt: { type: 'number', description: 'Slot start (Unix ms)' },
|
|
165
|
-
duration: { type: 'number', description: 'Duration in minutes' },
|
|
166
|
-
clientName: { type: 'string' },
|
|
167
|
-
clientEmail: { type: 'string', format: 'email' },
|
|
168
|
-
clientMessage: { type: 'string' },
|
|
169
|
-
},
|
|
170
|
-
}),
|
|
171
|
-
|
|
172
|
-
'@ossy/workspaces/actions/get-schemas': () => ({
|
|
173
|
-
type: 'object',
|
|
174
|
-
properties: { ...workspaceIdProp },
|
|
175
|
-
}),
|
|
176
|
-
|
|
177
|
-
'@ossy/workspaces/actions/import-schemas': () => ({
|
|
178
|
-
type: 'object',
|
|
179
|
-
required: ['schemas'],
|
|
180
|
-
properties: {
|
|
181
|
-
...workspaceIdProp,
|
|
182
|
-
schemas: {
|
|
183
|
-
type: 'array',
|
|
184
|
-
items: { type: 'object' },
|
|
185
|
-
description: 'Workspace-imported schema definitions',
|
|
186
|
-
},
|
|
187
|
-
},
|
|
188
|
-
}),
|
|
189
|
-
|
|
190
|
-
'@ossy/users/actions/create-api-token': () => ({
|
|
191
|
-
type: 'object',
|
|
192
|
-
required: ['name', 'description'],
|
|
193
|
-
properties: {
|
|
194
|
-
name: { type: 'string' },
|
|
195
|
-
description: { type: 'string' },
|
|
196
|
-
},
|
|
197
|
-
}),
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* @param {string} actionId
|
|
202
|
-
* @param {{ schemas?: object[] }} [ctx]
|
|
203
|
-
* @returns {object}
|
|
204
|
-
*/
|
|
205
|
-
export function buildActionInputSchema (actionId, ctx = {}) {
|
|
206
|
-
const schemas = ctx.schemas || []
|
|
207
|
-
const builder = ENVELOPES[actionId]
|
|
208
|
-
if (builder) {
|
|
209
|
-
return builder({ schemas })
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return {
|
|
213
|
-
type: 'object',
|
|
214
|
-
properties: {
|
|
215
|
-
...workspaceIdProp,
|
|
216
|
-
payload: { type: 'object', description: 'Action-specific payload' },
|
|
217
|
-
},
|
|
218
|
-
additionalProperties: true,
|
|
219
|
-
}
|
|
220
|
-
}
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Map resource template field types to JSON Schema (draft-07 subset).
|
|
3
|
-
*
|
|
4
|
-
* @param {{ name: string, type: string, options?: string[] }} field
|
|
5
|
-
* @returns {object}
|
|
6
|
-
*/
|
|
7
|
-
export function fieldToJsonSchema (field) {
|
|
8
|
-
const type = field?.type?.trim?.() || 'text'
|
|
9
|
-
|
|
10
|
-
switch (type) {
|
|
11
|
-
case 'number':
|
|
12
|
-
case 'timestamp':
|
|
13
|
-
return { type: 'number', description: `${field.name} (Unix ms for timestamp)` }
|
|
14
|
-
case 'boolean':
|
|
15
|
-
return { type: 'boolean' }
|
|
16
|
-
case 'select':
|
|
17
|
-
return {
|
|
18
|
-
type: 'string',
|
|
19
|
-
...(Array.isArray(field.options) && field.options.length
|
|
20
|
-
? { enum: field.options }
|
|
21
|
-
: {}),
|
|
22
|
-
}
|
|
23
|
-
case 'multiselect':
|
|
24
|
-
return {
|
|
25
|
-
type: 'array',
|
|
26
|
-
items: {
|
|
27
|
-
type: 'string',
|
|
28
|
-
...(Array.isArray(field.options) && field.options.length
|
|
29
|
-
? { enum: field.options }
|
|
30
|
-
: {}),
|
|
31
|
-
},
|
|
32
|
-
}
|
|
33
|
-
case 'file':
|
|
34
|
-
case 'image':
|
|
35
|
-
return {
|
|
36
|
-
type: 'object',
|
|
37
|
-
properties: { resourceId: { type: 'string' } },
|
|
38
|
-
required: ['resourceId'],
|
|
39
|
-
description: 'Reference to an uploaded binary resource',
|
|
40
|
-
}
|
|
41
|
-
case 'date-range':
|
|
42
|
-
return {
|
|
43
|
-
type: 'object',
|
|
44
|
-
properties: {
|
|
45
|
-
start: { type: 'number' },
|
|
46
|
-
end: { type: 'number' },
|
|
47
|
-
},
|
|
48
|
-
required: ['start', 'end'],
|
|
49
|
-
}
|
|
50
|
-
case 'date-ranges':
|
|
51
|
-
return {
|
|
52
|
-
type: 'array',
|
|
53
|
-
items: {
|
|
54
|
-
type: 'object',
|
|
55
|
-
properties: {
|
|
56
|
-
start: { type: 'number' },
|
|
57
|
-
end: { type: 'number' },
|
|
58
|
-
},
|
|
59
|
-
required: ['start', 'end'],
|
|
60
|
-
},
|
|
61
|
-
}
|
|
62
|
-
case 'email':
|
|
63
|
-
return { type: 'string', format: 'email' }
|
|
64
|
-
case 'textarea':
|
|
65
|
-
case 'richtext':
|
|
66
|
-
case 'text':
|
|
67
|
-
case 'date':
|
|
68
|
-
default:
|
|
69
|
-
return { type: 'string' }
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* @param {{ id: string, name?: string, fields?: Array<object> }} template
|
|
75
|
-
* @returns {object}
|
|
76
|
-
*/
|
|
77
|
-
export function templateToJsonSchema (template) {
|
|
78
|
-
const properties = {}
|
|
79
|
-
for (const field of template.fields || []) {
|
|
80
|
-
if (!field?.name) continue
|
|
81
|
-
properties[field.name] = fieldToJsonSchema(field)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return {
|
|
85
|
-
title: template.id,
|
|
86
|
-
type: 'object',
|
|
87
|
-
properties,
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* @param {Array<{ id: string, fields?: Array<object> }>} templates
|
|
93
|
-
* @returns {object | undefined}
|
|
94
|
-
*/
|
|
95
|
-
export function templatesContentOneOf (templates) {
|
|
96
|
-
const branches = (templates || [])
|
|
97
|
-
.filter(t => t?.id && Array.isArray(t.fields))
|
|
98
|
-
.map(templateToJsonSchema)
|
|
99
|
-
|
|
100
|
-
if (!branches.length) return undefined
|
|
101
|
-
|
|
102
|
-
return { oneOf: branches }
|
|
103
|
-
}
|