@ossy/app 1.40.2 → 1.40.3
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 +43 -12
- package/cli/build.task.js +49 -10
- package/cli/get-platform-files.task.js +17 -9
- package/cli/manifest-plugin.js +250 -25
- package/package.json +15 -11
- package/runtime/page-runtime.js +5 -3
- package/runtime/resolve-app-slots.js +89 -0
- package/src/manifest/action-id-to-tool-name.js +29 -0
- package/src/manifest/build-action-input-schema.js +220 -0
- package/src/manifest/build-actions-schema.js +36 -0
- package/src/manifest/build-capabilities.js +190 -0
- package/src/manifest/build-manifest-summary.js +11 -5
- package/src/manifest/extract-task-catalog.js +1 -0
- package/src/manifest/template-to-json-schema.js +103 -0
- package/src/shell/App.jsx +6 -3
- package/src/shell/AppSettings.jsx +11 -0
- package/src/shell/DevPagesPanel.jsx +17 -13
- package/src/shell/ThemeEditor.jsx +18 -33
- package/src/shell/WorkspaceAppSettingsSync.jsx +39 -0
- package/src/shell/index.js +5 -0
- package/src/shell/patchUserAppSettings.js +10 -0
- package/src/shell/resolveEndpoints.js +43 -0
- package/src/shell/resolveWorkspaceServices.js +20 -0
- package/src/shell/themeEditorStyles.js +49 -0
- package/src/shell/useShellWorkspace.js +40 -0
- package/runtime/resolve-shell-slots.js +0 -112
|
@@ -0,0 +1,220 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { buildCapabilities, capabilitiesToActionsSchema } from './build-capabilities.js'
|
|
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'
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { actionIdToToolName, humanizeActionId } from './action-id-to-tool-name.js'
|
|
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
|
+
}
|
|
@@ -32,7 +32,7 @@ export function buildManifestSummary (manifest) {
|
|
|
32
32
|
apis: [],
|
|
33
33
|
actions: [],
|
|
34
34
|
components: [],
|
|
35
|
-
|
|
35
|
+
schemas: [],
|
|
36
36
|
tasks: [],
|
|
37
37
|
integrations: [],
|
|
38
38
|
emails: [],
|
|
@@ -69,15 +69,21 @@ export function buildManifestSummary (manifest) {
|
|
|
69
69
|
add(component.package, 'components', { id: component.id })
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
for (const template of manifest.
|
|
73
|
-
add(template.package, '
|
|
72
|
+
for (const template of manifest.schemas || []) {
|
|
73
|
+
add(template.package, 'schemas', {
|
|
74
74
|
id: template.id,
|
|
75
75
|
...(template.title ? { title: template.title } : {}),
|
|
76
76
|
})
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
for (const task of manifest.tasks || []) {
|
|
80
|
-
add(task.package, 'tasks', {
|
|
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
|
+
})
|
|
81
87
|
}
|
|
82
88
|
|
|
83
89
|
for (const integration of manifest.integrations || []) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { extractTaskCatalogEntry, buildTaskCatalogEdges } from '@ossy/schema'
|
|
@@ -0,0 +1,103 @@
|
|
|
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
|
+
}
|
package/src/shell/App.jsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { SDK } from '@ossy/sdk'
|
|
3
3
|
import { WorkspaceProvider } from '@ossy/sdk-react'
|
|
4
|
-
import { Theme, ComponentSlotsProvider, LocaleProvider } from '@ossy/design-system'
|
|
4
|
+
import { Theme, ComponentSlotsProvider, LocaleProvider, DEFAULT_FORM_FIELD_SLOTS } from '@ossy/design-system'
|
|
5
|
+
import { SchemasReadBootstrap } from '@ossy/workspaces/SchemasReadBootstrap'
|
|
5
6
|
import { ThemeEditor } from './ThemeEditor.jsx'
|
|
6
7
|
import { defaultAppSettings } from './AppSettings.jsx'
|
|
7
8
|
import { Router } from '@ossy/router-react'
|
|
@@ -16,7 +17,8 @@ export const App = ({ children, language, messages, fallbackMessages, ..._appSet
|
|
|
16
17
|
|
|
17
18
|
const sdk = SDK.of({
|
|
18
19
|
apiUrl: appSettings.apiUrl,
|
|
19
|
-
workspaceId: appSettings.workspaceId
|
|
20
|
+
workspaceId: appSettings.workspaceId,
|
|
21
|
+
actionRoutes: appSettings.actionRoutes,
|
|
20
22
|
})
|
|
21
23
|
|
|
22
24
|
return (
|
|
@@ -28,9 +30,10 @@ export const App = ({ children, language, messages, fallbackMessages, ..._appSet
|
|
|
28
30
|
supportedLanguages={appSettings.supportedLanguages}
|
|
29
31
|
>
|
|
30
32
|
<AppContext.Provider value={contextSettings}>
|
|
31
|
-
<ComponentSlotsProvider slots={components || {}}>
|
|
33
|
+
<ComponentSlotsProvider slots={{ ...DEFAULT_FORM_FIELD_SLOTS, ...(components || {}) }}>
|
|
32
34
|
<Theme theme={appSettings.theme} themes={appSettings.themes}>
|
|
33
35
|
<WorkspaceProvider sdk={sdk}>
|
|
36
|
+
<SchemasReadBootstrap schemas={appSettings.schemas} />
|
|
34
37
|
<Router {...appSettings} pages={appSettings.pages || []}>
|
|
35
38
|
{children}
|
|
36
39
|
{appSettings.devMode && <ThemeEditor />}
|
|
@@ -25,7 +25,18 @@ export function defaultAppSettings() {
|
|
|
25
25
|
faviconHref: undefined,
|
|
26
26
|
/** When true, main app sidebar is collapsed to icons (from server cookie / app-settings). */
|
|
27
27
|
sidebarPrimaryCollapsed: false,
|
|
28
|
+
/** Cached workspace shell snapshot (from user-app-settings cookie; refreshed client-side). */
|
|
29
|
+
workspaceName: undefined,
|
|
30
|
+
workspaceServices: undefined,
|
|
31
|
+
workspaces: undefined,
|
|
28
32
|
/** Grouped manifest entries by npm package — for dev tooling. */
|
|
29
33
|
manifestSummary: undefined,
|
|
34
|
+
/** Merged system + workspace schemas (SSR bootstrap for client validation). */
|
|
35
|
+
schemas: undefined,
|
|
36
|
+
/** Task catalog from `app build` (ADR 0011) — for automation topology UI. */
|
|
37
|
+
taskCatalog: undefined,
|
|
38
|
+
taskGraphEdges: undefined,
|
|
39
|
+
/** Action id → HTTP route map for sdk.invoke transport routing. */
|
|
40
|
+
actionRoutes: undefined,
|
|
30
41
|
}
|
|
31
42
|
}
|
|
@@ -57,36 +57,40 @@ export const DevPagesPanel = () => {
|
|
|
57
57
|
|
|
58
58
|
return (
|
|
59
59
|
<View gap="m">
|
|
60
|
-
<Text variant="
|
|
60
|
+
<Text variant="heading-tertiary" text="design-system.dev.pagesPanel.title" />
|
|
61
61
|
|
|
62
62
|
<View gap="xs" style={currentPageStyles}>
|
|
63
|
-
<Text
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
<Text
|
|
64
|
+
variant="heading-tertiary"
|
|
65
|
+
text="design-system.dev.pagesPanel.currentPage"
|
|
66
|
+
style={{ marginBottom: 'var(--space-xs)' }}
|
|
67
|
+
/>
|
|
66
68
|
<Text style={monoStyle}>
|
|
67
|
-
<strong
|
|
69
|
+
<strong><Text as="span" text="design-system.dev.pagesPanel.id" />:</strong> {app?.pageId || '—'}
|
|
68
70
|
</Text>
|
|
69
71
|
<Text style={monoStyle}>
|
|
70
|
-
<strong
|
|
72
|
+
<strong><Text as="span" text="design-system.dev.pagesPanel.path" />:</strong> {currentPathLabel}
|
|
71
73
|
</Text>
|
|
72
74
|
<Text style={monoStyle}>
|
|
73
|
-
<strong
|
|
75
|
+
<strong><Text as="span" text="design-system.dev.pagesPanel.url" />:</strong> {currentUrl}
|
|
74
76
|
</Text>
|
|
75
77
|
{packageDetailHref && currentPage?.package && (
|
|
76
78
|
<Button
|
|
77
79
|
variant="link"
|
|
78
80
|
href={packageDetailHref}
|
|
81
|
+
label="design-system.dev.pagesPanel.viewPackage"
|
|
82
|
+
params={{ package: currentPage.package }}
|
|
79
83
|
style={{ alignSelf: 'flex-start', padding: 0, height: 'auto' }}
|
|
80
|
-
|
|
81
|
-
View package ({currentPage.package})
|
|
82
|
-
</Button>
|
|
84
|
+
/>
|
|
83
85
|
)}
|
|
84
86
|
</View>
|
|
85
87
|
|
|
86
88
|
<View gap="s">
|
|
87
|
-
<Text
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
<Text
|
|
90
|
+
variant="heading-tertiary"
|
|
91
|
+
text="design-system.dev.pagesPanel.pages"
|
|
92
|
+
params={{ count: pages.length }}
|
|
93
|
+
/>
|
|
90
94
|
|
|
91
95
|
{groups.map(({ key, label, pages: groupPages }) => (
|
|
92
96
|
<View key={key} gap="xs">
|