@puredesktop/create-app 1.0.0-beta.1 → 1.0.0-beta.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 +164 -159
- package/package.json +1 -1
- package/template/README.md +299 -16
- package/template/agents.md +11 -0
- package/template/gitignore.tpl +5 -5
- package/template/index.html +12 -12
- package/template/package.json +22 -21
- package/template/plugin.json +3 -2
- package/template/scripts/validate-puredesktop-app.mjs +277 -0
- package/template/src/App.tsx +49 -49
- package/template/src/bridge/platformBridge.ts +31 -31
- package/template/src/components/AppShell.tsx +31 -31
- package/template/src/constants.ts +1 -1
- package/template/src/hooks/useAppBoot.ts +45 -45
- package/template/src/main.tsx +9 -9
- package/template/src/types.ts +5 -5
- package/template/tsconfig.json +15 -15
- package/template/vite-env.d.ts +1 -1
- package/template/vite.config.js +28 -28
package/template/index.html
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>{{APP_TITLE}}</title>
|
|
7
|
-
</head>
|
|
8
|
-
<body>
|
|
9
|
-
<div id="root"></div>
|
|
10
|
-
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
-
</body>
|
|
12
|
-
</html>
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>{{APP_TITLE}}</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
package/template/package.json
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "{{PACKAGE_NAME}}",
|
|
3
|
-
"private": true,
|
|
4
|
-
"version": "0.1.0",
|
|
5
|
-
"type": "module",
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PACKAGE_NAME}}",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "vite",
|
|
8
8
|
"build": "vite build",
|
|
9
|
-
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
9
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
10
|
+
"puredesktop:check": "node scripts/validate-puredesktop-app.mjs"
|
|
10
11
|
},
|
|
11
|
-
"dependencies": {
|
|
12
|
-
"{{UI_PACKAGE_NAME}}": "{{UI_PACKAGE_VERSION}}",
|
|
13
|
-
"react": "^19.1.0",
|
|
14
|
-
"react-dom": "^19.1.0",
|
|
15
|
-
"styled-components": "^6.1.18"
|
|
16
|
-
},
|
|
17
|
-
"devDependencies": {
|
|
18
|
-
"@swc/plugin-styled-components": "^12.9.0",
|
|
19
|
-
"@types/react": "^19.1.6",
|
|
20
|
-
"@types/react-dom": "^19.1.5",
|
|
21
|
-
"@vitejs/plugin-react-swc": "^3.10.2",
|
|
22
|
-
"typescript": "^5.8.3",
|
|
23
|
-
"vite": "^6.3.5"
|
|
24
|
-
}
|
|
25
|
-
}
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"{{UI_PACKAGE_NAME}}": "{{UI_PACKAGE_VERSION}}",
|
|
14
|
+
"react": "^19.1.0",
|
|
15
|
+
"react-dom": "^19.1.0",
|
|
16
|
+
"styled-components": "^6.1.18"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@swc/plugin-styled-components": "^12.9.0",
|
|
20
|
+
"@types/react": "^19.1.6",
|
|
21
|
+
"@types/react-dom": "^19.1.5",
|
|
22
|
+
"@vitejs/plugin-react-swc": "^3.10.2",
|
|
23
|
+
"typescript": "^5.8.3",
|
|
24
|
+
"vite": "^6.3.5"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/template/plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"id": "{{PLUGIN_ID}}",
|
|
4
4
|
"name": "{{APP_TITLE}}",
|
|
5
|
-
"permissions": ["network", "settings"],
|
|
5
|
+
"permissions": ["network", "settings", "agents"],
|
|
6
6
|
"entrypoint": {
|
|
7
7
|
"kind": "dev-url",
|
|
8
8
|
"url": "http://localhost:{{APP_PORT}}"
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"navigationLabel": "{{APP_TITLE}}",
|
|
15
15
|
"productName": "pure.{{APP_SLUG}}",
|
|
16
16
|
"kind": "{{APP_SLUG}}",
|
|
17
|
-
"description": "{{APP_TITLE}}
|
|
17
|
+
"description": "{{APP_TITLE}} - built with PureScience Desktop.",
|
|
18
|
+
"usePureDesktopAiPanel": true
|
|
18
19
|
}
|
|
19
20
|
}
|
|
@@ -0,0 +1,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 sourceText = await readProjectText(join(ROOT, 'src'))
|
|
203
|
+
const distText = await readProjectText(distPath)
|
|
204
|
+
const registrationHints = ['usePlatformAgentTools', 'registerAgentTools', 'agents.tools.register']
|
|
205
|
+
if (!registrationHints.some(hint => sourceText.includes(hint))) {
|
|
206
|
+
errors.push('Declared app agent tools must be registered in source with usePlatformAgentTools or agents.tools.register.')
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
for (const toolName of toolNames) {
|
|
210
|
+
if (!sourceText.includes(toolName)) {
|
|
211
|
+
errors.push(`Declared app agent tool "${toolName}" was not found in source files.`)
|
|
212
|
+
}
|
|
213
|
+
if (!distText.includes(toolName)) {
|
|
214
|
+
errors.push(`Declared app agent tool "${toolName}" was not found in the built dist output.`)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!errors.some(error => error.includes('app agent tool'))) {
|
|
219
|
+
info.push(`Verified ${toolNames.size} app agent tool${toolNames.size === 1 ? '' : 's'} in source and dist.`)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function readProjectText(root) {
|
|
224
|
+
const chunks = []
|
|
225
|
+
await collectText(root, chunks)
|
|
226
|
+
return chunks.join('\n')
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function collectText(path, chunks) {
|
|
230
|
+
const stats = await stat(path).catch(() => null)
|
|
231
|
+
if (!stats) return
|
|
232
|
+
if (stats.isFile()) {
|
|
233
|
+
if (!TEXT_FILE_RE.test(path)) return
|
|
234
|
+
chunks.push(await readFile(path, 'utf8').catch(() => ''))
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
if (!stats.isDirectory()) return
|
|
238
|
+
const entries = await readdir(path, { withFileTypes: true }).catch(() => [])
|
|
239
|
+
for (const entry of entries) {
|
|
240
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue
|
|
241
|
+
await collectText(join(path, entry.name), chunks)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function isFile(path) {
|
|
246
|
+
return stat(path)
|
|
247
|
+
.then(stats => stats.isFile())
|
|
248
|
+
.catch(() => false)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function isDirectory(path) {
|
|
252
|
+
return stat(path)
|
|
253
|
+
.then(stats => stats.isDirectory())
|
|
254
|
+
.catch(() => false)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function nonEmptyString(value) {
|
|
258
|
+
return typeof value === 'string' && value.trim().length > 0
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function isPlainObject(value) {
|
|
262
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function isLocalHost(hostname) {
|
|
266
|
+
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1'
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function safeRelativeDir(value) {
|
|
270
|
+
if (!nonEmptyString(value)) return false
|
|
271
|
+
const normalized = value.replaceAll('\\', '/')
|
|
272
|
+
return !normalized.startsWith('/') && !normalized.includes('..')
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function errorMessage(error) {
|
|
276
|
+
return error instanceof Error ? error.message : String(error)
|
|
277
|
+
}
|
package/template/src/App.tsx
CHANGED
|
@@ -1,49 +1,49 @@
|
|
|
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
|
-
import { usePlatformBridge } from '@puredesktop/puredesktop-ui-bridge/bridge/react/usePlatformBridge'
|
|
4
|
-
import { AppShell } from './components/AppShell'
|
|
5
|
-
import { isStandaloneDevMode } from './bridge/platformBridge'
|
|
6
|
-
import { useAppBoot } from './hooks/useAppBoot'
|
|
7
|
-
|
|
8
|
-
export function App(): React.ReactElement {
|
|
9
|
-
const { error: bridgeError, ready } = usePlatformBridge()
|
|
10
|
-
const standaloneDev = isStandaloneDevMode()
|
|
11
|
-
const bootReady = ready || standaloneDev
|
|
12
|
-
const { boot, bootError, booting } = useAppBoot(bootReady)
|
|
13
|
-
|
|
14
|
-
if (bridgeError && !standaloneDev) {
|
|
15
|
-
return (
|
|
16
|
-
<AppFrame>
|
|
17
|
-
<EmptyState
|
|
18
|
-
tone="error"
|
|
19
|
-
title="Bridge unavailable"
|
|
20
|
-
message={bridgeError.message}
|
|
21
|
-
/>
|
|
22
|
-
</AppFrame>
|
|
23
|
-
)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (!bootReady || !boot) {
|
|
27
|
-
return (
|
|
28
|
-
<AppFrame>
|
|
29
|
-
<EmptyState
|
|
30
|
-
tone={bootError ? 'error' : 'neutral'}
|
|
31
|
-
title={bootError ? 'Boot failed' : '{{APP_TITLE}}'}
|
|
32
|
-
message={
|
|
33
|
-
bootError
|
|
34
|
-
? bootError.message
|
|
35
|
-
: booting
|
|
36
|
-
? 'Loading…'
|
|
37
|
-
: 'Waiting for PureScience shell bridge…'
|
|
38
|
-
}
|
|
39
|
-
/>
|
|
40
|
-
</AppFrame>
|
|
41
|
-
)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return (
|
|
45
|
-
<AppFrame>
|
|
46
|
-
<AppShell settings={boot.settings} />
|
|
47
|
-
</AppFrame>
|
|
48
|
-
)
|
|
49
|
-
}
|
|
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
|
+
import { usePlatformBridge } from '@puredesktop/puredesktop-ui-bridge/bridge/react/usePlatformBridge'
|
|
4
|
+
import { AppShell } from './components/AppShell'
|
|
5
|
+
import { isStandaloneDevMode } from './bridge/platformBridge'
|
|
6
|
+
import { useAppBoot } from './hooks/useAppBoot'
|
|
7
|
+
|
|
8
|
+
export function App(): React.ReactElement {
|
|
9
|
+
const { error: bridgeError, ready } = usePlatformBridge()
|
|
10
|
+
const standaloneDev = isStandaloneDevMode()
|
|
11
|
+
const bootReady = ready || standaloneDev
|
|
12
|
+
const { boot, bootError, booting } = useAppBoot(bootReady)
|
|
13
|
+
|
|
14
|
+
if (bridgeError && !standaloneDev) {
|
|
15
|
+
return (
|
|
16
|
+
<AppFrame>
|
|
17
|
+
<EmptyState
|
|
18
|
+
tone="error"
|
|
19
|
+
title="Bridge unavailable"
|
|
20
|
+
message={bridgeError.message}
|
|
21
|
+
/>
|
|
22
|
+
</AppFrame>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!bootReady || !boot) {
|
|
27
|
+
return (
|
|
28
|
+
<AppFrame>
|
|
29
|
+
<EmptyState
|
|
30
|
+
tone={bootError ? 'error' : 'neutral'}
|
|
31
|
+
title={bootError ? 'Boot failed' : '{{APP_TITLE}}'}
|
|
32
|
+
message={
|
|
33
|
+
bootError
|
|
34
|
+
? bootError.message
|
|
35
|
+
: booting
|
|
36
|
+
? 'Loading…'
|
|
37
|
+
: 'Waiting for PureScience shell bridge…'
|
|
38
|
+
}
|
|
39
|
+
/>
|
|
40
|
+
</AppFrame>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<AppFrame>
|
|
46
|
+
<AppShell settings={boot.settings} />
|
|
47
|
+
</AppFrame>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
import { bridge } from '@puredesktop/puredesktop-ui-bridge/bridge/client'
|
|
2
|
-
import { PLATFORM_BRIDGE_METHODS } from '@puredesktop/puredesktop-ui-bridge/bridge/methods'
|
|
3
|
-
import { APP_SLUG } from '../constants'
|
|
4
|
-
import type { AppSettings } from '../types'
|
|
5
|
-
|
|
6
|
-
export { bridge }
|
|
7
|
-
|
|
8
|
-
const STANDALONE_SETTINGS_KEY = 'puredesktop:{{APP_SLUG}}:settings'
|
|
9
|
-
|
|
10
|
-
export function isStandaloneDevMode(): boolean {
|
|
11
|
-
return import.meta.env.DEV && window.parent === window
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function readStandaloneSettings(): AppSettings {
|
|
15
|
-
try {
|
|
16
|
-
const raw = window.localStorage.getItem(STANDALONE_SETTINGS_KEY)
|
|
17
|
-
return raw ? (JSON.parse(raw) as AppSettings) : {}
|
|
18
|
-
} catch {
|
|
19
|
-
return {}
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export async function fetchAppSettings(): Promise<AppSettings> {
|
|
24
|
-
if (isStandaloneDevMode()) {
|
|
25
|
-
return readStandaloneSettings()
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return bridge.call<AppSettings>(PLATFORM_BRIDGE_METHODS.SETTINGS_APP_GET, [
|
|
29
|
-
APP_SLUG,
|
|
30
|
-
])
|
|
31
|
-
}
|
|
1
|
+
import { bridge } from '@puredesktop/puredesktop-ui-bridge/bridge/client'
|
|
2
|
+
import { PLATFORM_BRIDGE_METHODS } from '@puredesktop/puredesktop-ui-bridge/bridge/methods'
|
|
3
|
+
import { APP_SLUG } from '../constants'
|
|
4
|
+
import type { AppSettings } from '../types'
|
|
5
|
+
|
|
6
|
+
export { bridge }
|
|
7
|
+
|
|
8
|
+
const STANDALONE_SETTINGS_KEY = 'puredesktop:{{APP_SLUG}}:settings'
|
|
9
|
+
|
|
10
|
+
export function isStandaloneDevMode(): boolean {
|
|
11
|
+
return import.meta.env.DEV && window.parent === window
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function readStandaloneSettings(): AppSettings {
|
|
15
|
+
try {
|
|
16
|
+
const raw = window.localStorage.getItem(STANDALONE_SETTINGS_KEY)
|
|
17
|
+
return raw ? (JSON.parse(raw) as AppSettings) : {}
|
|
18
|
+
} catch {
|
|
19
|
+
return {}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function fetchAppSettings(): Promise<AppSettings> {
|
|
24
|
+
if (isStandaloneDevMode()) {
|
|
25
|
+
return readStandaloneSettings()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return bridge.call<AppSettings>(PLATFORM_BRIDGE_METHODS.SETTINGS_APP_GET, [
|
|
29
|
+
APP_SLUG,
|
|
30
|
+
])
|
|
31
|
+
}
|
|
@@ -1,31 +1,31 @@
|
|
|
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
|
-
import type { AppSettings } from '../types'
|
|
5
|
-
|
|
6
|
-
const Root = styled.div`
|
|
7
|
-
display: flex;
|
|
8
|
-
flex-direction: column;
|
|
9
|
-
gap: 12px;
|
|
10
|
-
padding: 32px;
|
|
11
|
-
max-width: 720px;
|
|
12
|
-
`
|
|
13
|
-
|
|
14
|
-
export interface AppShellProps {
|
|
15
|
-
settings: AppSettings
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function AppShell({ settings }: AppShellProps): React.ReactElement {
|
|
19
|
-
return (
|
|
20
|
-
<Root>
|
|
21
|
-
<Heading level={1}>{{APP_TITLE}}</Heading>
|
|
22
|
-
<Text>
|
|
23
|
-
Start building in <code>src/components/AppShell.tsx</code>. Bridge boot
|
|
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}
|
|
29
|
-
</Root>
|
|
30
|
-
)
|
|
31
|
-
}
|
|
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
|
+
import type { AppSettings } from '../types'
|
|
5
|
+
|
|
6
|
+
const Root = styled.div`
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-direction: column;
|
|
9
|
+
gap: 12px;
|
|
10
|
+
padding: 32px;
|
|
11
|
+
max-width: 720px;
|
|
12
|
+
`
|
|
13
|
+
|
|
14
|
+
export interface AppShellProps {
|
|
15
|
+
settings: AppSettings
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function AppShell({ settings }: AppShellProps): React.ReactElement {
|
|
19
|
+
return (
|
|
20
|
+
<Root>
|
|
21
|
+
<Heading level={1}>{{APP_TITLE}}</Heading>
|
|
22
|
+
<Text>
|
|
23
|
+
Start building in <code>src/components/AppShell.tsx</code>. Bridge boot
|
|
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}
|
|
29
|
+
</Root>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const APP_SLUG = '{{APP_SLUG}}'
|
|
1
|
+
export const APP_SLUG = '{{APP_SLUG}}'
|