@puredesktop/create-app 2.1.1 → 2.1.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.
@@ -388,7 +388,7 @@ Created ${title} at ${outDir}
388
388
  npm run dev
389
389
 
390
390
  Before shipping:
391
- npm run typecheck
391
+ npm run typecheck
392
392
  npm run build
393
393
  npm run puredesktop:check
394
394
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@puredesktop/create-app",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
4
4
  "description": "Scaffold a PureDesktop plugin app (Vite + bridge SDK)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,14 +19,14 @@ entrypoint URL.
19
19
  ## Build
20
20
 
21
21
  ```bash
22
- npm run typecheck
22
+ npm run typecheck
23
23
  npm run build
24
24
  npm run puredesktop:check
25
25
  ```
26
26
 
27
- `puredesktop:check` verifies the package facts the shell needs before
28
- registration: manifest shape, entrypoint, permissions, `agents.md`, build output,
29
- and declared app agent tools.
27
+ `puredesktop:check` verifies the package facts the shell needs before
28
+ registration: manifest shape, entrypoint, permissions, `agents.md`, build output,
29
+ and declared app agent tools.
30
30
 
31
31
  ## Project Layout
32
32
 
@@ -26,6 +26,6 @@ Key files in this scaffold:
26
26
  - `src/bridge/platformBridge.ts`: local app bridge surface.
27
27
  - `src/hooks/useAppBoot.ts`: app boot data loading after the bridge is ready.
28
28
  - `src/components/AppShell.tsx`: first app-owned UI surface.
29
- - `scripts/validate-puredesktop-app.mjs`: registration and tool scaffold check.
29
+ - `scripts/validate-puredesktop-app.mjs`: registration and tool scaffold check.
30
30
 
31
31
  Use `npm run puredesktop:check` after `npm run build` before registration.
@@ -47,6 +47,11 @@ import { fetchAppSettings, networkFetch } from '../bridge/platformBridge'
47
47
  Keep request-shape knowledge in this bridge folder. Component code calls
48
48
  domain-named helpers.
49
49
 
50
+ `platformBridge.ts` is both the exported app bridge surface and the place for
51
+ app-owned bridge wrappers. Package bridge helpers are imported as local bindings
52
+ first, then re-exported for app code. App-specific wrappers in this file should
53
+ call those local bindings.
54
+
50
55
  ## Helper Domains
51
56
 
52
57
  - [Dialog](./dialog.md)
@@ -98,11 +98,11 @@ registration in the iframe executes the tool.
98
98
  Run:
99
99
 
100
100
  ```bash
101
- npm run typecheck
101
+ npm run typecheck
102
102
  npm run build
103
103
  npm run puredesktop:check
104
104
  ```
105
105
 
106
- The validator checks manifest identity, required scripts and dependencies,
107
- `agents.md`, `app.usePureDesktopAiPanel`, permissions, built output, and declared
108
- agent tool presence in source and dist.
106
+ The validator checks manifest identity, required scripts and dependencies,
107
+ `agents.md`, `app.usePureDesktopAiPanel`, permissions, built output, and declared
108
+ agent tool presence in source and dist.
@@ -106,8 +106,8 @@ state set `requiresApproval: true` unless the action is intentionally safe.
106
106
 
107
107
  ## Validation
108
108
 
109
- `npm run puredesktop:check` verifies declared tool names appear in source and in
110
- the built output. For generated tool scaffolds, it also verifies:
109
+ `npm run puredesktop:check` verifies declared tool names appear in source and in
110
+ the built output. For generated tool scaffolds, it also verifies:
111
111
 
112
112
  - `src/agents/catalog.ts` exports `APP_AGENT_TOOL_NAMES`;
113
113
  - every manifest tool name appears in the catalog;
@@ -10,7 +10,7 @@
10
10
  "puredesktop:check": "node scripts/validate-puredesktop-app.mjs"
11
11
  },
12
12
  "dependencies": {
13
- "@puredesktop/puredesktop-ui-bridge": "2.1.1",
13
+ "@puredesktop/puredesktop-ui-bridge": "2.1.3",
14
14
  "react": "^19.1.0",
15
15
  "react-dom": "^19.1.0",
16
16
  "styled-components": "^6.1.18"
@@ -1,342 +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 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
- }
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
+ }
@@ -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'
@@ -1,22 +1,14 @@
1
1
  import { bridge } from '@puredesktop/puredesktop-ui-bridge/bridge/client'
