@puredesktop/create-app 2.1.8 → 2.1.10

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.
@@ -4,12 +4,9 @@
4
4
  *
5
5
  * Published as @puredesktop/create-app -> npm create @puredesktop/app@latest
6
6
  */
7
- import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
8
- import { dirname, join, resolve } from 'node:path'
9
- import { fileURLToPath } from 'node:url'
7
+ import { scaffoldAppFromFiles } from '../lib/scaffold.mjs'
10
8
 
11
- const __dirname = dirname(fileURLToPath(import.meta.url))
12
- const TEMPLATE_DIR = join(__dirname, '..', 'template')
9
+ class UsageError extends Error {}
13
10
 
14
11
  function usage() {
15
12
  console.log(`Usage: npm create @puredesktop/app@latest [dir] [options]
@@ -18,10 +15,13 @@ Options:
18
15
  --name <title> Display name (default: derived from dir)
19
16
  --slug <slug> App slug for settings/bridge (default: dir name)
20
17
  --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
18
+ --target-path <path>
19
+ Scaffold into this path instead of [dir]
20
+ --allow-existing Allow target path to already exist
21
+ --manifest-json-file <path>
22
+ Read structured plugin.json manifest draft from a file
23
+ --agents-md-file <path>
24
+ Read structured agents.md content from a file
25
25
 
26
26
  Examples:
27
27
  npm create @puredesktop/app@latest my-tool
@@ -32,11 +32,13 @@ Examples:
32
32
  function parseArgs(argv) {
33
33
  const positional = []
34
34
  const options = {
35
+ agentsMdFile: '',
36
+ allowExisting: false,
37
+ manifestJsonFile: '',
35
38
  name: '',
36
- slug: '',
37
39
  port: 5300,
38
- manifestJsonBase64: '',
39
- agentsMdBase64: '',
40
+ slug: '',
41
+ targetPath: '',
40
42
  }
41
43
 
42
44
  for (let i = 0; i < argv.length; i += 1) {
@@ -57,333 +59,43 @@ function parseArgs(argv) {
57
59
  options.port = Number(argv[++i])
58
60
  continue
59
61
  }
60
- if (arg === '--manifest-json-base64') {
61
- options.manifestJsonBase64 = argv[++i] ?? ''
62
+ if (arg === '--target-path') {
63
+ options.targetPath = argv[++i] ?? ''
64
+ continue
65
+ }
66
+ if (arg === '--allow-existing') {
67
+ options.allowExisting = true
68
+ continue
69
+ }
70
+ if (arg === '--manifest-json-file') {
71
+ options.manifestJsonFile = argv[++i] ?? ''
62
72
  continue
63
73
  }
64
- if (arg === '--agents-md-base64') {
65
- options.agentsMdBase64 = argv[++i] ?? ''
74
+ if (arg === '--agents-md-file') {
75
+ options.agentsMdFile = argv[++i] ?? ''
66
76
  continue
67
77
  }
68
78
  if (arg.startsWith('-')) {
69
- console.error(`Unknown option: ${arg}`)
70
- usage()
71
- process.exit(1)
79
+ throw new UsageError(`Unknown option: ${arg}`)
72
80
  }
73
81
  positional.push(arg)
74
82
  }
75
83
 
76
- if (!Number.isInteger(options.port) || options.port <= 0) {
77
- console.error('--port must be a positive integer')
78
- process.exit(1)
79
- }
80
-
81
- return { targetDir: positional[0] ?? 'puredesktop-app', options }
82
- }
83
-
84
- function slugify(value) {
85
- return value
86
- .trim()
87
- .toLowerCase()
88
- .replace(/[^a-z0-9]+/g, '-')
89
- .replace(/^-+|-+$/g, '')
90
- }
91
-
92
- function titleCaseFromSlug(slug) {
93
- return slug
94
- .split('-')
95
- .filter(Boolean)
96
- .map(part => part.charAt(0).toUpperCase() + part.slice(1))
97
- .join(' ')
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)
84
+ if (options.targetPath && positional.length > 0) {
85
+ throw new UsageError('Pass either [dir] or --target-path, not both')
169
86
  }
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
87
 
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)
88
+ return {
89
+ options,
90
+ targetDir: options.targetPath || positional[0] || 'puredesktop-app',
210
91
  }
211
92
  }
212
93
 
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) {
309
- for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
310
- const srcPath = join(srcDir, entry.name)
311
- const destName = entry.name.replace(/\.tpl$/u, '')
312
- const destPath = join(destDir, destName)
313
- if (entry.isDirectory()) {
314
- mkdirSync(destPath, { recursive: true })
315
- copyTemplate(srcPath, destPath, vars)
316
- continue
317
- }
318
- const raw = readFileSync(srcPath, 'utf8')
319
- const rendered = raw.replace(/\{\{(\w+)\}\}/g, (_match, key) => vars[key] ?? '')
320
- writeFileSync(destPath, rendered)
321
- }
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)
346
-
347
- if (existsSync(outDir)) {
348
- console.error(`Target already exists: ${outDir}`)
349
- process.exit(1)
350
- }
351
-
352
- mkdirSync(outDir, { recursive: true })
353
-
354
- const vars = {
355
- APP_TITLE: title,
356
- APP_SLUG: slug,
357
- APP_NAME: slug,
358
- PLUGIN_ID: pluginId,
359
- APP_PORT: String(options.port),
360
- PACKAGE_NAME: `@puredesktop/${slug}`,
361
- }
362
-
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(`
94
+ function printSuccess({ outDir, port, targetDir, title }) {
95
+ console.log(`
384
96
  Created ${title} at ${outDir}
385
97
 
386
- cd ${dirName}
98
+ cd ${targetDir}
387
99
  npm install
388
100
  npm run dev
389
101
 
@@ -393,9 +105,29 @@ Before shipping:
393
105
  npm run puredesktop:check
394
106
 
395
107
  Register in PureDesktop (File -> Register App...) with:
396
- entrypoint: http://localhost:${options.port}
397
- permissions: network, settings, filesystem, agents
108
+ entrypoint: http://localhost:${port}
109
+ permissions: network, settings, filesystem, agents
398
110
 
399
- Build product surfaces in src/components, workflow hooks in src/hooks, and domain logic in src/lib.
400
- Keep src/App.tsx and src/components/AppShell.tsx thin.
401
- `)
111
+ Build product surfaces in src/components, workflow hooks in src/hooks, and domain logic in src/lib.
112
+ Keep src/App.tsx and src/components/AppShell.tsx thin.
113
+ `)
114
+ }
115
+
116
+ try {
117
+ const { options, targetDir } = parseArgs(process.argv.slice(2))
118
+ const result = scaffoldAppFromFiles({
119
+ agentsMdFile: options.agentsMdFile,
120
+ allowExisting: options.allowExisting,
121
+ manifestJsonFile: options.manifestJsonFile,
122
+ name: options.name,
123
+ port: options.port,
124
+ slug: options.slug,
125
+ targetDir,
126
+ })
127
+
128
+ printSuccess(result)
129
+ } catch (error) {
130
+ console.error(error instanceof Error ? error.message : String(error))
131
+ if (error instanceof UsageError) usage()
132
+ process.exit(1)
133
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@puredesktop/create-app",
3
- "version": "2.1.8",
3
+ "version": "2.1.10",
4
4
  "description": "Scaffold a PureDesktop plugin app (Vite + bridge SDK)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,10 +9,11 @@
9
9
  },
10
10
  "files": [
11
11
  "bin",
12
+ "lib",
12
13
  "template"
13
14
  ],
14
15
  "publishConfig": {
15
- "access": "restricted",
16
+ "access": "public",
16
17
  "registry": "https://registry.npmjs.org"
17
18
  },
18
19
  "engines": {
@@ -36,9 +36,9 @@ and declared app agent tools.
36
36
  - `src/App.tsx`: bridge and boot gates wrapped in `AppFrame`.
37
37
  - `src/bridge/platformBridge.ts`: local app bridge exports.
38
38
  - `src/hooks/useAppBoot.ts`: boot data loading after bridge readiness.
39
- - `src/components/AppShell.tsx`: thin app-owned composition root.
40
- - `src/components/StarterWorkspace.tsx`: temporary starter screen to replace
41
- when the first product surface exists.
39
+ - `src/components/AppShell.tsx`: thin app-owned composition root.
40
+ - `src/components/StarterWorkspace.tsx`: temporary starter screen to replace
41
+ when the first product surface exists.
42
42
  - `docs/`: builder-facing app, bridge, agent, and tool-handler docs.
43
43
 
44
44
  ## Builder Docs
@@ -13,10 +13,11 @@ Read these in order when building or repairing an app:
13
13
  2. [Plugin manifest](./plugin-manifest.md)
14
14
  3. [Bridge helpers](./bridge/README.md)
15
15
  4. [App agent](./agent.md)
16
- 5. [Tool handlers](./tool-handlers.md)
16
+ 5. [Tool handlers](./tool-handlers.md)
17
17
  6. [Adding an app agent tool](./howtos/adding-agent-tool.md)
18
- 7. [UI and components](./ui-and-components.md)
19
- 8. [Theme CSS variables](./theme-vars.md)
18
+ 7. [UI and components](./ui-and-components.md)
19
+ 8. [Using the document editor](./howtos/using-document-editor.md)
20
+ 9. [Theme CSS variables](./theme-vars.md)
20
21
 
21
22
  Key files in this scaffold:
22
23
 
@@ -26,11 +27,11 @@ Key files in this scaffold:
26
27
  - `src/App.tsx`: bridge readiness, boot loading, and `AppFrame` wrapper.
27
28
  - `src/bridge/platformBridge.ts`: local app bridge surface.
28
29
  - `src/hooks/useAppBoot.ts`: app boot data loading after the bridge is ready.
29
- - `src/components/AppShell.tsx`: thin app-owned composition root.
30
- - `src/components/StarterWorkspace.tsx`: temporary starter screen to replace
31
- when the first product surface exists.
32
- - `src/lib/starterWorkspace.ts`: starter data/helper example showing where
33
- reusable non-rendering logic belongs.
30
+ - `src/components/AppShell.tsx`: thin app-owned composition root.
31
+ - `src/components/StarterWorkspace.tsx`: temporary starter screen to replace
32
+ when the first product surface exists.
33
+ - `src/lib/starterWorkspace.ts`: starter data/helper example showing where
34
+ reusable non-rendering logic belongs.
34
35
  - `scripts/validate-puredesktop-app.mjs`: registration and tool scaffold check.
35
36
 
36
37
  Use `npm run puredesktop:check` after `npm run build` before registration.