@puredesktop/create-app 2.1.0 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,277 +1,342 @@
1
- #!/usr/bin/env node
2
- import { existsSync } from 'node:fs'
3
- import { readFile, readdir, stat } from 'node:fs/promises'
4
- import { join } from 'node:path'
5
-
6
- const ROOT = process.cwd()
7
- const MANIFEST_FILE = 'plugin.json'
8
- const PACKAGE_FILE = 'package.json'
9
- const TEXT_FILE_RE = /\.(cjs|css|html|js|json|jsx|mjs|ts|tsx|txt)$/i
10
-
11
- const errors = []
12
- const warnings = []
13
- const info = []
14
-
15
- const manifest = await readManifest()
16
- const packageJson = await readPackageJson()
17
- const distPath = join(ROOT, 'dist')
18
-
19
- if (packageJson) {
20
- validatePackageJson(packageJson)
21
- }
22
-
23
- if (manifest) {
24
- validateManifest(manifest)
25
- }
26
-
27
- if (await isDirectory(distPath)) {
28
- info.push('dist folder is present for static registration.')
29
- } else {
30
- errors.push('dist folder is required before the app can be registered. Run npm run build first.')
31
- }
32
-
33
- if (await isFile(join(ROOT, 'agents.md'))) {
34
- info.push('agents.md is present.')
35
- } else {
36
- errors.push('agents.md is required for every PureDesktop app.')
37
- }
38
-
39
- if (manifest?.permissions?.includes('agents')) {
40
- info.push('agents permission is enabled.')
41
- } else {
42
- errors.push('plugin.json must include the agents permission.')
43
- }
44
-
45
- if (manifest?.app?.usePureDesktopAiPanel === true) {
46
- info.push('app.usePureDesktopAiPanel is true.')
47
- } else {
48
- errors.push('plugin.json app.usePureDesktopAiPanel must be true for now.')
49
- }
50
-
51
- if (manifest) {
52
- await validateAgentTools(manifest, distPath)
53
- }
54
-
55
- for (const message of info) console.log(`ok: ${message}`)
56
- for (const message of warnings) console.warn(`warn: ${message}`)
57
- for (const message of errors) console.error(`error: ${message}`)
58
-
59
- if (errors.length > 0) {
60
- console.error(`\nPureDesktop app validation failed with ${errors.length} error${errors.length === 1 ? '' : 's'}.`)
61
- process.exit(1)
62
- }
63
-
64
- console.log('\nPureDesktop app validation passed.')
65
-
66
- async function readManifest() {
67
- return readJsonFile(MANIFEST_FILE)
68
- }
69
-
70
- async function readPackageJson() {
71
- return readJsonFile(PACKAGE_FILE)
72
- }
73
-
74
- async function readJsonFile(fileName) {
75
- const filePath = join(ROOT, fileName)
76
- if (!existsSync(filePath)) {
77
- errors.push(`${fileName} is missing.`)
78
- return null
79
- }
80
- try {
81
- return JSON.parse(await readFile(filePath, 'utf8'))
82
- } catch (error) {
83
- errors.push(`${fileName} could not be read: ${errorMessage(error)}`)
84
- return null
85
- }
86
- }
87
-
88
- function validatePackageJson(pkg) {
89
- const scripts = isPlainObject(pkg.scripts) ? pkg.scripts : {}
90
- requirePackageScript(scripts, 'dev')
91
- requirePackageScript(scripts, 'build')
92
- requirePackageScript(scripts, 'typecheck')
93
- requirePackageScript(scripts, 'puredesktop:check')
94
-
95
- const dependencies = isPlainObject(pkg.dependencies) ? pkg.dependencies : {}
96
- requirePackageDependency(dependencies, '@puredesktop/puredesktop-ui-bridge')
97
- requirePackageDependency(dependencies, 'react')
98
- requirePackageDependency(dependencies, 'react-dom')
99
- requirePackageDependency(dependencies, 'styled-components')
100
-
101
- const devDependencies = isPlainObject(pkg.devDependencies) ? pkg.devDependencies : {}
102
- requirePackageDependency(devDependencies, '@vitejs/plugin-react-swc')
103
- requirePackageDependency(devDependencies, 'typescript')
104
- requirePackageDependency(devDependencies, 'vite')
105
-
106
- if (errors.length === 0) info.push('package.json has the required PureDesktop scripts and dependencies.')
107
- }
108
-
109
- function requirePackageScript(scripts, name) {
110
- if (!nonEmptyString(scripts[name])) {
111
- errors.push(`package.json scripts.${name} is required.`)
112
- }
113
- }
114
-
115
- function requirePackageDependency(dependencies, name) {
116
- if (!nonEmptyString(dependencies[name])) {
117
- errors.push(`package.json must include ${name}.`)
118
- }
119
- }
120
-
121
- function validateManifest(manifest) {
122
- if (manifest.schemaVersion !== 1) errors.push('schemaVersion must be 1.')
123
- if (!nonEmptyString(manifest.id)) errors.push('id is required.')
124
- if (!nonEmptyString(manifest.name)) errors.push('name is required.')
125
- if (!isPlainObject(manifest.app)) {
126
- errors.push('app is required.')
127
- return
128
- }
129
- if (!nonEmptyString(manifest.app.slug)) errors.push('app.slug is required.')
130
- if (!nonEmptyString(manifest.app.name)) errors.push('app.name is required.')
131
-
132
- const permissions = Array.isArray(manifest.permissions) ? manifest.permissions : []
133
- validateEntrypoint(manifest.entrypoint ?? manifest.app.entrypoint, permissions)
134
-
135
- if (errors.length === 0) info.push('plugin.json is valid.')
136
- }
137
-
138
- function validateEntrypoint(entrypoint, permissions) {
139
- if (!isPlainObject(entrypoint)) {
140
- errors.push('entrypoint is required.')
141
- return
142
- }
143
- if (entrypoint.kind === 'dev-url') {
144
- if (!nonEmptyString(entrypoint.url)) {
145
- errors.push('entrypoint.url is required for dev-url.')
146
- return
147
- }
148
- let url
149
- try {
150
- url = new URL(entrypoint.url)
151
- } catch {
152
- errors.push('entrypoint.url must be a valid http or https URL.')
153
- return
154
- }
155
- if (url.protocol !== 'http:' && url.protocol !== 'https:') {
156
- errors.push('entrypoint.url must use http or https.')
157
- }
158
- if (!isLocalHost(url.hostname) && !permissions.includes('network')) {
159
- errors.push('Non-local entrypoint URLs require the network permission.')
160
- }
161
- return
162
- }
163
- if (entrypoint.kind === 'built-static') {
164
- if (!safeRelativeDir(entrypoint.dir)) {
165
- errors.push('built-static entrypoint.dir must be a safe relative directory.')
166
- }
167
- if (entrypoint.dir !== 'dist') {
168
- warnings.push(`PureDesktop registration packages usually use built-static dir "dist" instead of "${entrypoint.dir}".`)
169
- }
170
- return
171
- }
172
- errors.push(`Unsupported entrypoint kind: ${String(entrypoint.kind ?? '') || '(missing)'}.`)
173
- }
174
-
175
- async function validateAgentTools(manifest, distPath) {
176
- const tools = manifest.app?.agents?.tools ?? []
177
- if (!Array.isArray(tools) || tools.length === 0) {
178
- info.push('No app agent tools declared.')
179
- return
180
- }
181
-
182
- if (!manifest.permissions?.includes('agents')) {
183
- errors.push('app.agents.tools is declared, so plugin.json must include the agents permission.')
184
- }
185
-
186
- const toolNames = new Set()
187
- tools.forEach((tool, index) => {
188
- const label = `app.agents.tools[${index}]`
189
- const name = typeof tool?.name === 'string' ? tool.name.trim() : ''
190
- if (!name) {
191
- errors.push(`${label}.name is required.`)
192
- return
193
- }
194
- if (toolNames.has(name)) errors.push(`Duplicate app agent tool name: ${name}.`)
195
- toolNames.add(name)
196
- if (!tool.description?.trim()) errors.push(`${label}.description is required.`)
197
- if (!isPlainObject(tool.inputSchema)) errors.push(`${label}.inputSchema must be an object.`)
198
- })
199
-
200
- if (toolNames.size === 0) return
201
-
202
- const 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
- }
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'
@@ -33,8 +33,8 @@ export function App(): React.ReactElement {
33
33
  bootError
34
34
  ? bootError.message
35
35
  : booting
36
- ? 'Loading'
37
- : 'Waiting for PureScience shell bridge'
36
+ ? 'Loading...'
37
+ : 'Waiting for PureDesktop shell bridge...'
38
38
  }
39
39
  />
40
40
  </AppFrame>