@puredesktop/create-app 2.1.7 → 2.1.9

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.
@@ -18,13 +18,13 @@ Options:
18
18
  --name <title> Display name (default: derived from dir)
19
19
  --slug <slug> App slug for settings/bridge (default: dir name)
20
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
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,12 @@ Examples:
32
32
  function parseArgs(argv) {
33
33
  const positional = []
34
34
  const options = {
35
- name: '',
36
- slug: '',
37
- port: 5300,
38
- manifestJsonBase64: '',
39
- agentsMdBase64: '',
40
- }
35
+ name: '',
36
+ slug: '',
37
+ port: 5300,
38
+ manifestJsonFile: '',
39
+ agentsMdFile: '',
40
+ }
41
41
 
42
42
  for (let i = 0; i < argv.length; i += 1) {
43
43
  const arg = argv[i]
@@ -57,14 +57,14 @@ function parseArgs(argv) {
57
57
  options.port = Number(argv[++i])
58
58
  continue
59
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
- }
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
+ }
68
68
  if (arg.startsWith('-')) {
69
69
  console.error(`Unknown option: ${arg}`)
70
70
  usage()
@@ -97,36 +97,27 @@ function titleCaseFromSlug(slug) {
97
97
  .join(' ')
98
98
  }
99
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
- }
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
+ }
130
121
  if (manifest.schemaVersion !== 1) {
131
122
  console.error('manifest.schemaVersion must be 1')
132
123
  process.exit(1)
@@ -161,14 +152,31 @@ function normalizeStructuredManifest(manifest, port) {
161
152
  })
162
153
  }
163
154
 
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
- }
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
+ }
172
180
 
173
181
  function requiredString(value, label) {
174
182
  const text = typeof value === 'string' ? value.trim() : ''
@@ -183,15 +191,7 @@ function isPlainObject(value) {
183
191
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
184
192
  }
185
193
 
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) {
194
+ function stripUndefinedProperties(value) {
195
195
  return Object.fromEntries(
196
196
  Object.entries(value).filter(([, entry]) => entry !== undefined),
197
197
  )
@@ -321,24 +321,24 @@ function copyTemplate(srcDir, destDir, vars) {
321
321
  }
322
322
  }
323
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
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
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
- }
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
342
  const slug = structuredSlug || options.slug || slugify(dirName) || 'my-app'
343
343
  const title = structuredTitle || options.name || titleCaseFromSlug(slug) || 'My App'
344
344
  const pluginId = structuredManifest?.id ?? slug
@@ -394,7 +394,8 @@ Before shipping:
394
394
 
395
395
  Register in PureDesktop (File -> Register App...) with:
396
396
  entrypoint: http://localhost:${options.port}
397
- permissions: network, settings, agents
397
+ permissions: network, settings, filesystem, agents
398
398
 
399
- Build your UI in src/components/AppShell.tsx.
400
- `)
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
+ `)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@puredesktop/create-app",
3
- "version": "2.1.7",
3
+ "version": "2.1.9",
4
4
  "description": "Scaffold a PureDesktop plugin app (Vite + bridge SDK)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,7 +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`: first app-owned UI surface.
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.
40
42
  - `docs/`: builder-facing app, bridge, agent, and tool-handler docs.
41
43
 
42
44
  ## Builder Docs
@@ -14,8 +14,9 @@ Read these in order when building or repairing an app:
14
14
  3. [Bridge helpers](./bridge/README.md)
15
15
  4. [App agent](./agent.md)
16
16
  5. [Tool handlers](./tool-handlers.md)
17
- 6. [UI and components](./ui-and-components.md)
18
- 7. [Theme CSS variables](./theme-vars.md)
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)
19
20
 
20
21
  Key files in this scaffold:
21
22
 
@@ -25,7 +26,11 @@ Key files in this scaffold:
25
26
  - `src/App.tsx`: bridge readiness, boot loading, and `AppFrame` wrapper.
26
27
  - `src/bridge/platformBridge.ts`: local app bridge surface.
27
28
  - `src/hooks/useAppBoot.ts`: app boot data loading after the bridge is ready.
28
- - `src/components/AppShell.tsx`: first app-owned UI surface.
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.
29
34
  - `scripts/validate-puredesktop-app.mjs`: registration and tool scaffold check.
30
35
 
31
36
  Use `npm run puredesktop:check` after `npm run build` before registration.
