@puredesktop/create-app 2.1.5 → 2.1.6
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 +319 -319
- package/package.json +1 -1
- package/template/README.md +52 -52
- package/template/agents.md +11 -11
- package/template/docs/README.md +30 -30
- package/template/docs/agent.md +58 -58
- package/template/docs/app-lifecycle.md +60 -60
- package/template/docs/bridge/README.md +84 -84
- package/template/docs/bridge/dialog.md +56 -56
- package/template/docs/bridge/fs.md +95 -95
- package/template/docs/bridge/network.md +38 -38
- package/template/docs/bridge/settings.md +63 -63
- package/template/docs/bridge/storage.md +48 -48
- package/template/docs/plugin-manifest.md +104 -104
- package/template/docs/theme-vars.md +138 -138
- package/template/docs/tool-handlers.md +122 -122
- package/template/docs/ui-and-components.md +57 -56
- package/template/package.json +10 -10
- package/template/plugin.json +20 -20
- package/template/src/App.tsx +4 -4
- package/template/src/bridge/platformBridge.ts +122 -122
- package/template/src/components/AppShell.tsx +9 -9
|
@@ -1,43 +1,43 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Scaffold an external PureDesktop plugin app.
|
|
3
|
+
* Scaffold an external PureDesktop plugin app.
|
|
4
4
|
*
|
|
5
|
-
* Published as @puredesktop/create-app -> npm create @puredesktop/app@latest
|
|
5
|
+
* Published as @puredesktop/create-app -> npm create @puredesktop/app@latest
|
|
6
6
|
*/
|
|
7
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
8
8
|
import { dirname, join, resolve } from 'node:path'
|
|
9
9
|
import { fileURLToPath } from 'node:url'
|
|
10
10
|
|
|
11
|
-
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
12
|
-
const TEMPLATE_DIR = join(__dirname, '..', 'template')
|
|
13
|
-
|
|
14
|
-
function usage() {
|
|
15
|
-
console.log(`Usage: npm create @puredesktop/app@latest [dir] [options]
|
|
16
|
-
|
|
17
|
-
Options:
|
|
18
|
-
--name <title> Display name (default: derived from dir)
|
|
19
|
-
--slug <slug> App slug for settings/bridge (default: dir name)
|
|
20
|
-
--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
|
|
25
|
-
|
|
26
|
-
Examples:
|
|
27
|
-
npm create @puredesktop/app@latest my-tool
|
|
28
|
-
npm create @puredesktop/app@latest my-tool -- --name "My Tool" --slug mytool --port 5310
|
|
29
|
-
`)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function parseArgs(argv) {
|
|
33
|
-
const positional = []
|
|
34
|
-
const options = {
|
|
35
|
-
name: '',
|
|
36
|
-
slug: '',
|
|
37
|
-
port: 5300,
|
|
38
|
-
manifestJsonBase64: '',
|
|
39
|
-
agentsMdBase64: '',
|
|
40
|
-
}
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
12
|
+
const TEMPLATE_DIR = join(__dirname, '..', 'template')
|
|
13
|
+
|
|
14
|
+
function usage() {
|
|
15
|
+
console.log(`Usage: npm create @puredesktop/app@latest [dir] [options]
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
--name <title> Display name (default: derived from dir)
|
|
19
|
+
--slug <slug> App slug for settings/bridge (default: dir name)
|
|
20
|
+
--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
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
npm create @puredesktop/app@latest my-tool
|
|
28
|
+
npm create @puredesktop/app@latest my-tool -- --name "My Tool" --slug mytool --port 5310
|
|
29
|
+
`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseArgs(argv) {
|
|
33
|
+
const positional = []
|
|
34
|
+
const options = {
|
|
35
|
+
name: '',
|
|
36
|
+
slug: '',
|
|
37
|
+
port: 5300,
|
|
38
|
+
manifestJsonBase64: '',
|
|
39
|
+
agentsMdBase64: '',
|
|
40
|
+
}
|
|
41
41
|
|
|
42
42
|
for (let i = 0; i < argv.length; i += 1) {
|
|
43
43
|
const arg = argv[i]
|
|
@@ -53,18 +53,18 @@ function parseArgs(argv) {
|
|
|
53
53
|
options.slug = argv[++i] ?? ''
|
|
54
54
|
continue
|
|
55
55
|
}
|
|
56
|
-
if (arg === '--port') {
|
|
57
|
-
options.port = Number(argv[++i])
|
|
58
|
-
continue
|
|
59
|
-
}
|
|
60
|
-
if (arg === '--manifest-json-base64') {
|
|
61
|
-
options.manifestJsonBase64 = argv[++i] ?? ''
|
|
62
|
-
continue
|
|
63
|
-
}
|
|
64
|
-
if (arg === '--agents-md-base64') {
|
|
65
|
-
options.agentsMdBase64 = argv[++i] ?? ''
|
|
66
|
-
continue
|
|
67
|
-
}
|
|
56
|
+
if (arg === '--port') {
|
|
57
|
+
options.port = Number(argv[++i])
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
if (arg === '--manifest-json-base64') {
|
|
61
|
+
options.manifestJsonBase64 = argv[++i] ?? ''
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
if (arg === '--agents-md-base64') {
|
|
65
|
+
options.agentsMdBase64 = argv[++i] ?? ''
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
68
|
if (arg.startsWith('-')) {
|
|
69
69
|
console.error(`Unknown option: ${arg}`)
|
|
70
70
|
usage()
|
|
@@ -89,223 +89,223 @@ function slugify(value) {
|
|
|
89
89
|
.replace(/^-+|-+$/g, '')
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
function titleCaseFromSlug(slug) {
|
|
92
|
+
function titleCaseFromSlug(slug) {
|
|
93
93
|
return slug
|
|
94
94
|
.split('-')
|
|
95
95
|
.filter(Boolean)
|
|
96
96
|
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
97
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)
|
|
169
|
-
}
|
|
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
|
-
|
|
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)
|
|
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)
|
|
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) {
|
|
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)
|
|
169
|
+
}
|
|
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
|
+
|
|
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)
|
|
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)
|
|
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
309
|
for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
|
|
310
310
|
const srcPath = join(srcDir, entry.name)
|
|
311
311
|
const destName = entry.name.replace(/\.tpl$/u, '')
|
|
@@ -319,30 +319,30 @@ function copyTemplate(srcDir, destDir, vars) {
|
|
|
319
319
|
const rendered = raw.replace(/\{\{(\w+)\}\}/g, (_match, key) => vars[key] ?? '')
|
|
320
320
|
writeFileSync(destPath, rendered)
|
|
321
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)
|
|
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
346
|
|
|
347
347
|
if (existsSync(outDir)) {
|
|
348
348
|
console.error(`Target already exists: ${outDir}`)
|
|
@@ -354,47 +354,47 @@ mkdirSync(outDir, { recursive: true })
|
|
|
354
354
|
const vars = {
|
|
355
355
|
APP_TITLE: title,
|
|
356
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(`
|
|
384
|
-
Created ${title} at ${outDir}
|
|
385
|
-
|
|
386
|
-
cd ${dirName}
|
|
387
|
-
npm install
|
|
388
|
-
npm run dev
|
|
389
|
-
|
|
390
|
-
Before shipping:
|
|
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(`
|
|
384
|
+
Created ${title} at ${outDir}
|
|
385
|
+
|
|
386
|
+
cd ${dirName}
|
|
387
|
+
npm install
|
|
388
|
+
npm run dev
|
|
389
|
+
|
|
390
|
+
Before shipping:
|
|
391
391
|
npm run typecheck
|
|
392
|
-
npm run build
|
|
393
|
-
npm run puredesktop:check
|
|
392
|
+
npm run build
|
|
393
|
+
npm run puredesktop:check
|
|
394
394
|
|
|
395
|
-
Register in PureDesktop (File -> Register App...) with:
|
|
395
|
+
Register in PureDesktop (File -> Register App...) with:
|
|
396
396
|
entrypoint: http://localhost:${options.port}
|
|
397
|
-
permissions: network, settings, agents
|
|
397
|
+
permissions: network, settings, agents
|
|
398
398
|
|
|
399
399
|
Build your UI in src/components/AppShell.tsx.
|
|
400
400
|
`)
|