@puredesktop/create-app 2.1.1 → 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 +1 -1
- package/package.json +1 -1
- package/template/README.md +4 -4
- package/template/docs/README.md +1 -1
- package/template/docs/bridge/README.md +5 -0
- package/template/docs/plugin-manifest.md +4 -4
- package/template/docs/tool-handlers.md +2 -2
- package/template/package.json +1 -1
- package/template/scripts/validate-puredesktop-app.mjs +342 -342
- package/template/src/App.tsx +2 -2
- package/template/src/bridge/platformBridge.ts +88 -44
- package/template/src/components/AppShell.tsx +7 -7
package/package.json
CHANGED
package/template/README.md
CHANGED
|
@@ -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
|
|
package/template/docs/README.md
CHANGED
|
@@ -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;
|
package/template/package.json
CHANGED
|
@@ -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.
|
|
13
|
+
"@puredesktop/puredesktop-ui-bridge": "2.1.2",
|
|
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
|
+
}
|
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'
|
|
@@ -1,22 +1,14 @@
|
|
|
1
1
|
import { bridge } from '@puredesktop/puredesktop-ui-bridge/bridge/client'
|
|
2
|
-
import {
|
|
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
|
-
|
|
6
|
+
import {
|
|
11
7
|
getPlatformPreferences,
|
|
12
8
|
patchPlatformPreferences,
|
|
13
9
|
} from '@puredesktop/puredesktop-ui-bridge/bridge/preferences'
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
24
|
+
import {
|
|
42
25
|
openPlatformFileDialog,
|
|
43
26
|
openPlatformFolderDialog,
|
|
44
27
|
openPlatformImageDialog,
|
|
45
28
|
savePlatformFolderDialog,
|
|
46
29
|
} from '@puredesktop/puredesktop-ui-bridge/bridge/dialog'
|
|
47
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
}
|