@velarscript/cli 0.20.1 → 0.21.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.
@@ -16,18 +16,29 @@ Activate Desktop explicitly and declare the smallest required authority:
16
16
  "desktop": {
17
17
  "productName": "Example",
18
18
  "identifier": "com.example.app",
19
+ "windows": {
20
+ "main": { "width": 1280, "height": 820 },
21
+ "note-preview": { "style": "panel", "frame": false, "aspectRatio": 1.6, "width": 512, "height": 320 }
22
+ },
23
+ "services": {
24
+ "core": { "payload": "dist/service-core", "entry": "main.js", "restart": "always" }
25
+ },
19
26
  "permissions": {
20
- "files": ["project"],
27
+ "files": ["project", "dropped"],
21
28
  "processes": ["git"],
22
29
  "network": ["https://api.example.com"],
23
30
  "environment": [],
24
- "secrets": []
31
+ "secrets": [],
32
+ "links": ["https", "mailto"],
33
+ "notifications": true,
34
+ "secureStorage": ["CLOUD_SESSION"]
25
35
  }
26
36
  }
27
37
  }
28
38
  ```
29
39
 
30
- Desktop owns `velar/desktop`, `velar/desktop-test`, and permission-scoped
40
+ Desktop owns `velar/desktop`, `velar/window`, `velar/service`,
41
+ `velar/notification`, `velar/secure-storage`, `velar/desktop-test`, and permission-scoped
31
42
  implementations of `velar/fs`, `velar/path`, `velar/process`, `velar/http`, and
32
43
  `velar/env`. It composes Web components, JSX, Look, state, resources, actions,
33
44
  and browser tests. It does not expose a user main process, renderer project,
@@ -35,7 +46,208 @@ local server, port, or general IPC surface.
35
46
 
36
47
  The manifest is the authority. Never broaden a grant merely to silence a
37
48
  failure. File roots, executable identities, network origins, readable
38
- environment names, and opaque secret names are finite allowlists.
49
+ environment names, opaque secret names, link schemes, and credential slot names
50
+ are finite allowlists, and `notifications` is a single declaration of intent. A
51
+ capability the manifest never declared fails where it is *called*, naming the
52
+ line that would grant it — never at the import, and never silently.
53
+
54
+ ## Windows
55
+
56
+ `desktop.windows` declares every window kind the application may open, keyed by
57
+ kind. `main` is required and opens at launch; a kind that is not declared is
58
+ refused at the `openWindow` call, by name. Kind names are lowercase words joined
59
+ by single hyphens, at most 32 per application. Each kind's fields are closed
60
+ vocabularies with defaults: `title` (the product name), `width`/`height`/
61
+ `minWidth`/`minHeight`, `titleBar` (`standard` | `hidden-inset`), `material`
62
+ (`none` | `sidebar`), `style` (`window` | `panel`), `frame`, `level` (`normal` |
63
+ `floating`), `visibleOnAllWorkspaces`, `aspectRatio`, and `resizable`.
64
+
65
+ Every window loads the same application at the route given to `openWindow`, so
66
+ one source graph renders every window; `currentWindowKind()` is how a component
67
+ decides which one it is in.
68
+
69
+ ```velar fragment
70
+ import {WindowState, currentWindowKind, openWindow, windows} from "velar/window"
71
+
72
+ async def previewNote(note: string) -> number:
73
+ // Same kind and key focuses the window that already exists.
74
+ using preview = await openWindow("note-preview", {route: f"/preview?note={note}", key: f"note-{note}"})
75
+ const bounds = await preview.bounds()
76
+ await preview.setBounds({x: bounds.x, y: bounds.y, width: 512, height: 320})
77
+ using states = await preview.watchState()
78
+ let seen = 0
79
+ async for state in states:
80
+ seen += 1
81
+ if state == WindowState.closed: break
82
+ return seen + (await windows()).size + currentWindowKind().size
83
+ ```
84
+
85
+ A `Window` is an owned resource: `using` closes it, and the release is
86
+ idempotent. `currentWindow()` hands back this window rather than one you opened,
87
+ so hold it in a `const`. `watchState()` is a bounded pull stream — `moved`,
88
+ `resized`, `focused`, `blurred`, `closed` — that drains after `closed`; a slow
89
+ consumer coalesces repeated `moved`/`resized` instead of growing a queue.
90
+
91
+ Two host rules have no knob: closing `main` closes every other window and quits,
92
+ and closing the last window quits. Do not build an application that depends on
93
+ outliving them, and do not try to share state between windows through the
94
+ language — windows do not share a JavaScript context.
95
+
96
+ ## Service processes
97
+
98
+ `desktop.services` declares the long-running processes the *product* owns. The
99
+ language does four things and no more: it starts them, supervises them,
100
+ converges them when the application quits, and hands the renderer one
101
+ authenticated loopback channel to each. It does not sandbox them — a service
102
+ does not go through the capability worker, and declaring one makes it auditable
103
+ rather than confined. The service itself is not a language capability: writing
104
+ one is the product's job, exactly as the exclusion list at the end of this brief
105
+ says.
106
+
107
+ A service name follows the window kind's rule and at most eight may be declared.
108
+ `payload` is a project directory copied whole into
109
+ `Contents/Resources/services/<name>/` at package time; `entry` is a JavaScript
110
+ file inside it, run by the Node.js runtime the bundle carries. No other
111
+ executable is declarable: a short-lived process is `velar/process` with a
112
+ `processes` grant, and that is a different model on purpose. `restart` is
113
+ `always` (exponential backoff from 1s to a 30s cap, and five consecutive
114
+ failures reach the terminal `failed`) or `never`.
115
+
116
+ The host gives each service three variables and no more, the same three under
117
+ `velar dev` as in a packaged application: a loopback endpoint and a 128-bit token
118
+ in `VELAR_SERVICE_ENDPOINT` and `VELAR_SERVICE_TOKEN`, and the application's own
119
+ data directory in `VELAR_SERVICE_APP_DATA` — the exact path
120
+ `velar/desktop.appDataDirectory()` answers the renderer, already created. That
121
+ third one is standard because it is the only thing a service needs that cannot
122
+ be baked into its payload: it is the application's identity resolved against
123
+ this machine. `desktop.services` has no `env`, and a value that is the same on
124
+ every machine belongs in the payload rather than in the environment.
125
+
126
+ The service must run a WebSocket server on that endpoint. Readiness is the
127
+ handshake: the host sends `{"velar":"service-hello","token":"…"}` and the
128
+ service answers `{"velar":"service-ready"}`. A connection that opens with any other token must
129
+ be closed with WebSocket code 1008 and no answer — a loopback port is reachable
130
+ by every process on the machine, so the token is the whole of the channel's
131
+ authentication, and the pinned code is what separates a refusal from a service
132
+ that has not finished starting. Both sides wait 30 seconds. The host's readiness
133
+ probe is indistinguishable from an application `connect()`, so a service sees
134
+ connections open and close that no window asked for and must not read a closed
135
+ authenticated connection as an application-level event.
136
+
137
+ ```velar fragment
138
+ import {ServiceState, connect, watchServices} from "velar/service"
139
+
140
+ async def indexNote(id: string) -> string:
141
+ using channel = await connect("core")
142
+ await channel.send(f"put {id}")
143
+ return (await channel.next()) ?? ""
144
+
145
+ async def coreState() -> ServiceState:
146
+ using states = await watchServices()
147
+ const event = await states.next()
148
+ return event?.state ?? ServiceState.stopped
149
+
150
+ async def whyCoreFailed() -> string:
151
+ using states = await watchServices()
152
+ const event = await states.next()
153
+ return event?.detail ?? "no detail"
154
+ ```
155
+
156
+ A `ServiceStateEvent` is `{name, state, detail}`. `detail` is up to 4 KiB of what
157
+ the service last wrote to its own standard error, carried by `failed` and
158
+ `restarting` and null for every other state; show it to a person, never match on
159
+ it. The whole of a service's output is a rotating log file under the app-data
160
+ directory, which `packages/desktop/README.md` names.
161
+
162
+ Application code never holds the token: the host spends it itself on the first
163
+ frame of every connection. An undeclared name fails at the `connect` call, and a
164
+ service that is not `ready` is refused by state — services start before the
165
+ renderer loads and are not awaited, so read `watchServices()` rather than
166
+ assuming one is up. `ServiceConnection` keeps the `velar/websocket` client's
167
+ discipline: a backpressured `send`, a bounded pull `next`, and a release `using`
168
+ performs. It carries text.
169
+
170
+ `velar dev` runs the same services from `<project>/<payload>/<entry>` on the
171
+ system Node and converges them when the dev server closes. It does not watch or
172
+ rebuild them — a service's build is the product's own toolchain.
173
+
174
+ ## Notifications
175
+
176
+ `desktop.permissions.notifications: true` is the application's declaration that
177
+ it may notify at all; without it `requestPermission`, `show`, and `activations`
178
+ each fail at the call and name that line. The operating system's own answer is a
179
+ second, different gate — ask for it with `requestPermission()`, and expect
180
+ `granted`, `denied`, or `undetermined`. `show` on an unauthorized application
181
+ fails; it never quietly delivers nothing.
182
+
183
+ ```velar fragment
184
+ import {NotificationPermission, activations, requestPermission, show} from "velar/notification"
185
+
186
+ async def announce(packages: number) -> string:
187
+ if await requestPermission() != NotificationPermission.granted: return "not notified"
188
+ // A tag is the notification's identity: a second notification carrying it
189
+ // replaces the first, and an activation reports it back.
190
+ await show({title: "Build finished", body: f"{packages} packages", tag: "build"})
191
+ using clicks = await activations()
192
+ async for click in clicks: return click.tag ?? "untagged"
193
+ return "no activation"
194
+ ```
195
+
196
+ `title` is at most 256 characters, `body` 1024, `tag` 128. `activations()` is a
197
+ bounded pull stream of `{tag: string?}`; two clicks on one notification are one
198
+ activation, and the host brings the application forward with it, opening `main`
199
+ when no window is left.
200
+
201
+ ## Secure storage
202
+
203
+ `secureStorage` is a finite allowlist of credential slot names, spelled the way
204
+ `secrets` names are, and one name may appear in only one of the two lists. They
205
+ are different authorities: a `secrets` entry is an opaque value the environment
206
+ injects, while a `secureStorage` entry is a slot the application itself writes
207
+ and reads — a macOS keychain generic password under the application's bundle
208
+ identifier.
209
+
210
+ ```velar fragment
211
+ import {get, remove, set} from "velar/secure-storage"
212
+
213
+ async def rotate(token: string) -> bool:
214
+ await set("CLOUD_SESSION", token) // at most 8 KiB
215
+ const stored = await get("CLOUD_SESSION")
216
+ await remove("CLOUD_SESSION") // removing what is absent is not an error
217
+ await remove("CLOUD_SESSION")
218
+ return stored != null
219
+ ```
220
+
221
+ A name outside the allowlist fails at the call and lists the declared names.
222
+ Never render, log, or serialize a stored value; report whether a credential is
223
+ present, not what it is.
224
+
225
+ ## Links, displays, power, dropped files, and probes
226
+
227
+ ```velar fragment
228
+ import {PowerState, SystemPermission, displays, openExternal, permissionStatus, watchDroppedFiles, watchPower} from "velar/desktop"
229
+
230
+ async def sleepAware() -> string:
231
+ await openExternal("https://example.com/guide") // scheme must be in `links`
232
+ const attached = await displays()
233
+ const ready = await permissionStatus(SystemPermission.screenRecording)
234
+ using states = await watchPower()
235
+ using drops = await watchDroppedFiles() // needs files: ["dropped"]
236
+ async for state in states:
237
+ if state == PowerState.suspended: break
238
+ async for batch in drops: return f"{batch.paths.size}:{attached.size}:{ready}"
239
+ return "none"
240
+ ```
241
+
242
+ `links` is a closed set of `http`, `https`, and `mailto`; any other scheme is
243
+ refused at the call and again by the host. `displays()` answers the same
244
+ `Display` record a window's own `display()` does. `watchPower()` carries
245
+ transitions only — a machine already awake publishes nothing on waking.
246
+ `watchDroppedFiles()` needs the `dropped` file root and reports the real paths of
247
+ the files a user's drag gesture brought in, in gesture order; the page still gets
248
+ its ordinary DOM `drop` event, and the two are the same gesture.
249
+ `permissionStatus` only reads. There is no request function: asking the user for
250
+ a system permission belongs to the product flow that consumes the answer.
39
251
 
40
252
  ## Capability model
41
253
 
@@ -69,7 +281,48 @@ mount(<App />, "#app")
69
281
  Put target-specific calls in narrow service modules so UI components consume
70
282
  checked application data instead of transport details. Use
71
283
  `velar/desktop-test` only from official browser-test modules; plain unit tests
72
- should cover pure policy and conversion logic without platform authority.
284
+ should cover pure policy and conversion logic without platform authority. Its
285
+ fake host answers every module above for the page and lets a browser test
286
+ produce the host events a real system would. Two of its choices are made before
287
+ the first `browser.open()` and sealed by it — `setPlatform` and `setWindowKind`
288
+ — and the rest are events inside a running page: `openWindows`, `focusWindow`,
289
+ `moveWindow`, `closeWindow`, `setNotificationPermission`, `shownNotifications`,
290
+ `activateNotification`, `secureStorageNames`, `publishPower`, `dropFiles`,
291
+ `setSystemPermission`, and `openedLinks`. `secureStorageNames` reports the names
292
+ the fake keychain holds and never the values: a test seam that handed a
293
+ credential back would be the exception that ends that rule.
294
+
295
+ ## Updating the installed application
296
+
297
+ ```velar fragment
298
+ import {applyUpdate} from "velar/desktop"
299
+
300
+ async def install(archivePath: string) -> string:
301
+ try:
302
+ await applyUpdate(archivePath)
303
+ return "replaced; relaunching"
304
+ catch error:
305
+ return f"refused: {error.message}"
306
+ ```
307
+
308
+ `applyUpdate` is the mechanism and nothing else. There is no feed, no channel,
309
+ no version check, no automatic download, and no delta format — deciding when to
310
+ look, where to look, and what to tell the user is the product's, and so is
311
+ downloading the archive with the capabilities the application already has.
312
+
313
+ The host expands the archive elsewhere, requires the application inside it to
314
+ carry this application's bundle identifier and this application's signing Team
315
+ ID, and only then replaces the installed bundle atomically and relaunches. Every
316
+ failure leaves the current install untouched. Do not write a fallback that
317
+ retries with a different archive or works around a refusal: a refusal means the
318
+ archive is not this application.
319
+
320
+ A development build is ad-hoc-signed and therefore has **no** Team ID, so
321
+ `applyUpdate` refuses it by name. That is not a bug to route around — an update
322
+ path where no team matches no team is an update path that accepts every archive
323
+ on the machine. Test the flow with `velar/desktop-test`'s `setSigningTeam`,
324
+ `stageUpdate` and `appliedUpdates`, and expect the real call to work only in a
325
+ Developer ID signed install.
73
326
 
74
327
  ## Build and finish
75
328
 
@@ -78,7 +331,29 @@ output, and `velar package` creates the native application containing the
78
331
  system-WebView host and capability worker. It does not embed compiler or
79
332
  Workbench tooling.
80
333
 
334
+ `velar package` output is self-contained: it carries one bare Node.js executable
335
+ at `Contents/MacOS/node`, whose version belongs to the toolchain generation
336
+ rather than the project. Do not add a manifest field for it, do not check for
337
+ Node at install time, and do not tell a user to install one. The first package on
338
+ a machine downloads and verifies the official archive; later ones use the
339
+ verified cache and need no network.
340
+
341
+ `desktop.build.sizeBudgetBytes` measures the application's own components; the
342
+ runtime is reported separately and has its own toolchain-owned ceiling. Do not
343
+ raise the budget to make room for it.
344
+
345
+ Signing always happens — ad-hoc when `desktop.build.signing.identity` is absent,
346
+ so a local build runs on arm64. Set `identity`, `entitlements` and
347
+ `notarization.keychainProfile` when the product distributes. Never put an Apple
348
+ ID, a password, or an App Store Connect key in `velar.json`: the keychain profile
349
+ name is the only credential-shaped thing that belongs there, and it is a
350
+ reference the local keychain resolves.
351
+
81
352
  Run `velar format`, `velar check`, `velar test`, the Desktop browser tests,
82
- `velar build`, and the platform packaging gate. Runnable target examples live
353
+ `velar build`, and the platform packaging gate, whose acceptance is the packaged
354
+ host's `--headless-smoke` (host up, capability worker up on the bundled runtime,
355
+ one real capability round-trip, every declared service started and through its
356
+ authenticated handshake to `ready`, converged, exit 0). `--verify-bundle` is the
357
+ static bundle check beside it and is not an acceptance. Runnable target examples live
83
358
  in `examples/tour/desktop/`; diagnostics and checked manifest vocabulary
84
359
  outrank this brief if they disagree.
@@ -13,7 +13,7 @@ browser application activates `@velarscript/web`:
13
13
  ```json
