@puredesktop/create-app 1.0.0-beta.2 → 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.
@@ -11,20 +11,24 @@ import { fileURLToPath } from 'node:url'
11
11
  const __dirname = dirname(fileURLToPath(import.meta.url))
12
12
  const TEMPLATE_DIR = join(__dirname, '..', 'template')
13
13
  const DEFAULT_UI = '@puredesktop/puredesktop-ui-bridge'
14
- const DEFAULT_UI_VERSION = '1.0.0-beta.2'
14
+ const DEFAULT_UI_VERSION = '2.1.0'
15
15
 
16
16
  function usage() {
17
17
  console.log(`Usage: npm create @puredesktop/app@latest [dir] [options]
18
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
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
+ --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
28
+
29
+ Examples:
30
+ npm create @puredesktop/app@latest my-tool
31
+ npm create @puredesktop/app@latest my-tool -- --name "My Tool" --slug mytool --port 5310
28
32
  `)
29
33
  }
30
34
 
@@ -45,11 +49,13 @@ function uiDependencySpec(spec) {
45
49
  function parseArgs(argv) {
46
50
  const positional = []
47
51
  const options = {
48
- name: '',
49
- slug: '',
50
- port: 5300,
51
- ui: `${DEFAULT_UI}@${DEFAULT_UI_VERSION}`,
52
- }
52
+ name: '',
53
+ slug: '',
54
+ port: 5300,
55
+ ui: `${DEFAULT_UI}@${DEFAULT_UI_VERSION}`,
56
+ manifestJsonBase64: '',
57
+ agentsMdBase64: '',
58
+ }
53
59
 
54
60
  for (let i = 0; i < argv.length; i += 1) {
55
61
  const arg = argv[i]
@@ -69,10 +75,18 @@ function parseArgs(argv) {
69
75
  options.port = Number(argv[++i])
70
76
  continue
71
77
  }
72
- if (arg === '--ui') {
73
- options.ui = argv[++i] ?? options.ui
74
- continue
75
- }
78
+ if (arg === '--ui') {
79
+ options.ui = argv[++i] ?? options.ui
80
+ continue
81
+ }
82
+ if (arg === '--manifest-json-base64') {
83
+ options.manifestJsonBase64 = argv[++i] ?? ''
84
+ continue
85
+ }
86
+ if (arg === '--agents-md-base64') {
87
+ options.agentsMdBase64 = argv[++i] ?? ''
88
+ continue
89
+ }
76
90
  if (arg.startsWith('-')) {
77
91
  console.error(`Unknown option: ${arg}`)
78
92
  usage()
@@ -97,15 +111,119 @@ function slugify(value) {
97
111
  .replace(/^-+|-+$/g, '')
98
112
  }
99
113
 
100
- function titleCaseFromSlug(slug) {
114
+ function titleCaseFromSlug(slug) {
101
115
  return slug
102
116
  .split('-')
103
117
  .filter(Boolean)
104
118
  .map(part => part.charAt(0).toUpperCase() + part.slice(1))
105
119
  .join(' ')
106
- }
107
-
108
- function copyTemplate(srcDir, destDir, vars) {
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)
167
+ }
168
+
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')
190
+ process.exit(1)
191
+ }
192
+ return text.endsWith('\n') ? text : `${text}\n`
193
+ }
194
+
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
202
+ }
203
+
204
+ function isPlainObject(value) {
205
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
206
+ }
207
+
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)
224
+ }
225
+
226
+ function copyTemplate(srcDir, destDir, vars) {
109
227
  for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
110
228
  const srcPath = join(srcDir, entry.name)
111
229
  const destName = entry.name.replace(/\.tpl$/u, '')
@@ -119,15 +237,31 @@ function copyTemplate(srcDir, destDir, vars) {
119
237
  const rendered = raw.replace(/\{\{(\w+)\}\}/g, (_match, key) => vars[key] ?? '')
120
238
  writeFileSync(destPath, rendered)
121
239
  }
122
- }
123
-
124
- const { targetDir, options } = parseArgs(process.argv.slice(2))
125
- const uiDep = uiDependencySpec(options.ui)
126
- 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)
240
+ }
241
+
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
249
+ const uiDep = uiDependencySpec(options.ui)
250
+ const dirName = targetDir
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')
255
+ process.exit(1)
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)
131
265
 
132
266
  if (existsSync(outDir)) {
133
267
  console.error(`Target already exists: ${outDir}`)
@@ -147,9 +281,20 @@ const vars = {
147
281
  PACKAGE_NAME: `@puredesktop/${slug}`,
148
282
  }
149
283
 
150
- copyTemplate(TEMPLATE_DIR, outDir, vars)
151
-
152
- console.log(`
284
+ copyTemplate(TEMPLATE_DIR, outDir, vars)
285
+
286
+ if (structuredManifest) {
287
+ writeFileSync(
288
+ join(outDir, 'plugin.json'),
289
+ `${JSON.stringify(structuredManifest, null, 2)}\n`,
290
+ )
291
+ }
292
+
293
+ if (structuredAgentsMd) {
294
+ writeFileSync(join(outDir, 'agents.md'), structuredAgentsMd)
295
+ }
296
+
297
+ console.log(`
153
298
  Created ${title} at ${outDir}
154
299
 
155
300
  cd ${dirName}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@puredesktop/create-app",
3
- "version": "1.0.0-beta.2",
3
+ "version": "2.1.0",
4
4
  "description": "Scaffold a PureScience Desktop plugin app (Vite + bridge SDK)",
5
5
  "type": "module",
6
6
  "bin": {