@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.
@@ -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 '../src/manifest/discover-package-definitions.js'
7
- import { buildActionsSchema } from '../src/manifest/build-actions-schema.js'
8
- import { buildCapabilities } from '../src/manifest/build-capabilities.js'
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.1",
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.1",
53
- "@ossy/design-system": "^3.0.1",
54
- "@ossy/locale": "^3.0.1",
55
- "@ossy/package-catalog": "^3.0.1",
56
- "@ossy/pages": "^3.0.1",
57
- "@ossy/platform": "^3.0.1",
58
- "@ossy/resources": "^3.0.1",
59
- "@ossy/router": "^3.0.1",
60
- "@ossy/router-react": "^3.0.1",
61
- "@ossy/schema": "^3.0.1",
62
- "@ossy/sdk": "^3.0.1",
63
- "@ossy/sdk-react": "^3.0.1",
64
- "@ossy/themes": "^3.0.1",
65
- "@ossy/workspaces": "^3.0.1",
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": "4a70ec216448680d2fddc57b2a8a0077489720ae"
100
+ "gitHead": "f0a8af3c370bbaffe4f7388c14f9345a5c6c56e5"
100
101
  }
@@ -1,36 +1 @@
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'
1
+ export * from '@ossy/manifest/build-actions-schema'
@@ -1,190 +1 @@
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
- }
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
- import fs from 'node:fs'
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 '../manifest/build-manifest-summary.js'
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
- * Pick the package "home" page from manifest pages.
23
- * Convention: slug plus "/home", then any page id ending in "/home", then shortest route path.
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> }> }> }
@@ -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, resolvePackageHomePageId } from './buildSidebarNav.js'
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
- }