@puredesktop/create-app 1.0.0-beta.2 → 2.1.1
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 +308 -77
- package/package.json +2 -2
- package/template/README.md +39 -296
- package/template/docs/README.md +31 -0
- package/template/docs/agent.md +58 -0
- package/template/docs/app-lifecycle.md +60 -0
- package/template/docs/bridge/README.md +79 -0
- package/template/docs/bridge/dialog.md +56 -0
- package/template/docs/bridge/fs.md +95 -0
- package/template/docs/bridge/network.md +38 -0
- package/template/docs/bridge/settings.md +63 -0
- package/template/docs/bridge/storage.md +48 -0
- package/template/docs/plugin-manifest.md +108 -0
- package/template/docs/theme-vars.md +138 -0
- package/template/docs/tool-handlers.md +124 -0
- package/template/docs/ui-and-components.md +59 -0
- package/template/package.json +4 -4
- package/template/plugin.json +1 -1
- package/template/scripts/validate-puredesktop-app.mjs +66 -1
- package/template/src/App.tsx +2 -2
- package/template/src/bridge/platformBridge.ts +62 -15
|
@@ -1,55 +1,43 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Scaffold an external
|
|
3
|
+
* Scaffold an external PureDesktop plugin app.
|
|
4
4
|
*
|
|
5
|
-
* Published as @puredesktop/create-app
|
|
5
|
+
* Published as @puredesktop/create-app -> npm create @puredesktop/app@latest
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
8
8
|
import { dirname, join, resolve } from 'node:path'
|
|
9
9
|
import { fileURLToPath } from 'node:url'
|
|
10
10
|
|
|
11
|
-
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
12
|
-
const TEMPLATE_DIR = join(__dirname, '..', 'template')
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
function usage() {
|
|
17
|
-
console.log(`Usage: npm create @puredesktop/app@latest [dir] [options]
|
|
18
|
-
|
|
19
|
-
Options:
|
|
20
|
-
--name <title> Display name (default: derived from dir)
|
|
21
|
-
--slug <slug> App slug for settings/bridge (default: dir name)
|
|
22
|
-
--port <number> Dev server port in plugin.json (default: 5300)
|
|
23
|
-
--ui <spec> UI package spec (default: ${DEFAULT_UI}@${DEFAULT_UI_VERSION})
|
|
24
|
-
|
|
25
|
-
Examples:
|
|
26
|
-
npm create @puredesktop/app@latest my-tool
|
|
27
|
-
npm create @puredesktop/app@latest my-tool -- --name "My Tool" --slug mytool --port 5310
|
|
28
|
-
`)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function uiDependencySpec(spec) {
|
|
32
|
-
if (spec.startsWith('file:')) {
|
|
33
|
-
return {
|
|
34
|
-
name: '@puredesktop/puredesktop-ui-bridge',
|
|
35
|
-
version: spec,
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
const at = spec.lastIndexOf('@')
|
|
39
|
-
if (at > 1) {
|
|
40
|
-
return { name: spec.slice(0, at), version: spec.slice(at + 1) }
|
|
41
|
-
}
|
|
42
|
-
return { name: spec, version: DEFAULT_UI_VERSION }
|
|
43
|
-
}
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
12
|
+
const TEMPLATE_DIR = join(__dirname, '..', 'template')
|
|
13
|
+
|
|
14
|
+
function usage() {
|
|
15
|
+
console.log(`Usage: npm create @puredesktop/app@latest [dir] [options]
|
|
44
16
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
17
|
+
Options:
|
|
18
|
+
--name <title> Display name (default: derived from dir)
|
|
19
|
+
--slug <slug> App slug for settings/bridge (default: dir name)
|
|
20
|
+
--port <number> Dev server port in plugin.json (default: 5300)
|
|
21
|
+
--manifest-json-base64 <base64>
|
|
22
|
+
Structured plugin.json manifest draft for app-builder use
|
|
23
|
+
--agents-md-base64 <base64>
|
|
24
|
+
Structured agents.md content for app-builder use
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
npm create @puredesktop/app@latest my-tool
|
|
28
|
+
npm create @puredesktop/app@latest my-tool -- --name "My Tool" --slug mytool --port 5310
|
|
29
|
+
`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseArgs(argv) {
|
|
33
|
+
const positional = []
|
|
34
|
+
const options = {
|
|
35
|
+
name: '',
|
|
36
|
+
slug: '',
|
|
37
|
+
port: 5300,
|
|
38
|
+
manifestJsonBase64: '',
|
|
39
|
+
agentsMdBase64: '',
|
|
40
|
+
}
|
|
53
41
|
|
|
54
42
|
for (let i = 0; i < argv.length; i += 1) {
|
|
55
43
|
const arg = argv[i]
|
|
@@ -65,14 +53,18 @@ function parseArgs(argv) {
|
|
|
65
53
|
options.slug = argv[++i] ?? ''
|
|
66
54
|
continue
|
|
67
55
|
}
|
|
68
|
-
if (arg === '--port') {
|
|
69
|
-
options.port = Number(argv[++i])
|
|
70
|
-
continue
|
|
71
|
-
}
|
|
72
|
-
if (arg === '--
|
|
73
|
-
options.
|
|
74
|
-
continue
|
|
75
|
-
}
|
|
56
|
+
if (arg === '--port') {
|
|
57
|
+
options.port = Number(argv[++i])
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
if (arg === '--manifest-json-base64') {
|
|
61
|
+
options.manifestJsonBase64 = argv[++i] ?? ''
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
if (arg === '--agents-md-base64') {
|
|
65
|
+
options.agentsMdBase64 = argv[++i] ?? ''
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
76
68
|
if (arg.startsWith('-')) {
|
|
77
69
|
console.error(`Unknown option: ${arg}`)
|
|
78
70
|
usage()
|
|
@@ -97,15 +89,223 @@ function slugify(value) {
|
|
|
97
89
|
.replace(/^-+|-+$/g, '')
|
|
98
90
|
}
|
|
99
91
|
|
|
100
|
-
function titleCaseFromSlug(slug) {
|
|
92
|
+
function titleCaseFromSlug(slug) {
|
|
101
93
|
return slug
|
|
102
94
|
.split('-')
|
|
103
95
|
.filter(Boolean)
|
|
104
96
|
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
105
97
|
.join(' ')
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function decodeBase64Utf8(value, label) {
|
|
101
|
+
if (!isBase64(value)) {
|
|
102
|
+
console.error(`${label} must be valid base64`)
|
|
103
|
+
process.exit(1)
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
return Buffer.from(value, 'base64').toString('utf8')
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error(`${label} must be valid base64: ${errorMessage(error)}`)
|
|
109
|
+
process.exit(1)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function readStructuredManifest(value, port) {
|
|
114
|
+
const text = decodeBase64Utf8(value, '--manifest-json-base64')
|
|
115
|
+
let manifest
|
|
116
|
+
try {
|
|
117
|
+
manifest = JSON.parse(text)
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error(`--manifest-json-base64 must decode to JSON: ${errorMessage(error)}`)
|
|
120
|
+
process.exit(1)
|
|
121
|
+
}
|
|
122
|
+
return normalizeStructuredManifest(manifest, port)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function normalizeStructuredManifest(manifest, port) {
|
|
126
|
+
if (!isPlainObject(manifest)) {
|
|
127
|
+
console.error('--manifest-json-base64 must decode to a manifest object')
|
|
128
|
+
process.exit(1)
|
|
129
|
+
}
|
|
130
|
+
if (manifest.schemaVersion !== 1) {
|
|
131
|
+
console.error('manifest.schemaVersion must be 1')
|
|
132
|
+
process.exit(1)
|
|
133
|
+
}
|
|
134
|
+
const id = requiredString(manifest.id, 'manifest.id')
|
|
135
|
+
const name = requiredString(manifest.name, 'manifest.name')
|
|
136
|
+
if (!isPlainObject(manifest.app)) {
|
|
137
|
+
console.error('manifest.app is required')
|
|
138
|
+
process.exit(1)
|
|
139
|
+
}
|
|
140
|
+
const appSlug = requiredString(manifest.app.slug, 'manifest.app.slug')
|
|
141
|
+
const appName = requiredString(manifest.app.name, 'manifest.app.name')
|
|
142
|
+
if (slugify(appSlug) !== appSlug) {
|
|
143
|
+
console.error('manifest.app.slug must use lowercase letters, numbers, and hyphens')
|
|
144
|
+
process.exit(1)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return stripUndefinedProperties({
|
|
148
|
+
...manifest,
|
|
149
|
+
id,
|
|
150
|
+
name,
|
|
151
|
+
entrypoint: {
|
|
152
|
+
kind: 'dev-url',
|
|
153
|
+
url: `http://localhost:${port}`,
|
|
154
|
+
},
|
|
155
|
+
app: stripUndefinedProperties({
|
|
156
|
+
...manifest.app,
|
|
157
|
+
slug: appSlug,
|
|
158
|
+
name: appName,
|
|
159
|
+
entrypoint: undefined,
|
|
160
|
+
}),
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function readStructuredAgentsMd(value) {
|
|
165
|
+
const text = decodeBase64Utf8(value, '--agents-md-base64')
|
|
166
|
+
if (!text.trim()) {
|
|
167
|
+
console.error('--agents-md-base64 must decode to non-empty agents.md content')
|
|
168
|
+
process.exit(1)
|
|
169
|
+
}
|
|
170
|
+
return text.endsWith('\n') ? text : `${text}\n`
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function requiredString(value, label) {
|
|
174
|
+
const text = typeof value === 'string' ? value.trim() : ''
|
|
175
|
+
if (!text) {
|
|
176
|
+
console.error(`${label} is required`)
|
|
177
|
+
process.exit(1)
|
|
178
|
+
}
|
|
179
|
+
return text
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function isPlainObject(value) {
|
|
183
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function isBase64(value) {
|
|
187
|
+
if (typeof value !== 'string' || !value.trim()) return false
|
|
188
|
+
const normalized = value.trim()
|
|
189
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/u.test(normalized)) return false
|
|
190
|
+
if (normalized.length % 4 !== 0) return false
|
|
191
|
+
return Buffer.from(normalized, 'base64').toString('base64') === normalized
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function stripUndefinedProperties(value) {
|
|
195
|
+
return Object.fromEntries(
|
|
196
|
+
Object.entries(value).filter(([, entry]) => entry !== undefined),
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function errorMessage(error) {
|
|
201
|
+
return error instanceof Error ? error.message : String(error)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function readGeneratedManifest(outDir) {
|
|
205
|
+
try {
|
|
206
|
+
return JSON.parse(readFileSync(join(outDir, 'plugin.json'), 'utf8'))
|
|
207
|
+
} catch (error) {
|
|
208
|
+
console.error(`Generated plugin.json could not be read: ${errorMessage(error)}`)
|
|
209
|
+
process.exit(1)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function readAgentToolNames(manifest) {
|
|
214
|
+
const tools = manifest?.app?.agents?.tools
|
|
215
|
+
if (!Array.isArray(tools)) return []
|
|
216
|
+
return tools
|
|
217
|
+
.map(tool => (typeof tool?.name === 'string' ? tool.name.trim() : ''))
|
|
218
|
+
.filter(Boolean)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function jsonString(value) {
|
|
222
|
+
return JSON.stringify(value)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function writeAgentToolScaffold(outDir, appSlug, toolNames) {
|
|
226
|
+
if (toolNames.length === 0) return
|
|
227
|
+
|
|
228
|
+
const agentsDir = join(outDir, 'src', 'agents')
|
|
229
|
+
const handlersDir = join(agentsDir, 'handlers')
|
|
230
|
+
const hooksDir = join(outDir, 'src', 'hooks')
|
|
231
|
+
mkdirSync(handlersDir, { recursive: true })
|
|
232
|
+
mkdirSync(hooksDir, { recursive: true })
|
|
233
|
+
|
|
234
|
+
writeFileSync(
|
|
235
|
+
join(agentsDir, 'catalog.ts'),
|
|
236
|
+
`export const APP_AGENT_TOOL_NAMES = ${JSON.stringify(toolNames, null, 2)} as const
|
|
237
|
+
|
|
238
|
+
export const APP_AGENT_LOG_LABEL = ${jsonString(appSlug)}
|
|
239
|
+
`,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
const handlerEntries = toolNames
|
|
243
|
+
.map(
|
|
244
|
+
name =>
|
|
245
|
+
` ${jsonString(name)}: async () => notImplemented(${jsonString(name)}),`,
|
|
246
|
+
)
|
|
247
|
+
.join('\n')
|
|
248
|
+
|
|
249
|
+
writeFileSync(
|
|
250
|
+
join(handlersDir, 'index.ts'),
|
|
251
|
+
`import { agentToolErrorContent } from '@puredesktop/puredesktop-ui-bridge/bridge/agentToolHelpers'
|
|
252
|
+
import type { AgentToolHandler } from '@puredesktop/puredesktop-ui-bridge/bridge/react/usePlatformAgentTools'
|
|
253
|
+
|
|
254
|
+
function notImplemented(toolName: string) {
|
|
255
|
+
return agentToolErrorContent(
|
|
256
|
+
\`Tool "\${toolName}" is declared in plugin.json but its runtime handler has not been implemented yet.\`,
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export const appAgentHandlers = {
|
|
261
|
+
${handlerEntries}
|
|
262
|
+
} satisfies Record<string, AgentToolHandler>
|
|
263
|
+
`,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
writeFileSync(
|
|
267
|
+
join(hooksDir, 'useAppAgentTools.ts'),
|
|
268
|
+
`import { usePlatformAgentTools } from '@puredesktop/puredesktop-ui-bridge/bridge/react/usePlatformAgentTools'
|
|
269
|
+
import { APP_AGENT_LOG_LABEL, APP_AGENT_TOOL_NAMES } from '../agents/catalog'
|
|
270
|
+
import { appAgentHandlers } from '../agents/handlers'
|
|
271
|
+
|
|
272
|
+
export function useAppAgentTools(ready: boolean): void {
|
|
273
|
+
usePlatformAgentTools({
|
|
274
|
+
ready,
|
|
275
|
+
tools: APP_AGENT_TOOL_NAMES,
|
|
276
|
+
logLabel: APP_AGENT_LOG_LABEL,
|
|
277
|
+
handlers: appAgentHandlers,
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
`,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
wireAgentToolHook(outDir)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function wireAgentToolHook(outDir) {
|
|
287
|
+
const appPath = join(outDir, 'src', 'App.tsx')
|
|
288
|
+
const source = readFileSync(appPath, 'utf8')
|
|
289
|
+
const importPattern = /import \{ useAppBoot \} from '\.\/hooks\/useAppBoot'\r?\n/
|
|
290
|
+
const callPattern = /( const \{ error: bridgeError, ready \} = usePlatformBridge\(\)\r?\n)/
|
|
291
|
+
|
|
292
|
+
if (!importPattern.test(source) || !callPattern.test(source)) {
|
|
293
|
+
console.error('Generated src/App.tsx does not match the agent tool scaffold insertion points.')
|
|
294
|
+
process.exit(1)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const withImport = source.replace(
|
|
298
|
+
importPattern,
|
|
299
|
+
match => `${match}import { useAppAgentTools } from './hooks/useAppAgentTools'\n`,
|
|
300
|
+
)
|
|
301
|
+
const withHook = withImport.replace(
|
|
302
|
+
callPattern,
|
|
303
|
+
(_match, line) => `${line} useAppAgentTools(ready)\n`,
|
|
304
|
+
)
|
|
305
|
+
writeFileSync(appPath, withHook)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function copyTemplate(srcDir, destDir, vars) {
|
|
109
309
|
for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
|
|
110
310
|
const srcPath = join(srcDir, entry.name)
|
|
111
311
|
const destName = entry.name.replace(/\.tpl$/u, '')
|
|
@@ -119,15 +319,30 @@ function copyTemplate(srcDir, destDir, vars) {
|
|
|
119
319
|
const rendered = raw.replace(/\{\{(\w+)\}\}/g, (_match, key) => vars[key] ?? '')
|
|
120
320
|
writeFileSync(destPath, rendered)
|
|
121
321
|
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const { targetDir, options } = parseArgs(process.argv.slice(2))
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const { targetDir, options } = parseArgs(process.argv.slice(2))
|
|
325
|
+
const structuredManifest = options.manifestJsonBase64
|
|
326
|
+
? readStructuredManifest(options.manifestJsonBase64, options.port)
|
|
327
|
+
: null
|
|
328
|
+
const structuredAgentsMd = options.agentsMdBase64
|
|
329
|
+
? readStructuredAgentsMd(options.agentsMdBase64)
|
|
330
|
+
: null
|
|
331
|
+
const dirName = targetDir
|
|
332
|
+
const structuredSlug = structuredManifest?.app.slug ?? ''
|
|
333
|
+
const structuredTitle = structuredManifest?.app.name ?? ''
|
|
334
|
+
if (structuredManifest && options.slug && options.slug !== structuredSlug) {
|
|
335
|
+
console.error('--slug must match manifest.app.slug when --manifest-json-base64 is used')
|
|
336
|
+
process.exit(1)
|
|
337
|
+
}
|
|
338
|
+
if (structuredManifest && options.name && options.name !== structuredTitle) {
|
|
339
|
+
console.error('--name must match manifest.app.name when --manifest-json-base64 is used')
|
|
340
|
+
process.exit(1)
|
|
341
|
+
}
|
|
342
|
+
const slug = structuredSlug || options.slug || slugify(dirName) || 'my-app'
|
|
343
|
+
const title = structuredTitle || options.name || titleCaseFromSlug(slug) || 'My App'
|
|
344
|
+
const pluginId = structuredManifest?.id ?? slug
|
|
345
|
+
const outDir = resolve(process.cwd(), dirName)
|
|
131
346
|
|
|
132
347
|
if (existsSync(outDir)) {
|
|
133
348
|
console.error(`Target already exists: ${outDir}`)
|
|
@@ -139,18 +354,34 @@ mkdirSync(outDir, { recursive: true })
|
|
|
139
354
|
const vars = {
|
|
140
355
|
APP_TITLE: title,
|
|
141
356
|
APP_SLUG: slug,
|
|
142
|
-
APP_NAME: slug,
|
|
143
|
-
PLUGIN_ID: pluginId,
|
|
144
|
-
APP_PORT: String(options.port),
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
PACKAGE_NAME: `@puredesktop/${slug}`,
|
|
148
|
-
}
|
|
357
|
+
APP_NAME: slug,
|
|
358
|
+
PLUGIN_ID: pluginId,
|
|
359
|
+
APP_PORT: String(options.port),
|
|
360
|
+
PACKAGE_NAME: `@puredesktop/${slug}`,
|
|
361
|
+
}
|
|
149
362
|
|
|
150
|
-
copyTemplate(TEMPLATE_DIR, outDir, vars)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
363
|
+
copyTemplate(TEMPLATE_DIR, outDir, vars)
|
|
364
|
+
|
|
365
|
+
if (structuredManifest) {
|
|
366
|
+
writeFileSync(
|
|
367
|
+
join(outDir, 'plugin.json'),
|
|
368
|
+
`${JSON.stringify(structuredManifest, null, 2)}\n`,
|
|
369
|
+
)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (structuredAgentsMd) {
|
|
373
|
+
writeFileSync(join(outDir, 'agents.md'), structuredAgentsMd)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const generatedManifest = readGeneratedManifest(outDir)
|
|
377
|
+
writeAgentToolScaffold(
|
|
378
|
+
outDir,
|
|
379
|
+
generatedManifest?.app?.slug ?? slug,
|
|
380
|
+
readAgentToolNames(generatedManifest),
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
console.log(`
|
|
384
|
+
Created ${title} at ${outDir}
|
|
154
385
|
|
|
155
386
|
cd ${dirName}
|
|
156
387
|
npm install
|
|
@@ -161,7 +392,7 @@ Before shipping:
|
|
|
161
392
|
npm run build
|
|
162
393
|
npm run puredesktop:check
|
|
163
394
|
|
|
164
|
-
Register in
|
|
395
|
+
Register in PureDesktop (File -> Register App...) with:
|
|
165
396
|
entrypoint: http://localhost:${options.port}
|
|
166
397
|
permissions: network, settings, agents
|
|
167
398
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@puredesktop/create-app",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Scaffold a
|
|
3
|
+
"version": "2.1.1",
|
|
4
|
+
"description": "Scaffold a PureDesktop plugin app (Vite + bridge SDK)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"create-app": "./bin/create-puredesktop-app.mjs",
|