2
- import { getPlatformAppSettings } from '@puredesktop/puredesktop-ui-bridge/bridge/appSettings'
3
- import { APP_SLUG } from '../constants'
4
- import type { AppSettings } from '../types'
5
-
6
- export {
2
+ import {
7
3
  getPlatformAppSettings,
8
4
  updatePlatformAppSettings,
9
5
  } from '@puredesktop/puredesktop-ui-bridge/bridge/appSettings'
10
- export {
6
+ import {
11
7
  getPlatformPreferences,
12
8
  patchPlatformPreferences,
13
9
  } from '@puredesktop/puredesktop-ui-bridge/bridge/preferences'
14
- export {
15
- networkFetch,
16
- type PlatformNetworkFetchRequest,
17
- type PlatformNetworkFetchResponse,
18
- } from '@puredesktop/puredesktop-ui-bridge/bridge/network'
19
- export {
10
+ import { networkFetch } from '@puredesktop/puredesktop-ui-bridge/bridge/network'
11
+ import {
20
12
  createPlatformFolder,
21
13
  deletePlatformFile,
22
14
  listPlatformFiles,
@@ -28,47 +20,99 @@ export {
28
20
  renamePlatformFile,
29
21
  writePlatformFileBinary,
30
22
  writePlatformTextFile,
31
- type PlatformFileCreateFolderResult,
32
- type PlatformFileDeleteResult,
33
- type PlatformFileEntry,
34
- type PlatformFileListResult,
35
- type PlatformFilePreviewUrlResult,
36
- type PlatformFileReadBinaryResult,
37
- type PlatformFileReadPreviewResult,
38
- type PlatformFileRenameResult,
39
- type PlatformFileWriteResult,
40
23
  } from '@puredesktop/puredesktop-ui-bridge/bridge/fs'
41
- export {
24
+ import {
42
25
  openPlatformFileDialog,
43
26
  openPlatformFolderDialog,
44
27
  openPlatformImageDialog,
45
28
  savePlatformFolderDialog,
46
29
  } from '@puredesktop/puredesktop-ui-bridge/bridge/dialog'
47
- export {
30
+ import {
48
31
  readPlatformStorageJson,
49
32
  writePlatformStorageJson,
50
- type PlatformStorageJsonReadResult,
51
- type PlatformStorageJsonRequest,
52
- type PlatformStorageJsonWriteRequest,
53
- type PlatformStorageJsonWriteResult,
54
33
  } from '@puredesktop/puredesktop-ui-bridge/bridge/storage'
55
- export { bridge }
56
-
57
- const STANDALONE_SETTINGS_KEY = 'puredesktop:{{APP_SLUG}}:settings'
58
-
59
- export function isStandaloneDevMode(): boolean {
60
- return import.meta.env.DEV && window.parent === window
61
- }
62
-
63
- function readStandaloneSettings(): AppSettings {
64
- try {
65
- const raw = window.localStorage.getItem(STANDALONE_SETTINGS_KEY)
66
- return raw ? (JSON.parse(raw) as AppSettings) : {}
67
- } catch {
68
- return {}
69
- }
70
- }
71
-
34
+ import { APP_SLUG } from '../constants'
35
+ import type { AppSettings } from '../types'
36
+ import type {
37
+ PlatformNetworkFetchRequest,
38
+ PlatformNetworkFetchResponse,
39
+ } from '@puredesktop/puredesktop-ui-bridge/bridge/network'
40
+ import type {
41
+ PlatformFileCreateFolderResult,
42
+ PlatformFileDeleteResult,
43
+ PlatformFileEntry,
44
+ PlatformFileListResult,
45
+ PlatformFilePreviewUrlResult,
46
+ PlatformFileReadBinaryResult,
47
+ PlatformFileReadPreviewResult,
48
+ PlatformFileRenameResult,
49
+ PlatformFileWriteResult,
50
+ } from '@puredesktop/puredesktop-ui-bridge/bridge/fs'
51
+ import type {
52
+ PlatformStorageJsonReadResult,
53
+ PlatformStorageJsonRequest,
54
+ PlatformStorageJsonWriteRequest,
55
+ PlatformStorageJsonWriteResult,
56
+ } from '@puredesktop/puredesktop-ui-bridge/bridge/storage'
57
+
58
+ export {
59
+ getPlatformAppSettings,
60
+ updatePlatformAppSettings,
61
+ getPlatformPreferences,
62
+ patchPlatformPreferences,
63
+ networkFetch,
64
+ createPlatformFolder,
65
+ deletePlatformFile,
66
+ listPlatformFiles,
67
+ readPlatformFileBinary,
68
+ readPlatformFileBinaryDataUrl,
69
+ readPlatformFilePreview,
70
+ readPlatformFilePreviewUrl,
71
+ readPlatformTextFile,
72
+ renamePlatformFile,
73
+ writePlatformFileBinary,
74
+ writePlatformTextFile,
75
+ openPlatformFileDialog,
76
+ openPlatformFolderDialog,
77
+ openPlatformImageDialog,
78
+ savePlatformFolderDialog,
79
+ readPlatformStorageJson,
80
+ writePlatformStorageJson,
81
+ bridge,
82
+ }
83
+ export type {
84
+ PlatformNetworkFetchRequest,
85
+ PlatformNetworkFetchResponse,
86
+ PlatformFileCreateFolderResult,
87
+ PlatformFileDeleteResult,
88
+ PlatformFileEntry,
89
+ PlatformFileListResult,
90
+ PlatformFilePreviewUrlResult,
91
+ PlatformFileReadBinaryResult,
92
+ PlatformFileReadPreviewResult,
93
+ PlatformFileRenameResult,
94
+ PlatformFileWriteResult,
95
+ PlatformStorageJsonReadResult,
96
+ PlatformStorageJsonRequest,
97
+ PlatformStorageJsonWriteRequest,
98
+ PlatformStorageJsonWriteResult,
99
+ }
100
+
101
+ const STANDALONE_SETTINGS_KEY = 'puredesktop:{{APP_SLUG}}:settings'
102
+
103
+ export function isStandaloneDevMode(): boolean {
104
+ return import.meta.env.DEV && window.parent === window
105
+ }
106
+
107
+ function readStandaloneSettings(): AppSettings {
108
+ try {
109
+ const raw = window.localStorage.getItem(STANDALONE_SETTINGS_KEY)
110
+ return raw ? (JSON.parse(raw) as AppSettings) : {}
111
+ } catch {
112
+ return {}
113
+ }
114
+ }
115
+
72
116
  export async function fetchAppSettings(): Promise<AppSettings> {
73
117
  if (isStandaloneDevMode()) {
74
118
  return readStandaloneSettings()
@@ -1,6 +1,6 @@
1
- import styled from 'styled-components'
2
- import { Heading } from '@puredesktop/puredesktop-ui-bridge/components/common/typography/Heading'
3
- import { Text } from '@puredesktop/puredesktop-ui-bridge/components/common/typography/Text'
1
+ import styled from 'styled-components'
2
+ import { Heading } from '@puredesktop/puredesktop-ui-bridge/components/common/typography/Heading'
3
+ import { Text } from '@puredesktop/puredesktop-ui-bridge/components/common/typography/Text'
4
4
  import type { AppSettings } from '../types'
5
5
 
6
6
  const Root = styled.div`
@@ -22,10 +22,10 @@ export function AppShell({ settings }: AppShellProps): React.ReactElement {
22
22
  <Text>
23
23
  Start building in <code>src/components/AppShell.tsx</code>. Bridge boot
24
24
  is wired — AppFrame, settings load, and standalone browser dev all work.
25
- </Text>
26
- {Object.keys(settings).length > 0 ? (
27
- <Text meta>Loaded {Object.keys(settings).length} setting key(s).</Text>
28
- ) : null}
25
+ </Text>
26
+ {Object.keys(settings).length > 0 ? (
27
+ <Text meta>Loaded {Object.keys(settings).length} setting key(s).</Text>
28
+ ) : null}
29
29
  </Root>
30
30
  )
31
31
  }