@puredesktop/create-app 2.1.0 → 2.1.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/bin/create-puredesktop-app.mjs +135 -49
- package/package.json +2 -2
- package/template/README.md +39 -296
- package/template/docs/README.md +31 -0
- package/template/docs/agent.md +58 -0
- package/template/docs/app-lifecycle.md +60 -0
- package/template/docs/bridge/README.md +84 -0
- package/template/docs/bridge/dialog.md +56 -0
- package/template/docs/bridge/fs.md +95 -0
- package/template/docs/bridge/network.md +38 -0
- package/template/docs/bridge/settings.md +63 -0
- package/template/docs/bridge/storage.md +48 -0
- package/template/docs/plugin-manifest.md +108 -0
- package/template/docs/theme-vars.md +138 -0
- package/template/docs/tool-handlers.md +124 -0
- package/template/docs/ui-and-components.md +59 -0
- package/template/package.json +4 -4
- package/template/plugin.json +1 -1
- package/template/scripts/validate-puredesktop-app.mjs +342 -277
- package/template/src/App.tsx +4 -4
- package/template/src/bridge/platformBridge.ts +122 -31
- package/template/src/components/AppShell.tsx +7 -7
|
@@ -1,277 +1,342 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { existsSync } from 'node:fs'
|
|
3
|
-
import { readFile, readdir, stat } from 'node:fs/promises'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
|
|
6
|
-
const ROOT = process.cwd()
|
|
7
|
-
const MANIFEST_FILE = 'plugin.json'
|
|
8
|
-
const PACKAGE_FILE = 'package.json'
|
|
9
|
-
const TEXT_FILE_RE = /\.(cjs|css|html|js|json|jsx|mjs|ts|tsx|txt)$/i
|
|
10
|
-
|
|
11
|
-
const errors = []
|
|
12
|
-
const warnings = []
|
|
13
|
-
const info = []
|
|
14
|
-
|
|
15
|
-
const manifest = await readManifest()
|
|
16
|
-
const packageJson = await readPackageJson()
|
|
17
|
-
const distPath = join(ROOT, 'dist')
|
|
18
|
-
|
|
19
|
-
if (packageJson) {
|
|
20
|
-
validatePackageJson(packageJson)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (manifest) {
|
|
24
|
-
validateManifest(manifest)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (await isDirectory(distPath)) {
|
|
28
|
-
info.push('dist folder is present for static registration.')
|
|
29
|
-
} else {
|
|
30
|
-
errors.push('dist folder is required before the app can be registered. Run npm run build first.')
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (await isFile(join(ROOT, 'agents.md'))) {
|
|
34
|
-
info.push('agents.md is present.')
|
|
35
|
-
} else {
|
|
36
|
-
errors.push('agents.md is required for every PureDesktop app.')
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (manifest?.permissions?.includes('agents')) {
|
|
40
|
-
info.push('agents permission is enabled.')
|
|
41
|
-
} else {
|
|
42
|
-
errors.push('plugin.json must include the agents permission.')
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (manifest?.app?.usePureDesktopAiPanel === true) {
|
|
46
|
-
info.push('app.usePureDesktopAiPanel is true.')
|
|
47
|
-
} else {
|
|
48
|
-
errors.push('plugin.json app.usePureDesktopAiPanel must be true for now.')
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (manifest) {
|
|
52
|
-
await validateAgentTools(manifest, distPath)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
for (const message of info) console.log(`ok: ${message}`)
|
|
56
|
-
for (const message of warnings) console.warn(`warn: ${message}`)
|
|
57
|
-
for (const message of errors) console.error(`error: ${message}`)
|
|
58
|
-
|
|
59
|
-
if (errors.length > 0) {
|
|
60
|
-
console.error(`\nPureDesktop app validation failed with ${errors.length} error${errors.length === 1 ? '' : 's'}.`)
|
|
61
|
-
process.exit(1)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
console.log('\nPureDesktop app validation passed.')
|
|
65
|
-
|
|
66
|
-
async function readManifest() {
|
|
67
|
-
return readJsonFile(MANIFEST_FILE)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function readPackageJson() {
|
|
71
|
-
return readJsonFile(PACKAGE_FILE)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async function readJsonFile(fileName) {
|
|
75
|
-
const filePath = join(ROOT, fileName)
|
|
76
|
-
if (!existsSync(filePath)) {
|
|
77
|
-
errors.push(`${fileName} is missing.`)
|
|
78
|
-
return null
|
|
79
|
-
}
|
|
80
|
-
try {
|
|
81
|
-
return JSON.parse(await readFile(filePath, 'utf8'))
|
|
82
|
-
} catch (error) {
|
|
83
|
-
errors.push(`${fileName} could not be read: ${errorMessage(error)}`)
|
|
84
|
-
return null
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function validatePackageJson(pkg) {
|
|
89
|
-
const scripts = isPlainObject(pkg.scripts) ? pkg.scripts : {}
|
|
90
|
-
requirePackageScript(scripts, 'dev')
|
|
91
|
-
requirePackageScript(scripts, 'build')
|
|
92
|
-
requirePackageScript(scripts, 'typecheck')
|
|
93
|
-
requirePackageScript(scripts, 'puredesktop:check')
|
|
94
|
-
|
|
95
|
-
const dependencies = isPlainObject(pkg.dependencies) ? pkg.dependencies : {}
|
|
96
|
-
requirePackageDependency(dependencies, '@puredesktop/puredesktop-ui-bridge')
|
|
97
|
-
requirePackageDependency(dependencies, 'react')
|
|
98
|
-
requirePackageDependency(dependencies, 'react-dom')
|
|
99
|
-
requirePackageDependency(dependencies, 'styled-components')
|
|
100
|
-
|
|
101
|
-
const devDependencies = isPlainObject(pkg.devDependencies) ? pkg.devDependencies : {}
|
|
102
|
-
requirePackageDependency(devDependencies, '@vitejs/plugin-react-swc')
|
|
103
|
-
requirePackageDependency(devDependencies, 'typescript')
|
|
104
|
-
requirePackageDependency(devDependencies, 'vite')
|
|
105
|
-
|
|
106
|
-
if (errors.length === 0) info.push('package.json has the required PureDesktop scripts and dependencies.')
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function requirePackageScript(scripts, name) {
|
|
110
|
-
if (!nonEmptyString(scripts[name])) {
|
|
111
|
-
errors.push(`package.json scripts.${name} is required.`)
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function requirePackageDependency(dependencies, name) {
|
|
116
|
-
if (!nonEmptyString(dependencies[name])) {
|
|
117
|
-
errors.push(`package.json must include ${name}.`)
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function validateManifest(manifest) {
|
|
122
|
-
if (manifest.schemaVersion !== 1) errors.push('schemaVersion must be 1.')
|
|
123
|
-
if (!nonEmptyString(manifest.id)) errors.push('id is required.')
|
|
124
|
-
if (!nonEmptyString(manifest.name)) errors.push('name is required.')
|
|
125
|
-
if (!isPlainObject(manifest.app)) {
|
|
126
|
-
errors.push('app is required.')
|
|
127
|
-
return
|
|
128
|
-
}
|
|
129
|
-
if (!nonEmptyString(manifest.app.slug)) errors.push('app.slug is required.')
|
|
130
|
-
if (!nonEmptyString(manifest.app.name)) errors.push('app.name is required.')
|
|
131
|
-
|
|
132
|
-
const permissions = Array.isArray(manifest.permissions) ? manifest.permissions : []
|
|
133
|
-
validateEntrypoint(manifest.entrypoint ?? manifest.app.entrypoint, permissions)
|
|
134
|
-
|
|
135
|
-
if (errors.length === 0) info.push('plugin.json is valid.')
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function validateEntrypoint(entrypoint, permissions) {
|
|
139
|
-
if (!isPlainObject(entrypoint)) {
|
|
140
|
-
errors.push('entrypoint is required.')
|
|
141
|
-
return
|
|
142
|
-
}
|
|
143
|
-
if (entrypoint.kind === 'dev-url') {
|
|
144
|
-
if (!nonEmptyString(entrypoint.url)) {
|
|
145
|
-
errors.push('entrypoint.url is required for dev-url.')
|
|
146
|
-
return
|
|
147
|
-
}
|
|
148
|
-
let url
|
|
149
|
-
try {
|
|
150
|
-
url = new URL(entrypoint.url)
|
|
151
|
-
} catch {
|
|
152
|
-
errors.push('entrypoint.url must be a valid http or https URL.')
|
|
153
|
-
return
|
|
154
|
-
}
|
|
155
|
-
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
156
|
-
errors.push('entrypoint.url must use http or https.')
|
|
157
|
-
}
|
|
158
|
-
if (!isLocalHost(url.hostname) && !permissions.includes('network')) {
|
|
159
|
-
errors.push('Non-local entrypoint URLs require the network permission.')
|
|
160
|
-
}
|
|
161
|
-
return
|
|
162
|
-
}
|
|
163
|
-
if (entrypoint.kind === 'built-static') {
|
|
164
|
-
if (!safeRelativeDir(entrypoint.dir)) {
|
|
165
|
-
errors.push('built-static entrypoint.dir must be a safe relative directory.')
|
|
166
|
-
}
|
|
167
|
-
if (entrypoint.dir !== 'dist') {
|
|
168
|
-
warnings.push(`PureDesktop registration packages usually use built-static dir "dist" instead of "${entrypoint.dir}".`)
|
|
169
|
-
}
|
|
170
|
-
return
|
|
171
|
-
}
|
|
172
|
-
errors.push(`Unsupported entrypoint kind: ${String(entrypoint.kind ?? '') || '(missing)'}.`)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
async function validateAgentTools(manifest, distPath) {
|
|
176
|
-
const tools = manifest.app?.agents?.tools ?? []
|
|
177
|
-
if (!Array.isArray(tools) || tools.length === 0) {
|
|
178
|
-
info.push('No app agent tools declared.')
|
|
179
|
-
return
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (!manifest.permissions?.includes('agents')) {
|
|
183
|
-
errors.push('app.agents.tools is declared, so plugin.json must include the agents permission.')
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const toolNames = new Set()
|
|
187
|
-
tools.forEach((tool, index) => {
|
|
188
|
-
const label = `app.agents.tools[${index}]`
|
|
189
|
-
const name = typeof tool?.name === 'string' ? tool.name.trim() : ''
|
|
190
|
-
if (!name) {
|
|
191
|
-
errors.push(`${label}.name is required.`)
|
|
192
|
-
return
|
|
193
|
-
}
|
|
194
|
-
if (toolNames.has(name)) errors.push(`Duplicate app agent tool name: ${name}.`)
|
|
195
|
-
toolNames.add(name)
|
|
196
|
-
if (!tool.description?.trim()) errors.push(`${label}.description is required.`)
|
|
197
|
-
if (!isPlainObject(tool.inputSchema)) errors.push(`${label}.inputSchema must be an object.`)
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
if (toolNames.size === 0) return
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (!
|
|
214
|
-
errors.push(`Declared app agent tool "${toolName}" was not found in
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { readFile, readdir, stat } from 'node:fs/promises'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
const ROOT = process.cwd()
|
|
7
|
+
const MANIFEST_FILE = 'plugin.json'
|
|
8
|
+
const PACKAGE_FILE = 'package.json'
|
|
9
|
+
const TEXT_FILE_RE = /\.(cjs|css|html|js|json|jsx|mjs|ts|tsx|txt)$/i
|
|
10
|
+
|
|
11
|
+
const errors = []
|
|
12
|
+
const warnings = []
|
|
13
|
+
const info = []
|
|
14
|
+
|
|
15
|
+
const manifest = await readManifest()
|
|
16
|
+
const packageJson = await readPackageJson()
|
|
17
|
+
const distPath = join(ROOT, 'dist')
|
|
18
|
+
|
|
19
|
+
if (packageJson) {
|
|
20
|
+
validatePackageJson(packageJson)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (manifest) {
|
|
24
|
+
validateManifest(manifest)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (await isDirectory(distPath)) {
|
|
28
|
+
info.push('dist folder is present for static registration.')
|
|
29
|
+
} else {
|
|
30
|
+
errors.push('dist folder is required before the app can be registered. Run npm run build first.')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (await isFile(join(ROOT, 'agents.md'))) {
|
|
34
|
+
info.push('agents.md is present.')
|
|
35
|
+
} else {
|
|
36
|
+
errors.push('agents.md is required for every PureDesktop app.')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (manifest?.permissions?.includes('agents')) {
|
|
40
|
+
info.push('agents permission is enabled.')
|
|
41
|
+
} else {
|
|
42
|
+
errors.push('plugin.json must include the agents permission.')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (manifest?.app?.usePureDesktopAiPanel === true) {
|
|
46
|
+
info.push('app.usePureDesktopAiPanel is true.')
|
|
47
|
+
} else {
|
|
48
|
+
errors.push('plugin.json app.usePureDesktopAiPanel must be true for now.')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (manifest) {
|
|
52
|
+
await validateAgentTools(manifest, distPath)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const message of info) console.log(`ok: ${message}`)
|
|
56
|
+
for (const message of warnings) console.warn(`warn: ${message}`)
|
|
57
|
+
for (const message of errors) console.error(`error: ${message}`)
|
|
58
|
+
|
|
59
|
+
if (errors.length > 0) {
|
|
60
|
+
console.error(`\nPureDesktop app validation failed with ${errors.length} error${errors.length === 1 ? '' : 's'}.`)
|
|
61
|
+
process.exit(1)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log('\nPureDesktop app validation passed.')
|
|
65
|
+
|
|
66
|
+
async function readManifest() {
|
|
67
|
+
return readJsonFile(MANIFEST_FILE)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function readPackageJson() {
|
|
71
|
+
return readJsonFile(PACKAGE_FILE)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function readJsonFile(fileName) {
|
|
75
|
+
const filePath = join(ROOT, fileName)
|
|
76
|
+
if (!existsSync(filePath)) {
|
|
77
|
+
errors.push(`${fileName} is missing.`)
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(await readFile(filePath, 'utf8'))
|
|
82
|
+
} catch (error) {
|
|
83
|
+
errors.push(`${fileName} could not be read: ${errorMessage(error)}`)
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function validatePackageJson(pkg) {
|
|
89
|
+
const scripts = isPlainObject(pkg.scripts) ? pkg.scripts : {}
|
|
90
|
+
requirePackageScript(scripts, 'dev')
|
|
91
|
+
requirePackageScript(scripts, 'build')
|
|
92
|
+
requirePackageScript(scripts, 'typecheck')
|
|
93
|
+
requirePackageScript(scripts, 'puredesktop:check')
|
|
94
|
+
|
|
95
|
+
const dependencies = isPlainObject(pkg.dependencies) ? pkg.dependencies : {}
|
|
96
|
+
requirePackageDependency(dependencies, '@puredesktop/puredesktop-ui-bridge')
|
|
97
|
+
requirePackageDependency(dependencies, 'react')
|
|
98
|
+
requirePackageDependency(dependencies, 'react-dom')
|
|
99
|
+
requirePackageDependency(dependencies, 'styled-components')
|
|
100
|
+
|
|
101
|
+
const devDependencies = isPlainObject(pkg.devDependencies) ? pkg.devDependencies : {}
|
|
102
|
+
requirePackageDependency(devDependencies, '@vitejs/plugin-react-swc')
|
|
103
|
+
requirePackageDependency(devDependencies, 'typescript')
|
|
104
|
+
requirePackageDependency(devDependencies, 'vite')
|
|
105
|
+
|
|
106
|
+
if (errors.length === 0) info.push('package.json has the required PureDesktop scripts and dependencies.')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function requirePackageScript(scripts, name) {
|
|
110
|
+
if (!nonEmptyString(scripts[name])) {
|
|
111
|
+
errors.push(`package.json scripts.${name} is required.`)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function requirePackageDependency(dependencies, name) {
|
|
116
|
+
if (!nonEmptyString(dependencies[name])) {
|
|
117
|
+
errors.push(`package.json must include ${name}.`)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function validateManifest(manifest) {
|
|
122
|
+
if (manifest.schemaVersion !== 1) errors.push('schemaVersion must be 1.')
|
|
123
|
+
if (!nonEmptyString(manifest.id)) errors.push('id is required.')
|
|
124
|
+
if (!nonEmptyString(manifest.name)) errors.push('name is required.')
|
|
125
|
+
if (!isPlainObject(manifest.app)) {
|
|
126
|
+
errors.push('app is required.')
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
if (!nonEmptyString(manifest.app.slug)) errors.push('app.slug is required.')
|
|
130
|
+
if (!nonEmptyString(manifest.app.name)) errors.push('app.name is required.')
|
|
131
|
+
|
|
132
|
+
const permissions = Array.isArray(manifest.permissions) ? manifest.permissions : []
|
|
133
|
+
validateEntrypoint(manifest.entrypoint ?? manifest.app.entrypoint, permissions)
|
|
134
|
+
|
|
135
|
+
if (errors.length === 0) info.push('plugin.json is valid.')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function validateEntrypoint(entrypoint, permissions) {
|
|
139
|
+
if (!isPlainObject(entrypoint)) {
|
|
140
|
+
errors.push('entrypoint is required.')
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
if (entrypoint.kind === 'dev-url') {
|
|
144
|
+
if (!nonEmptyString(entrypoint.url)) {
|
|
145
|
+
errors.push('entrypoint.url is required for dev-url.')
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
let url
|
|
149
|
+
try {
|
|
150
|
+
url = new URL(entrypoint.url)
|
|
151
|
+
} catch {
|
|
152
|
+
errors.push('entrypoint.url must be a valid http or https URL.')
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
156
|
+
errors.push('entrypoint.url must use http or https.')
|
|
157
|
+
}
|
|
158
|
+
if (!isLocalHost(url.hostname) && !permissions.includes('network')) {
|
|
159
|
+
errors.push('Non-local entrypoint URLs require the network permission.')
|
|
160
|
+
}
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
if (entrypoint.kind === 'built-static') {
|
|
164
|
+
if (!safeRelativeDir(entrypoint.dir)) {
|
|
165
|
+
errors.push('built-static entrypoint.dir must be a safe relative directory.')
|
|
166
|
+
}
|
|
167
|
+
if (entrypoint.dir !== 'dist') {
|
|
168
|
+
warnings.push(`PureDesktop registration packages usually use built-static dir "dist" instead of "${entrypoint.dir}".`)
|
|
169
|
+
}
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
errors.push(`Unsupported entrypoint kind: ${String(entrypoint.kind ?? '') || '(missing)'}.`)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function validateAgentTools(manifest, distPath) {
|
|
176
|
+
const tools = manifest.app?.agents?.tools ?? []
|
|
177
|
+
if (!Array.isArray(tools) || tools.length === 0) {
|
|
178
|
+
info.push('No app agent tools declared.')
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!manifest.permissions?.includes('agents')) {
|
|
183
|
+
errors.push('app.agents.tools is declared, so plugin.json must include the agents permission.')
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const toolNames = new Set()
|
|
187
|
+
tools.forEach((tool, index) => {
|
|
188
|
+
const label = `app.agents.tools[${index}]`
|
|
189
|
+
const name = typeof tool?.name === 'string' ? tool.name.trim() : ''
|
|
190
|
+
if (!name) {
|
|
191
|
+
errors.push(`${label}.name is required.`)
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
if (toolNames.has(name)) errors.push(`Duplicate app agent tool name: ${name}.`)
|
|
195
|
+
toolNames.add(name)
|
|
196
|
+
if (!tool.description?.trim()) errors.push(`${label}.description is required.`)
|
|
197
|
+
if (!isPlainObject(tool.inputSchema)) errors.push(`${label}.inputSchema must be an object.`)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
if (toolNames.size === 0) return
|
|
201
|
+
|
|
202
|
+
const manifestToolNames = [...toolNames]
|
|
203
|
+
await validateAgentToolScaffold(manifestToolNames)
|
|
204
|
+
|
|
205
|
+
const sourceText = await readProjectText(join(ROOT, 'src'))
|
|
206
|
+
const distText = await readProjectText(distPath)
|
|
207
|
+
const registrationHints = ['usePlatformAgentTools', 'registerAgentTools', 'agents.tools.register']
|
|
208
|
+
if (!registrationHints.some(hint => sourceText.includes(hint))) {
|
|
209
|
+
errors.push('Declared app agent tools must be registered in source with usePlatformAgentTools or agents.tools.register.')
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (const toolName of manifestToolNames) {
|
|
213
|
+
if (!sourceText.includes(toolName)) {
|
|
214
|
+
errors.push(`Declared app agent tool "${toolName}" was not found in source files.`)
|
|
215
|
+
}
|
|
216
|
+
if (!distText.includes(toolName)) {
|
|
217
|
+
errors.push(`Declared app agent tool "${toolName}" was not found in the built dist output.`)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!errors.some(error => error.includes('app agent tool'))) {
|
|
222
|
+
info.push(`Verified ${toolNames.size} app agent tool${toolNames.size === 1 ? '' : 's'} in source and dist.`)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function validateAgentToolScaffold(toolNames) {
|
|
227
|
+
const catalogPath = join(ROOT, 'src', 'agents', 'catalog.ts')
|
|
228
|
+
const handlersPath = join(ROOT, 'src', 'agents', 'handlers', 'index.ts')
|
|
229
|
+
const hookPath = join(ROOT, 'src', 'hooks', 'useAppAgentTools.ts')
|
|
230
|
+
const appPath = join(ROOT, 'src', 'App.tsx')
|
|
231
|
+
|
|
232
|
+
const catalogText = await readRequiredText(
|
|
233
|
+
catalogPath,
|
|
234
|
+
'Declared app agent tools require src/agents/catalog.ts.',
|
|
235
|
+
)
|
|
236
|
+
const handlersText = await readRequiredText(
|
|
237
|
+
handlersPath,
|
|
238
|
+
'Declared app agent tools require src/agents/handlers/index.ts.',
|
|
239
|
+
)
|
|
240
|
+
const hookText = await readRequiredText(
|
|
241
|
+
hookPath,
|
|
242
|
+
'Declared app agent tools require src/hooks/useAppAgentTools.ts.',
|
|
243
|
+
)
|
|
244
|
+
const appText = await readRequiredText(
|
|
245
|
+
appPath,
|
|
246
|
+
'Declared app agent tools require src/App.tsx.',
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
if (!catalogText || !handlersText || !hookText || !appText) return
|
|
250
|
+
|
|
251
|
+
if (!catalogText.includes('APP_AGENT_TOOL_NAMES')) {
|
|
252
|
+
errors.push('src/agents/catalog.ts must export APP_AGENT_TOOL_NAMES.')
|
|
253
|
+
}
|
|
254
|
+
if (!handlersText.includes('appAgentHandlers')) {
|
|
255
|
+
errors.push('src/agents/handlers/index.ts must export appAgentHandlers.')
|
|
256
|
+
}
|
|
257
|
+
if (!hookText.includes('usePlatformAgentTools')) {
|
|
258
|
+
errors.push('src/hooks/useAppAgentTools.ts must call usePlatformAgentTools.')
|
|
259
|
+
}
|
|
260
|
+
if (!hookText.includes('APP_AGENT_TOOL_NAMES')) {
|
|
261
|
+
errors.push('src/hooks/useAppAgentTools.ts must register APP_AGENT_TOOL_NAMES.')
|
|
262
|
+
}
|
|
263
|
+
if (!appText.includes('useAppAgentTools(ready)')) {
|
|
264
|
+
errors.push('src/App.tsx must call useAppAgentTools(ready).')
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
for (const toolName of toolNames) {
|
|
268
|
+
if (!catalogText.includes(toolName)) {
|
|
269
|
+
errors.push(`src/agents/catalog.ts is missing app agent tool "${toolName}".`)
|
|
270
|
+
}
|
|
271
|
+
if (!handlersText.includes(toolName)) {
|
|
272
|
+
errors.push(`src/agents/handlers/index.ts is missing app agent tool "${toolName}".`)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function readRequiredText(path, message) {
|
|
278
|
+
if (!(await isFile(path))) {
|
|
279
|
+
errors.push(message)
|
|
280
|
+
return ''
|
|
281
|
+
}
|
|
282
|
+
return readFile(path, 'utf8').catch(error => {
|
|
283
|
+
errors.push(`${message} ${errorMessage(error)}`)
|
|
284
|
+
return ''
|
|
285
|
+
})
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function readProjectText(root) {
|
|
289
|
+
const chunks = []
|
|
290
|
+
await collectText(root, chunks)
|
|
291
|
+
return chunks.join('\n')
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function collectText(path, chunks) {
|
|
295
|
+
const stats = await stat(path).catch(() => null)
|
|
296
|
+
if (!stats) return
|
|
297
|
+
if (stats.isFile()) {
|
|
298
|
+
if (!TEXT_FILE_RE.test(path)) return
|
|
299
|
+
chunks.push(await readFile(path, 'utf8').catch(() => ''))
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
if (!stats.isDirectory()) return
|
|
303
|
+
const entries = await readdir(path, { withFileTypes: true }).catch(() => [])
|
|
304
|
+
for (const entry of entries) {
|
|
305
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue
|
|
306
|
+
await collectText(join(path, entry.name), chunks)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function isFile(path) {
|
|
311
|
+
return stat(path)
|
|
312
|
+
.then(stats => stats.isFile())
|
|
313
|
+
.catch(() => false)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function isDirectory(path) {
|
|
317
|
+
return stat(path)
|
|
318
|
+
.then(stats => stats.isDirectory())
|
|
319
|
+
.catch(() => false)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function nonEmptyString(value) {
|
|
323
|
+
return typeof value === 'string' && value.trim().length > 0
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function isPlainObject(value) {
|
|
327
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function isLocalHost(hostname) {
|
|
331
|
+
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1'
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function safeRelativeDir(value) {
|
|
335
|
+
if (!nonEmptyString(value)) return false
|
|
336
|
+
const normalized = value.replaceAll('\\', '/')
|
|
337
|
+
return !normalized.startsWith('/') && !normalized.includes('..')
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function errorMessage(error) {
|
|
341
|
+
return error instanceof Error ? error.message : String(error)
|
|
342
|
+
}
|
package/template/src/App.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { AppFrame } from '@puredesktop/puredesktop-ui-bridge/components/common/containers/AppFrame'
|
|
2
|
-
import { EmptyState } from '@puredesktop/puredesktop-ui-bridge/components/common/feedback/EmptyState'
|
|
1
|
+
import { AppFrame } from '@puredesktop/puredesktop-ui-bridge/components/common/containers/AppFrame'
|
|
2
|
+
import { EmptyState } from '@puredesktop/puredesktop-ui-bridge/components/common/feedback/EmptyState'
|
|
3
3
|
import { usePlatformBridge } from '@puredesktop/puredesktop-ui-bridge/bridge/react/usePlatformBridge'
|
|
4
4
|
import { AppShell } from './components/AppShell'
|
|
5
5
|
import { isStandaloneDevMode } from './bridge/platformBridge'
|
|
@@ -33,8 +33,8 @@ export function App(): React.ReactElement {
|
|
|
33
33
|
bootError
|
|
34
34
|
? bootError.message
|
|
35
35
|
: booting
|
|
36
|
-
? 'Loading
|
|
37
|
-
: 'Waiting for
|
|
36
|
+
? 'Loading...'
|
|
37
|
+
: 'Waiting for PureDesktop shell bridge...'
|
|
38
38
|
}
|
|
39
39
|
/>
|
|
40
40
|
</AppFrame>
|