@@ -0,0 +1,106 @@
1
+ # Adding An App Agent Tool
2
+
3
+ App agent tools are a three-file contract. A tool is not available until the
4
+ manifest, registration catalog, and runtime handler all contain the same name.
5
+
6
+ ## Edit Sequence
7
+
8
+ 1. Add the tool declaration to `plugin.json` under `app.agents.tools`.
9
+ Include a clear `description`, an `inputSchema`, and `requiresApproval`.
10
+ Set `requiresApproval: true` for writes that mutate app data, files,
11
+ external services, or broad app state.
12
+
13
+ 2. Add the same tool name to `src/agents/catalog.ts`.
14
+ `APP_AGENT_TOOL_NAMES` is the list registered with the shell at runtime. Keep
15
+ it in the same order as `plugin.json` `app.agents.tools`.
16
+
17
+ 3. Add the same tool name to `src/agents/handlers/index.ts`.
18
+ Implement the handler with app-owned state or domain helpers. Return compact
19
+ JSON for reads and a short result for writes. Return
20
+ `agentToolErrorContent(message)` for invalid input the assistant can correct.
21
+
22
+ 4. Update `agents.md` when the app assistant needs to know when or how to use
23
+ the tool.
24
+
25
+ 5. Run checks:
26
+
27
+ ```bash
28
+ npm run typecheck
29
+ npm run build
30
+ npm run puredesktop:check
31
+ ```
32
+
33
+ During dev mode, `npm run typecheck` is enough to catch TypeScript errors, but
34
+ `npm run puredesktop:check` only proves the final package after `npm run build`
35
+ refreshes `dist`.
36
+
37
+ ## Existing Tool Scaffold
38
+
39
+ For apps that already have tools, these files already exist:
40
+
41
+ ```text
42
+ src/
43
+ agents/
44
+ catalog.ts
45
+ handlers/
46
+ index.ts
47
+ hooks/
48
+ useAppAgentTools.ts
49
+ ```
50
+
51
+ Only edit `plugin.json`, `src/agents/catalog.ts`,
52
+ `src/agents/handlers/index.ts`, and optionally `agents.md`.
53
+
54
+ ## First Tool In An App
55
+
56
+ If `src/agents/` does not exist, create the scaffold:
57
+
58
+ ```ts
59
+ // src/agents/catalog.ts
60
+ export const APP_AGENT_TOOL_NAMES = ['listItems'] as const
61
+
62
+ export const APP_AGENT_LOG_LABEL = '{{APP_SLUG}}'
63
+ ```
64
+
65
+ ```ts
66
+ // src/agents/handlers/index.ts
67
+ import { agentToolErrorContent } from '@puredesktop/puredesktop-ui-bridge/bridge/agentToolHelpers'
68
+ import type { AgentToolHandler } from '@puredesktop/puredesktop-ui-bridge/bridge/react/usePlatformAgentTools'
69
+
70
+ const listItems: AgentToolHandler = async () => {
71
+ return agentToolErrorContent('listItems is not implemented yet.')
72
+ }
73
+
74
+ export const appAgentHandlers = {
75
+ listItems,
76
+ } satisfies Record<string, AgentToolHandler>
77
+ ```
78
+
79
+ ```ts
80
+ // src/hooks/useAppAgentTools.ts
81
+ import { usePlatformAgentTools } from '@puredesktop/puredesktop-ui-bridge/bridge/react/usePlatformAgentTools'
82
+ import { APP_AGENT_LOG_LABEL, APP_AGENT_TOOL_NAMES } from '../agents/catalog'
83
+ import { appAgentHandlers } from '../agents/handlers'
84
+
85
+ export function useAppAgentTools(ready: boolean): void {
86
+ usePlatformAgentTools({
87
+ ready,
88
+ tools: APP_AGENT_TOOL_NAMES,
89
+ logLabel: APP_AGENT_LOG_LABEL,
90
+ handlers: appAgentHandlers,
91
+ })
92
+ }
93
+ ```
94
+
95
+ Then wire the hook in `src/App.tsx` immediately after `usePlatformBridge()`:
96
+
97
+ ```ts
98
+ const { error: bridgeError, ready } = usePlatformBridge()
99
+ useAppAgentTools(ready)
100
+ ```
101
+
102
+ ## Common Failure
103
+
104
+ Do not update only `plugin.json` and the handler. If `APP_AGENT_TOOL_NAMES`
105
+ does not include the new tool, the iframe never registers it with the shell. The
106
+ assistant will see a declared tool that the running app cannot handle.
@@ -0,0 +1,242 @@
1
+ # Persisting App State
2
+
3
+ PureDesktop apps persist non-document state through UI bridge helpers. This
4
+ state is app-owned: the app defines the TypeScript type, default value, parser,
5
+ and update commands.
6
+
7
+ ## State Surfaces
8
+
9
+ | State | Helper | Permission |
10
+ | --- | --- | --- |
11
+ | Small app preferences | `usePlatformAppSettings` | `settings` |
12
+ | Internal JSON records | `usePlatformJsonStore` | `filesystem` |
13
+
14
+ Use document lifecycle APIs for user-visible documents, drafts, recents, rename,
15
+ duplicate, and open/save workflows.
16
+
17
+ ## Generated App Wiring
18
+
19
+ The generated app already has the bridge readiness boundary:
20
+
21
+ - `src/App.tsx` calls `usePlatformBridge()` and receives `ready`.
22
+ - `src/App.tsx` calls `useAppBoot(ready || standaloneDev)`.
23
+ - `src/hooks/useAppBoot.ts` loads startup settings through the app bridge.
24
+ - `src/components/AppShell.tsx` receives boot data as props.
25
+
26
+ Use that structure instead of adding bridge calls inside components.
27
+
28
+ When state is read once at startup, `useAppBoot` can load it and pass it down.
29
+ When state can change while the app is open, create a dedicated hook in
30
+ `src/hooks/` that calls the platform helper and exposes typed commands.
31
+
32
+ ## Add Editable App Settings
33
+
34
+ 1. Create or reuse the domain folder under `src/lib/<domain>/`.
35
+ 2. Define the settings type, default value, and parser in that domain folder.
36
+ 3. Create `src/hooks/use<Domain>Settings.ts`.
37
+ 4. Inside that hook, call `usePlatformAppSettings`.
38
+ 5. Pass `APP_SLUG` and the bridge `ready` value to the hook.
39
+ 6. Return typed settings plus app-specific command functions.
40
+ 7. Components call those command functions; they do not build settings patches.
41
+
42
+ Hook shape:
43
+
44
+ ```text
45
+ use real domain settings type
46
+ call usePlatformAppSettings with APP_SLUG, ready, default settings, parser
47
+ return settings, loading, error
48
+ return field-specific commands that call patchSettings
49
+ ```
50
+
51
+ Field-specific commands own the settings patch. Components call the command; they
52
+ do not assemble the patch object.
53
+
54
+ ## Add An Internal JSON Store
55
+
56
+ 1. Add `filesystem` to `plugin.json`.
57
+ 2. Create or reuse the domain folder under `src/lib/<domain>/`.
58
+ 3. Define the store type, empty value, parser, and pure update helpers in that
59
+ domain folder.
60
+ 4. Create `src/hooks/use<Domain>Store.ts`.
61
+ 5. Inside that hook, call `usePlatformJsonStore`.
62
+ 6. Use a file name that includes `APP_SLUG` and the real persisted record name.
63
+ 7. Return typed store state plus app-specific command functions.
64
+ 8. Components call those command functions; they do not edit raw JSON objects.
65
+
66
+ Hook shape:
67
+
68
+ ```text
69
+ use real domain store type
70
+ call usePlatformJsonStore with APP_SLUG, ready, file name, empty store, parser
71
+ return store, loading, saving, error
72
+ return domain commands that call updateValue with pure update functions
73
+ ```
74
+
75
+ Domain commands own the store update. Components call the command; they do not
76
+ edit raw JSON objects. Store update functions must return new store objects.
77
+
78
+ ## App Settings
79
+
80
+ Import:
81
+
82
+ ```ts
83
+ import { usePlatformAppSettings } from '@puredesktop/puredesktop-ui-bridge/bridge/react/usePlatformAppSettings'
84
+ ```
85
+
86
+ Use for small app preferences such as current mode, sorting, filtering, and
87
+ lightweight options.
88
+
89
+ Options:
90
+
91
+ ```ts
92
+ {
93
+ appSlug: string | null
94
+ enabled?: boolean
95
+ initialSettings?: TSettings
96
+ parse?: (value: Record<string, unknown>) => TSettings
97
+ }
98
+ ```
99
+
100
+ Returns:
101
+
102
+ ```ts
103
+ {
104
+ settings: TSettings
105
+ loading: boolean
106
+ error: Error | null
107
+ patchSettings: (patch: Record<string, unknown>) => Promise<TSettings>
108
+ reload: () => void
109
+ }
110
+ ```
111
+
112
+ Behavior:
113
+
114
+ - loads through `settings.app.get`;
115
+ - saves through `settings.app.update`;
116
+ - `patchSettings` shallow-merges the patch in shell preferences;
117
+ - the shell stores app settings under the current app slug;
118
+ - the app parser converts persisted `Record<string, unknown>` into the app's
119
+ typed settings object.
120
+
121
+ App ownership:
122
+
123
+ - keep the settings type in app code;
124
+ - keep defaults and parsing in app code;
125
+ - expose typed settings and typed commands to components;
126
+ - use app settings for preferences, not large record collections.
127
+
128
+ ## JSON Store
129
+
130
+ Import:
131
+
132
+ ```ts
133
+ import { usePlatformJsonStore } from '@puredesktop/puredesktop-ui-bridge/bridge/react/usePlatformJsonStore'
134
+ ```
135
+
136
+ Use for internal app records that are larger than settings and are not
137
+ user-visible documents.
138
+
139
+ Options:
140
+
141
+ ```ts
142
+ {
143
+ appSlug: string | null
144
+ fileName: `${string}.json` | null
145
+ initialValue: TValue
146
+ enabled?: boolean
147
+ parse?: (value: unknown) => TValue
148
+ }
149
+ ```
150
+
151
+ Returns:
152
+
153
+ ```ts
154
+ {
155
+ value: TValue
156
+ path: string | null
157
+ loading: boolean
158
+ saving: boolean
159
+ error: Error | null
160
+ saveValue: (next: TValue) => Promise<TValue>
161
+ updateValue: (updater: (current: TValue) => TValue) => Promise<TValue>
162
+ reload: () => void
163
+ }
164
+ ```
165
+
166
+ Behavior:
167
+
168
+ - loads through `storage.readJson`;
169
+ - saves through `storage.writeJson`;
170
+ - missing files load as `initialValue`;
171
+ - saved values replace the full JSON value;
172
+ - `updateValue` queues writes and applies each updater to the latest in-memory
173
+ value;
174
+ - `fileName` must be a single `.json` file name;
175
+ - the JSON file is stored in the shell-managed appdata folder.
176
+
177
+ App ownership:
178
+
179
+ - include `filesystem` in `plugin.json`;
180
+ - keep the store type in app code;
181
+ - keep defaults, parsing, and pure update functions in app code;
182
+ - use an app-prefixed file name with the real persisted record name;
183
+ - expose typed state and typed commands to components;
184
+ - never mutate the current store object in place.
185
+
186
+ ## File Shape
187
+
188
+ Use one `src/lib/<domain>/` folder per persisted domain. Do not put types,
189
+ defaults, parsing, pure updates, platform hooks, and UI in one file.
190
+
191
+ ```text
192
+ src/
193
+ hooks/
194
+ use<Domain>Settings.ts
195
+ use<Domain>Store.ts
196
+ lib/
197
+ <domain>/
198
+ types.ts
199
+ defaults.ts
200
+ parse.ts
201
+ updates.ts
202
+ index.ts
203
+ components/
204
+ ```
205
+
206
+ Responsibilities:
207
+
208
+ - `types.ts` exports the persisted domain types.
209
+ - `defaults.ts` exports default settings or empty store values.
210
+ - `parse.ts` converts unknown persisted values into valid domain state.
211
+ - `updates.ts` contains pure functions that return new state objects.
212
+ - `index.ts` re-exports the domain pieces used outside the folder.
213
+ - `src/hooks/use<Domain>Settings.ts` calls `usePlatformAppSettings`.
214
+ - `src/hooks/use<Domain>Store.ts` calls `usePlatformJsonStore`.
215
+ - components import the app-owned hooks and render state.
216
+
217
+ Split a file when it starts owning more than one responsibility. The hook should
218
+ not define the domain model. Components should not parse persisted data. Pure
219
+ update functions should not call platform helpers.
220
+
221
+ ## Verification
222
+
223
+ Run:
224
+
225
+ ```bash
226
+ npm run typecheck
227
+ npm run build
228
+ npm run puredesktop:check
229
+ ```
230
+
231
+ Source checks:
232
+
233
+ - app settings state has a domain settings type, default value, parser, and
234
+ hook;
235
+ - JSON store state has a domain store type, empty value, parser, pure update
236
+ helpers, and hook;
237
+ - components import the app-owned hooks instead of platform persistence helpers;
238
+ - `usePlatformJsonStore` is only imported from app-owned hooks;
239
+ - apps using `usePlatformJsonStore` include `filesystem` in `plugin.json`;
240
+ - JSON store file names include `APP_SLUG` and the real persisted record name;
241
+ - JSON store update functions return new objects instead of mutating existing
242
+ state.
@@ -10,7 +10,7 @@ agent tools.
10
10
  "schemaVersion": 1,
11
11
  "id": "{{PLUGIN_ID}}",
12
12
  "name": "{{APP_TITLE}}",
13
- "permissions": ["network", "settings", "agents"],
13
+ "permissions": ["network", "settings", "filesystem", "agents"],
14
14
  "entrypoint": {
15
15
  "kind": "dev-url",
16
16
  "url": "http://localhost:{{APP_PORT}}"