@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.
@@ -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.0-beta.1'
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 === '--port') {
69
- options.port = Number(argv[++i])
82
+ if (arg === '--manifest-json-base64') {
83
+ options.manifestJsonBase64 = argv[++i] ?? ''
70
84
  continue
71
85
  }
72
- if (arg === '--ui') {
73
- options.ui = argv[++i] ?? options.ui
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
- if (!Number.isInteger(options.port) || options.port <= 0) {
85
- console.error('--port must be a positive integer')
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
- return { targetDir: positional[0] ?? 'puredesktop-app', options }
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 slugify(value) {
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 titleCaseFromSlug(slug) {
101
- return slug
102
- .split('-')
103
- .filter(Boolean)
104
- .map(part => part.charAt(0).toUpperCase() + part.slice(1))
105
- .join(' ')
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 slug = options.slug || slugify(dirName) || 'my-app'
128
- const title = options.name || titleCaseFromSlug(slug) || 'My App'
129
- const pluginId = slug
130
- const outDir = resolve(process.cwd(), dirName)
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
- mkdirSync(outDir, { recursive: true })
138
-
139
- const vars = {
140
- APP_TITLE: title,
141
- APP_SLUG: slug,
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
- copyTemplate(TEMPLATE_DIR, outDir, vars)
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
- Register in PureScience Desktop (File → Register App…) with:
160
- entrypoint: http://localhost:${options.port}
161
- permissions: network (+ settings if you use app settings)
162
-
163
- Build your UI in src/components/AppShell.tsx.
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
+ `)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@puredesktop/create-app",
3
- "version": "1.0.0-beta.1",
3
+ "version": "2.1.0",
4
4
  "description": "Scaffold a PureScience Desktop plugin app (Vite + bridge SDK)",
5
5
  "type": "module",
6
6
  "bin": {