14
14
  {
15
15
  "dependencies": {
16
- "@velarscript/server": "0.20.1"
16
+ "@velarscript/server": "0.21.0"
17
17
  }
18
18
  }
19
19
  ```
package/skill/ai-skill.md CHANGED
@@ -244,8 +244,10 @@ def publicUser(source: SourceUser, requestId: string) -> PublicUser:
244
244
  `Type.from` is shallow and compile-time checked. It does not accept `unknown`;
245
245
  validate untrusted data with `Type.parse` first.
246
246
 
247
- `enum` declares finite string-backed states; a member may map an external
248
- wire spelling without losing its nominal identity:
247
+ `enum` declares finite states backed by a wire value; a member may map an
248
+ external spelling a string, or a safe integer where a protocol pins a version
249
+ — without losing its nominal identity. A member satisfies a contract for the
250
+ scalar its own wire value is, and nothing else:
249
251
 
250
252
  ```velar
251
253
  enum Status:
@@ -257,8 +259,14 @@ enum ProviderEventKind:
257
259
  textDelta = "response.output_text.delta"
258
260
  completed = "response.completed"
259
261
 
262
+ enum KernelProtocol:
263
+ v1 = 1
264
+ v2 = 2
265
+
260
266
  const status: Status = Status.active
267
+ const protocol: number = KernelProtocol.v2
261
268
  print(ProviderEventKind.textDelta)
269
+ print(str(protocol))
262
270
  ```
263
271
 
264
272
  Classes use typed body fields, one explicit constructor, and explicit `self`;