@puredesktop/create-app 2.1.9 → 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,13 +15,16 @@ 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-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
-
26
- Examples:
27
- npm create @puredesktop/app@latest my-tool
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
+
26
+ Examples:
27
+ npm create @puredesktop/app@latest my-tool
28
28
  npm create @puredesktop/app@latest my-tool -- --name "My Tool" --slug mytool --port 5310
29
29
  `)
30
30
  }
@@ -32,12 +32,14 @@ Examples:
32
32
  function parseArgs(argv) {
33
33
  const positional = []
34
34
  const options = {
35
- name: '',
36
- slug: '',
37
- port: 5300,
38
- manifestJsonFile: '',
39
- agentsMdFile: '',
40
- }
35
+ agentsMdFile: '',
36
+ allowExisting: false,
37
+ manifestJsonFile: '',
38
+ name: '',
39
+ port: 5300,
40
+ slug: '',
41
+ targetPath: '',
42
+ }
41
43
 
42
44
  for (let i = 0; i < argv.length; i += 1) {
43
45
  const arg = argv[i]
@@ -57,333 +59,43 @@ function parseArgs(argv) {
57
59
  options.port = Number(argv[++i])
58
60
  continue
59
61
  }
60
- if (arg === '--manifest-json-file') {
61
- options.manifestJsonFile = argv[++i] ?? ''
62
- continue
63
- }
64
- if (arg === '--agents-md-file') {
65
- options.agentsMdFile = argv[++i] ?? ''
66
- continue
67
- }
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] ?? ''
72
+ continue
73
+ }
74
+ if (arg === '--agents-md-file') {
75
+ options.agentsMdFile = argv[++i] ?? ''
76
+ continue
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 readStructuredManifestFile(path, port) {
101
- const text = readUtf8File(path, '--manifest-json-file')
102
- return readStructuredManifestText(text, port, '--manifest-json-file')
103
- }
104
-
105
- function readStructuredManifestText(text, port, label) {
106
- let manifest
107
- try {
108
- manifest = JSON.parse(text)
109
- } catch (error) {
110
- console.error(`${label} must contain JSON: ${errorMessage(error)}`)
111
- process.exit(1)
112
- }
113
- return normalizeStructuredManifest(manifest, port, label)
114
- }
115
-
116
- function normalizeStructuredManifest(manifest, port, label) {
117
- if (!isPlainObject(manifest)) {
118
- console.error(`${label} must contain a manifest object`)
119
- process.exit(1)
120
- }
121
- if (manifest.schemaVersion !== 1) {
122
- console.error('manifest.schemaVersion must be 1')
123
- process.exit(1)
124
- }
125
- const id = requiredString(manifest.id, 'manifest.id')
126
- const name = requiredString(manifest.name, 'manifest.name')
127
- if (!isPlainObject(manifest.app)) {
128
- console.error('manifest.app is required')
129
- process.exit(1)
130
- }
131
- const appSlug = requiredString(manifest.app.slug, 'manifest.app.slug')
132
- const appName = requiredString(manifest.app.name, 'manifest.app.name')
133
- if (slugify(appSlug) !== appSlug) {
134
- console.error('manifest.app.slug must use lowercase letters, numbers, and hyphens')
135
- process.exit(1)
136
- }
137
-
138
- return stripUndefinedProperties({
139
- ...manifest,
140
- id,
141
- name,
142
- entrypoint: {
143
- kind: 'dev-url',
144
- url: `http://localhost:${port}`,
145
- },
146
- app: stripUndefinedProperties({
147
- ...manifest.app,
148
- slug: appSlug,
149
- name: appName,
150
- entrypoint: undefined,
151
- }),
152
- })
153
- }
154
-
155
- function readStructuredAgentsMdFile(path) {
156
- const text = readUtf8File(path, '--agents-md-file')
157
- return readStructuredAgentsMdText(text, '--agents-md-file')
158
- }
159
-
160
- function readStructuredAgentsMdText(text, label) {
161
- if (!text.trim()) {
162
- console.error(`${label} must contain non-empty agents.md content`)
163
- process.exit(1)
164
- }
165
- return text.endsWith('\n') ? text : `${text}\n`
166
- }
167
-
168
- function readUtf8File(path, label) {
169
- if (typeof path !== 'string' || !path.trim()) {
170
- console.error(`${label} requires a file path`)
171
- process.exit(1)
172
- }
173
- try {
174
- return readFileSync(resolve(path), 'utf8')
175
- } catch (error) {
176
- console.error(`${label} could not be read: ${errorMessage(error)}`)
177
- process.exit(1)
178
- }
179
- }
180
-
181
- function requiredString(value, label) {
182
- const text = typeof value === 'string' ? value.trim() : ''
183
- if (!text) {
184
- console.error(`${label} is required`)
185
- process.exit(1)
186
- }
187
- return text
188
- }
189
-
190
- function isPlainObject(value) {
191
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
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)
210
- }
211
- }
212
-
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)
84
+ if (options.targetPath && positional.length > 0) {
85
+ throw new UsageError('Pass either [dir] or --target-path, not both')
295
86
  }
296
87
 
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)
88
+ return {
89
+ options,
90
+ targetDir: options.targetPath || positional[0] || 'puredesktop-app',
321
91
  }
322
92
  }
323
93
 
324
- const { targetDir, options } = parseArgs(process.argv.slice(2))
325
- const structuredManifest = options.manifestJsonFile
326
- ? readStructuredManifestFile(options.manifestJsonFile, options.port)
327
- : null
328
- const structuredAgentsMd = options.agentsMdFile
329
- ? readStructuredAgentsMdFile(options.agentsMdFile)
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 a structured manifest 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 a structured manifest 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
110
+
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
+ })
398
127
 
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
- `)
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.9",
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.