@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.
- package/bin/create-puredesktop-app.mjs +180 -35
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
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
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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}
|