@puredesktop/create-app 1.0.0-beta.1 → 2.1.0
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 +265 -115
- 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
|
@@ -1,164 +1,314 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Scaffold an external PureScience Desktop plugin app.
|
|
4
|
-
*
|
|
5
|
-
* Published as @puredesktop/create-app → npm create @puredesktop/app@latest
|
|
6
|
-
*/
|
|
7
|
-
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
8
|
-
import { dirname, join, resolve } from 'node:path'
|
|
9
|
-
import { fileURLToPath } from 'node:url'
|
|
10
|
-
|
|
11
|
-
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
12
|
-
const TEMPLATE_DIR = join(__dirname, '..', 'template')
|
|
13
|
-
const DEFAULT_UI = '@puredesktop/puredesktop-ui-bridge'
|
|
14
|
-
const DEFAULT_UI_VERSION = '1.0
|
|
15
|
-
|
|
16
|
-
function usage() {
|
|
17
|
-
console.log(`Usage: npm create @puredesktop/app@latest [dir] [options]
|
|
18
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Scaffold an external PureScience Desktop plugin app.
|
|
4
|
+
*
|
|
5
|
+
* Published as @puredesktop/create-app → npm create @puredesktop/app@latest
|
|
6
|
+
*/
|
|
7
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
8
|
+
import { dirname, join, resolve } from 'node:path'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
10
|
+
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
12
|
+
const TEMPLATE_DIR = join(__dirname, '..', 'template')
|
|
13
|
+
const DEFAULT_UI = '@puredesktop/puredesktop-ui-bridge'
|
|
14
|
+
const DEFAULT_UI_VERSION = '2.1.0'
|
|
15
|
+
|
|
16
|
+
function usage() {
|
|
17
|
+
console.log(`Usage: npm create @puredesktop/app@latest [dir] [options]
|
|
18
|
+
|
|
19
19
|
Options:
|
|
20
20
|
--name <title> Display name (default: derived from dir)
|
|
21
21
|
--slug <slug> App slug for settings/bridge (default: dir name)
|
|
22
22
|
--port <number> Dev server port in plugin.json (default: 5300)
|
|
23
23
|
--ui <spec> UI package spec (default: ${DEFAULT_UI}@${DEFAULT_UI_VERSION})
|
|
24
|
+
--manifest-json-base64 <base64>
|
|
25
|
+
Structured plugin.json manifest draft for app-builder use
|
|
26
|
+
--agents-md-base64 <base64>
|
|
27
|
+
Structured agents.md content for app-builder use
|
|
24
28
|
|
|
25
29
|
Examples:
|
|
26
30
|
npm create @puredesktop/app@latest my-tool
|
|
27
31
|
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
|
-
}
|
|
44
|
-
|
|
45
|
-
function parseArgs(argv) {
|
|
46
|
-
const positional = []
|
|
47
|
-
const options = {
|
|
32
|
+
`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function uiDependencySpec(spec) {
|
|
36
|
+
if (spec.startsWith('file:')) {
|
|
37
|
+
return {
|
|
38
|
+
name: '@puredesktop/puredesktop-ui-bridge',
|
|
39
|
+
version: spec,
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const at = spec.lastIndexOf('@')
|
|
43
|
+
if (at > 1) {
|
|
44
|
+
return { name: spec.slice(0, at), version: spec.slice(at + 1) }
|
|
45
|
+
}
|
|
46
|
+
return { name: spec, version: DEFAULT_UI_VERSION }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseArgs(argv) {
|
|
50
|
+
const positional = []
|
|
51
|
+
const options = {
|
|
48
52
|
name: '',
|
|
49
53
|
slug: '',
|
|
50
54
|
port: 5300,
|
|
51
55
|
ui: `${DEFAULT_UI}@${DEFAULT_UI_VERSION}`,
|
|
56
|
+
manifestJsonBase64: '',
|
|
57
|
+
agentsMdBase64: '',
|
|
52
58
|
}
|
|
53
|
-
|
|
54
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
55
|
-
const arg = argv[i]
|
|
56
|
-
if (arg === '--help' || arg === '-h') {
|
|
57
|
-
usage()
|
|
58
|
-
process.exit(0)
|
|
59
|
-
}
|
|
60
|
-
if (arg === '--name') {
|
|
61
|
-
options.name = argv[++i] ?? ''
|
|
62
|
-
continue
|
|
63
|
-
}
|
|
64
|
-
if (arg === '--slug') {
|
|
65
|
-
options.slug = argv[++i] ?? ''
|
|
59
|
+
|
|
60
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
61
|
+
const arg = argv[i]
|
|
62
|
+
if (arg === '--help' || arg === '-h') {
|
|
63
|
+
usage()
|
|
64
|
+
process.exit(0)
|
|
65
|
+
}
|
|
66
|
+
if (arg === '--name') {
|
|
67
|
+
options.name = argv[++i] ?? ''
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
if (arg === '--slug') {
|
|
71
|
+
options.slug = argv[++i] ?? ''
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
if (arg === '--port') {
|
|
75
|
+
options.port = Number(argv[++i])
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
if (arg === '--ui') {
|
|
79
|
+
options.ui = argv[++i] ?? options.ui
|
|
66
80
|
continue
|
|
67
81
|
}
|
|
68
|
-
if (arg === '--
|
|
69
|
-
options.
|
|
82
|
+
if (arg === '--manifest-json-base64') {
|
|
83
|
+
options.manifestJsonBase64 = argv[++i] ?? ''
|
|
70
84
|
continue
|
|
71
85
|
}
|
|
72
|
-
if (arg === '--
|
|
73
|
-
options.
|
|
86
|
+
if (arg === '--agents-md-base64') {
|
|
87
|
+
options.agentsMdBase64 = argv[++i] ?? ''
|
|
74
88
|
continue
|
|
75
89
|
}
|
|
76
|
-
if (arg.startsWith('-')) {
|
|
77
|
-
console.error(`Unknown option: ${arg}`)
|
|
78
|
-
usage()
|
|
79
|
-
process.exit(1)
|
|
80
|
-
}
|
|
81
|
-
positional.push(arg)
|
|
90
|
+
if (arg.startsWith('-')) {
|
|
91
|
+
console.error(`Unknown option: ${arg}`)
|
|
92
|
+
usage()
|
|
93
|
+
process.exit(1)
|
|
94
|
+
}
|
|
95
|
+
positional.push(arg)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!Number.isInteger(options.port) || options.port <= 0) {
|
|
99
|
+
console.error('--port must be a positive integer')
|
|
100
|
+
process.exit(1)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { targetDir: positional[0] ?? 'puredesktop-app', options }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function slugify(value) {
|
|
107
|
+
return value
|
|
108
|
+
.trim()
|
|
109
|
+
.toLowerCase()
|
|
110
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
111
|
+
.replace(/^-+|-+$/g, '')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function titleCaseFromSlug(slug) {
|
|
115
|
+
return slug
|
|
116
|
+
.split('-')
|
|
117
|
+
.filter(Boolean)
|
|
118
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
119
|
+
.join(' ')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function decodeBase64Utf8(value, label) {
|
|
123
|
+
if (!isBase64(value)) {
|
|
124
|
+
console.error(`${label} must be valid base64`)
|
|
125
|
+
process.exit(1)
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
return Buffer.from(value, 'base64').toString('utf8')
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error(`${label} must be valid base64: ${errorMessage(error)}`)
|
|
131
|
+
process.exit(1)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function readStructuredManifest(value, port) {
|
|
136
|
+
const text = decodeBase64Utf8(value, '--manifest-json-base64')
|
|
137
|
+
let manifest
|
|
138
|
+
try {
|
|
139
|
+
manifest = JSON.parse(text)
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error(`--manifest-json-base64 must decode to JSON: ${errorMessage(error)}`)
|
|
142
|
+
process.exit(1)
|
|
143
|
+
}
|
|
144
|
+
return normalizeStructuredManifest(manifest, port)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function normalizeStructuredManifest(manifest, port) {
|
|
148
|
+
if (!isPlainObject(manifest)) {
|
|
149
|
+
console.error('--manifest-json-base64 must decode to a manifest object')
|
|
150
|
+
process.exit(1)
|
|
151
|
+
}
|
|
152
|
+
if (manifest.schemaVersion !== 1) {
|
|
153
|
+
console.error('manifest.schemaVersion must be 1')
|
|
154
|
+
process.exit(1)
|
|
155
|
+
}
|
|
156
|
+
const id = requiredString(manifest.id, 'manifest.id')
|
|
157
|
+
const name = requiredString(manifest.name, 'manifest.name')
|
|
158
|
+
if (!isPlainObject(manifest.app)) {
|
|
159
|
+
console.error('manifest.app is required')
|
|
160
|
+
process.exit(1)
|
|
161
|
+
}
|
|
162
|
+
const appSlug = requiredString(manifest.app.slug, 'manifest.app.slug')
|
|
163
|
+
const appName = requiredString(manifest.app.name, 'manifest.app.name')
|
|
164
|
+
if (slugify(appSlug) !== appSlug) {
|
|
165
|
+
console.error('manifest.app.slug must use lowercase letters, numbers, and hyphens')
|
|
166
|
+
process.exit(1)
|
|
82
167
|
}
|
|
83
168
|
|
|
84
|
-
|
|
85
|
-
|
|
169
|
+
return stripUndefinedProperties({
|
|
170
|
+
...manifest,
|
|
171
|
+
id,
|
|
172
|
+
name,
|
|
173
|
+
entrypoint: {
|
|
174
|
+
kind: 'dev-url',
|
|
175
|
+
url: `http://localhost:${port}`,
|
|
176
|
+
},
|
|
177
|
+
app: stripUndefinedProperties({
|
|
178
|
+
...manifest.app,
|
|
179
|
+
slug: appSlug,
|
|
180
|
+
name: appName,
|
|
181
|
+
entrypoint: undefined,
|
|
182
|
+
}),
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function readStructuredAgentsMd(value) {
|
|
187
|
+
const text = decodeBase64Utf8(value, '--agents-md-base64')
|
|
188
|
+
if (!text.trim()) {
|
|
189
|
+
console.error('--agents-md-base64 must decode to non-empty agents.md content')
|
|
86
190
|
process.exit(1)
|
|
87
191
|
}
|
|
192
|
+
return text.endsWith('\n') ? text : `${text}\n`
|
|
193
|
+
}
|
|
88
194
|
|
|
89
|
-
|
|
195
|
+
function requiredString(value, label) {
|
|
196
|
+
const text = typeof value === 'string' ? value.trim() : ''
|
|
197
|
+
if (!text) {
|
|
198
|
+
console.error(`${label} is required`)
|
|
199
|
+
process.exit(1)
|
|
200
|
+
}
|
|
201
|
+
return text
|
|
90
202
|
}
|
|
91
203
|
|
|
92
|
-
function
|
|
93
|
-
return value
|
|
94
|
-
.trim()
|
|
95
|
-
.toLowerCase()
|
|
96
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
97
|
-
.replace(/^-+|-+$/g, '')
|
|
204
|
+
function isPlainObject(value) {
|
|
205
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
98
206
|
}
|
|
99
207
|
|
|
100
|
-
function
|
|
101
|
-
return
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
208
|
+
function isBase64(value) {
|
|
209
|
+
if (typeof value !== 'string' || !value.trim()) return false
|
|
210
|
+
const normalized = value.trim()
|
|
211
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/u.test(normalized)) return false
|
|
212
|
+
if (normalized.length % 4 !== 0) return false
|
|
213
|
+
return Buffer.from(normalized, 'base64').toString('base64') === normalized
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function stripUndefinedProperties(value) {
|
|
217
|
+
return Object.fromEntries(
|
|
218
|
+
Object.entries(value).filter(([, entry]) => entry !== undefined),
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function errorMessage(error) {
|
|
223
|
+
return error instanceof Error ? error.message : String(error)
|
|
106
224
|
}
|
|
107
225
|
|
|
108
226
|
function copyTemplate(srcDir, destDir, vars) {
|
|
109
|
-
for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
|
|
110
|
-
const srcPath = join(srcDir, entry.name)
|
|
111
|
-
const destName = entry.name.replace(/\.tpl$/u, '')
|
|
112
|
-
const destPath = join(destDir, destName)
|
|
113
|
-
if (entry.isDirectory()) {
|
|
114
|
-
mkdirSync(destPath, { recursive: true })
|
|
115
|
-
copyTemplate(srcPath, destPath, vars)
|
|
116
|
-
continue
|
|
117
|
-
}
|
|
118
|
-
const raw = readFileSync(srcPath, 'utf8')
|
|
119
|
-
const rendered = raw.replace(/\{\{(\w+)\}\}/g, (_match, key) => vars[key] ?? '')
|
|
120
|
-
writeFileSync(destPath, rendered)
|
|
121
|
-
}
|
|
227
|
+
for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
|
|
228
|
+
const srcPath = join(srcDir, entry.name)
|
|
229
|
+
const destName = entry.name.replace(/\.tpl$/u, '')
|
|
230
|
+
const destPath = join(destDir, destName)
|
|
231
|
+
if (entry.isDirectory()) {
|
|
232
|
+
mkdirSync(destPath, { recursive: true })
|
|
233
|
+
copyTemplate(srcPath, destPath, vars)
|
|
234
|
+
continue
|
|
235
|
+
}
|
|
236
|
+
const raw = readFileSync(srcPath, 'utf8')
|
|
237
|
+
const rendered = raw.replace(/\{\{(\w+)\}\}/g, (_match, key) => vars[key] ?? '')
|
|
238
|
+
writeFileSync(destPath, rendered)
|
|
239
|
+
}
|
|
122
240
|
}
|
|
123
241
|
|
|
124
242
|
const { targetDir, options } = parseArgs(process.argv.slice(2))
|
|
243
|
+
const structuredManifest = options.manifestJsonBase64
|
|
244
|
+
? readStructuredManifest(options.manifestJsonBase64, options.port)
|
|
245
|
+
: null
|
|
246
|
+
const structuredAgentsMd = options.agentsMdBase64
|
|
247
|
+
? readStructuredAgentsMd(options.agentsMdBase64)
|
|
248
|
+
: null
|
|
125
249
|
const uiDep = uiDependencySpec(options.ui)
|
|
126
250
|
const dirName = targetDir
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (existsSync(outDir)) {
|
|
133
|
-
console.error(`Target already exists: ${outDir}`)
|
|
251
|
+
const structuredSlug = structuredManifest?.app.slug ?? ''
|
|
252
|
+
const structuredTitle = structuredManifest?.app.name ?? ''
|
|
253
|
+
if (structuredManifest && options.slug && options.slug !== structuredSlug) {
|
|
254
|
+
console.error('--slug must match manifest.app.slug when --manifest-json-base64 is used')
|
|
134
255
|
process.exit(1)
|
|
135
256
|
}
|
|
257
|
+
if (structuredManifest && options.name && options.name !== structuredTitle) {
|
|
258
|
+
console.error('--name must match manifest.app.name when --manifest-json-base64 is used')
|
|
259
|
+
process.exit(1)
|
|
260
|
+
}
|
|
261
|
+
const slug = structuredSlug || options.slug || slugify(dirName) || 'my-app'
|
|
262
|
+
const title = structuredTitle || options.name || titleCaseFromSlug(slug) || 'My App'
|
|
263
|
+
const pluginId = structuredManifest?.id ?? slug
|
|
264
|
+
const outDir = resolve(process.cwd(), dirName)
|
|
265
|
+
|
|
266
|
+
if (existsSync(outDir)) {
|
|
267
|
+
console.error(`Target already exists: ${outDir}`)
|
|
268
|
+
process.exit(1)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
mkdirSync(outDir, { recursive: true })
|
|
272
|
+
|
|
273
|
+
const vars = {
|
|
274
|
+
APP_TITLE: title,
|
|
275
|
+
APP_SLUG: slug,
|
|
276
|
+
APP_NAME: slug,
|
|
277
|
+
PLUGIN_ID: pluginId,
|
|
278
|
+
APP_PORT: String(options.port),
|
|
279
|
+
UI_PACKAGE_NAME: uiDep.name,
|
|
280
|
+
UI_PACKAGE_VERSION: uiDep.version,
|
|
281
|
+
PACKAGE_NAME: `@puredesktop/${slug}`,
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
copyTemplate(TEMPLATE_DIR, outDir, vars)
|
|
136
285
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
APP_NAME: slug,
|
|
143
|
-
PLUGIN_ID: pluginId,
|
|
144
|
-
APP_PORT: String(options.port),
|
|
145
|
-
UI_PACKAGE_NAME: uiDep.name,
|
|
146
|
-
UI_PACKAGE_VERSION: uiDep.version,
|
|
147
|
-
PACKAGE_NAME: `@puredesktop/${slug}`,
|
|
286
|
+
if (structuredManifest) {
|
|
287
|
+
writeFileSync(
|
|
288
|
+
join(outDir, 'plugin.json'),
|
|
289
|
+
`${JSON.stringify(structuredManifest, null, 2)}\n`,
|
|
290
|
+
)
|
|
148
291
|
}
|
|
149
292
|
|
|
150
|
-
|
|
293
|
+
if (structuredAgentsMd) {
|
|
294
|
+
writeFileSync(join(outDir, 'agents.md'), structuredAgentsMd)
|
|
295
|
+
}
|
|
151
296
|
|
|
152
297
|
console.log(`
|
|
153
|
-
Created ${title} at ${outDir}
|
|
154
|
-
|
|
298
|
+
Created ${title} at ${outDir}
|
|
299
|
+
|
|
155
300
|
cd ${dirName}
|
|
156
301
|
npm install
|
|
157
302
|
npm run dev
|
|
158
303
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
304
|
+
Before shipping:
|
|
305
|
+
npm run typecheck
|
|
306
|
+
npm run build
|
|
307
|
+
npm run puredesktop:check
|
|
308
|
+
|
|
309
|
+
Register in PureScience Desktop (File → Register App…) with:
|
|
310
|
+
entrypoint: http://localhost:${options.port}
|
|
311
|
+
permissions: network, settings, agents
|
|
312
|
+
|
|
313
|
+
Build your UI in src/components/AppShell.tsx.
|
|
314
|
+
`)
|