@abide/abide 0.100.0 → 0.102.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.
Files changed (160) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/bin/abide.ts +227 -163
  3. package/docs/REGISTRY.md +726 -0
  4. package/docs/RULEBOOK.md +2753 -0
  5. package/docs/abide.md +12878 -0
  6. package/package.json +10 -3
  7. package/scaffold/AGENTS.md +31 -0
  8. package/scaffold/CLAUDE.md +1 -0
  9. package/scaffold/src/layout.abide +18 -0
  10. package/src/compiler/Lowering.ts +78 -9
  11. package/src/compiler/ShadowMap.ts +2 -2
  12. package/src/compiler/TypeSession.ts +262 -67
  13. package/src/compiler/argsSchemaOf.ts +1 -1
  14. package/src/compiler/checkApp.ts +43 -15
  15. package/src/compiler/checkedShadow.ts +39 -5
  16. package/src/compiler/classifyRead.ts +6 -4
  17. package/src/compiler/classifyTemplate.ts +1 -1
  18. package/src/compiler/compileApp.ts +433 -193
  19. package/src/compiler/compiledEntry.ts +43 -39
  20. package/src/compiler/componentGraph.ts +58 -71
  21. package/src/compiler/declaredExports.ts +24 -7
  22. package/src/compiler/emitClient.ts +141 -71
  23. package/src/compiler/emitComponent.ts +37 -8
  24. package/src/compiler/emitSource.ts +106 -13
  25. package/src/compiler/handlersUnder.ts +106 -56
  26. package/src/compiler/index.ts +7 -0
  27. package/src/compiler/mountsOf.ts +138 -0
  28. package/src/compiler/nearest.ts +14 -0
  29. package/src/compiler/parseAbide.ts +157 -20
  30. package/src/compiler/projectShadow.ts +213 -7
  31. package/src/compiler/reactiveTypes.ts +20 -7
  32. package/src/compiler/readSources.ts +15 -2
  33. package/src/compiler/readsIn.ts +61 -7
  34. package/src/compiler/renderMarkup.ts +3 -3
  35. package/src/compiler/scaffold.ts +30 -11
  36. package/src/compiler/serveLanguage.ts +125 -34
  37. package/src/compiler/sourceMapOf.ts +193 -0
  38. package/src/compiler/splitImports.ts +205 -83
  39. package/src/compiler/syncTypes.ts +32 -0
  40. package/src/compiler/tokenize.ts +14 -0
  41. package/src/compiler/writeDefaultProject.ts +1 -1
  42. package/src/compiler/writeTypes.ts +82 -0
  43. package/src/server/CLIENT_BUILD.ts +11 -13
  44. package/src/server/DEFAULT_PORT.ts +1 -1
  45. package/src/server/LAUNCHER_FILES.ts +9 -0
  46. package/src/server/MODULE_QUERIES.ts +18 -0
  47. package/src/server/TABLES.ts +7 -0
  48. package/src/server/VIRTUAL_DIRECTORY.ts +4 -5
  49. package/src/server/abideModules.ts +391 -0
  50. package/src/server/answerFailure.ts +16 -6
  51. package/src/server/answerRequest.ts +235 -106
  52. package/src/server/answerRpc.ts +124 -39
  53. package/src/server/answerSocket.ts +141 -48
  54. package/src/server/answerValue.ts +22 -9
  55. package/src/server/appDirectories.ts +1 -1
  56. package/src/server/appPlugins.ts +1 -1
  57. package/src/server/appTree.ts +179 -0
  58. package/src/server/argsFor.ts +2 -2
  59. package/src/server/assistant.ts +2 -2
  60. package/src/server/bridgeRelay.ts +6 -8
  61. package/src/server/claudeCode.ts +0 -6
  62. package/src/server/clientBuildOptions.ts +8 -3
  63. package/src/server/coerceArgs.ts +1 -1
  64. package/src/server/commands.ts +37 -37
  65. package/src/server/componentModules.d.ts +3 -1
  66. package/src/server/compressAnswer.ts +48 -14
  67. package/src/server/config.ts +25 -29
  68. package/src/server/configFields.ts +31 -25
  69. package/src/server/createApp.ts +59 -73
  70. package/src/server/devWorker.ts +144 -183
  71. package/src/server/executableName.ts +4 -0
  72. package/src/server/fenceServer.ts +37 -35
  73. package/src/server/generatedSurfaces.ts +99 -66
  74. package/src/server/hooks.ts +124 -52
  75. package/src/server/iconAccent.ts +191 -0
  76. package/src/server/index.ts +11 -7
  77. package/src/server/installSourceMaps.ts +88 -0
  78. package/src/server/launcherOptions.ts +34 -31
  79. package/src/server/launcherPage.ts +107 -64
  80. package/src/server/manifestOf.ts +16 -13
  81. package/src/server/namesUnder.ts +2 -2
  82. package/src/server/nativeMenu.ts +5 -5
  83. package/src/server/negotiateEncoding.ts +1 -1
  84. package/src/server/principal.ts +10 -5
  85. package/src/server/realOrAsGiven.ts +10 -0
  86. package/src/server/refuseAddressCollision.ts +21 -14
  87. package/src/server/remote.ts +49 -52
  88. package/src/server/render.ts +65 -94
  89. package/src/server/renderComponent.ts +5 -0
  90. package/src/server/reportHealth.ts +2 -2
  91. package/src/server/requestScope.ts +12 -1
  92. package/src/server/runBuild.ts +24 -0
  93. package/src/server/runCompiled.ts +5 -5
  94. package/src/server/runDesktop.ts +52 -27
  95. package/src/server/serveBuild.ts +43 -52
  96. package/src/server/serveDev.ts +81 -39
  97. package/src/server/shell.ts +7 -22
  98. package/src/server/sourceMappings.ts +139 -0
  99. package/src/server/surfaceOf.ts +18 -3
  100. package/src/server/testBuild.ts +1 -19
  101. package/src/server/withAbideSources.ts +117 -0
  102. package/src/shared/ADDRESSES.ts +7 -18
  103. package/src/shared/APP_META.ts +7 -0
  104. package/src/shared/CopyOptions.ts +1 -1
  105. package/src/shared/DEBUG_META.ts +1 -1
  106. package/src/shared/GLOBAL_GETS.ts +9 -3
  107. package/src/shared/RESERVED_DIRECTORIES.ts +15 -0
  108. package/src/shared/RPC_SERVER.ts +2 -2
  109. package/src/shared/cookies.ts +2 -2
  110. package/src/shared/csp.ts +1 -1
  111. package/src/shared/escapeText.ts +1 -1
  112. package/src/shared/filledPath.ts +85 -0
  113. package/src/shared/framing.ts +79 -6
  114. package/src/shared/headKeyOf.ts +2 -0
  115. package/src/shared/index.ts +1 -1
  116. package/src/shared/log.ts +27 -13
  117. package/src/shared/matchRoute.ts +23 -4
  118. package/src/shared/menuItem.ts +3 -3
  119. package/src/shared/reactive/Cursor.ts +3 -9
  120. package/src/shared/reactive/Keyed.ts +4 -3
  121. package/src/shared/reactive/NotLanded.ts +1 -1
  122. package/src/shared/reactive/Reactive.ts +81 -60
  123. package/src/shared/reactive/argsContract.ts +4 -4
  124. package/src/shared/reactive/canonicalKey.ts +4 -4
  125. package/src/shared/reactive/chunksOfStream.ts +6 -0
  126. package/src/shared/reactive/index.ts +1 -1
  127. package/src/shared/reactive/recordWrites.ts +103 -5
  128. package/src/shared/reactive/structural.ts +1 -1
  129. package/src/shared/reactive/tracking.ts +155 -21
  130. package/src/shared/reactive/watch.ts +10 -6
  131. package/src/shared/refuse.ts +1 -1
  132. package/src/shared/routePattern.ts +82 -46
  133. package/src/shared/rpc.ts +47 -13
  134. package/src/shared/rpcRequest.ts +27 -3
  135. package/src/shared/url.ts +4 -44
  136. package/src/shared/writePath.ts +1 -1
  137. package/src/ui/LAYER_NAMES.ts +1 -1
  138. package/src/ui/List.ts +3 -3
  139. package/src/ui/MOUNTING_LEVEL.ts +18 -0
  140. package/src/ui/Mismatch.ts +1 -1
  141. package/src/ui/RawRun.ts +1 -1
  142. package/src/ui/Router.ts +38 -31
  143. package/src/ui/Scope.ts +1 -1
  144. package/src/ui/TryRegion.ts +2 -2
  145. package/src/ui/applyHead.ts +33 -0
  146. package/src/ui/index.ts +1 -1
  147. package/src/ui/menuBridge.ts +1 -1
  148. package/src/ui/moveNode.ts +1 -1
  149. package/src/ui/reconcile.ts +2 -2
  150. package/src/ui/reportMenu.ts +2 -2
  151. package/src/ui/routing.ts +2 -2
  152. package/src/ui/setAttribute.ts +1 -1
  153. package/src/ui/setText.ts +1 -1
  154. package/src/ui/socketWire.ts +9 -7
  155. package/src/ui/wire.ts +1 -1
  156. package/scaffold/src/server/app.ts +0 -6
  157. package/scaffold/src/shared/.gitkeep +0 -0
  158. package/scaffold/src/ui/app.html +0 -8
  159. package/src/server/componentImports.ts +0 -56
  160. /package/scaffold/src/{ui/pages/page.abide → page.abide} +0 -0
@@ -3,6 +3,8 @@
3
3
  // (16.35), a document where `page` named one (17.2), a stream — a streaming handler's chunks, or
4
4
  // `Values` a handler returned — framed one chunk per pull (16.32, 16.44–16.47).
5
5
 
6
+ import { isFailed, problemOf } from '#shared/Failed.ts'
7
+ import { refusalFrame } from '#shared/framing.ts'
6
8
  import { compressAnswer } from './compressAnswer.ts'
7
9
  import { prefers } from './prefers.ts'
8
10
  import {
@@ -158,7 +160,7 @@ export function linesType(request: Request): string {
158
160
  : 'application/jsonl'
159
161
  }
160
162
 
161
- /** A stream answered at a framing already decided — a handler's, and `log.records` (36.7). */
163
+ /** A stream answered at a framing already decided — a handler's. */
162
164
  export function framed(
163
165
  chunks: AsyncIterator<unknown>,
164
166
  framing: Framing,
@@ -185,7 +187,7 @@ export function framed(
185
187
  * was reading in its scope.
186
188
  *
187
189
  * AN EVENT STREAM OPENS ON A COMMENT: a streamed answer's head goes out with its first byte, so a
188
- * stream with nothing yet to say — an empty `log.records` — held its reader's headers until it did.
190
+ * stream with nothing yet to say held its reader's headers until it did.
189
191
  *
190
192
  * `first` is a result already pulled — the one a framing was decided from — and is sent ahead of
191
193
  * the rest.
@@ -230,7 +232,20 @@ export function bodyOf(
230
232
  next = await chunks.next()
231
233
  } catch (thrown) {
232
234
  if (ended) return
233
- controller.error(thrown)
235
+ // 16.68 — A REFUSAL AFTER THE STATUS WAS SENT is the stream's last frame, so the
236
+ // reader refuses with it rather than reading a dropped connection. Bytes carry no
237
+ // framing to hold one, and anything else is not a refusal to send.
238
+ if (framing !== 'bytes' && isFailed(thrown)) {
239
+ controller.enqueue(
240
+ ENCODER.encode(
241
+ refusalFrame(
242
+ problemOf(thrown),
243
+ framing === 'sse' ? 'data: ' : '',
244
+ ),
245
+ ),
246
+ )
247
+ controller.close()
248
+ } else controller.error(thrown)
234
249
  release()
235
250
  return
236
251
  }
@@ -314,12 +329,10 @@ export function answerBlobTagged(
314
329
  })
315
330
  }
316
331
  }
317
- // A RANGE DECLINED — `If-Range` naming another version, or one this does not parse. Bun.serve
318
- // ranges a file-backed 200 body on its own and reads no `If-Range` when it does, so the file, or
319
- // its `stream()`, would go out 206 anyway; piped, the body is no longer one it can see is a file.
320
- const body =
321
- range === null ? blob : blob.stream().pipeThrough(new TransformStream())
322
- return new Response(body, { status, headers })
332
+ // A RANGE DECLINED — `If-Range` naming another version, or one this does not parse — goes out
333
+ // whole as the blob itself: Bun.serve ranges a file-backed body on its own, but declines exactly
334
+ // these, so a file still leaves from disk.
335
+ return new Response(blob, { status, headers })
323
336
  }
324
337
 
325
338
  /**
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The directories an app's own files stand in: `root`, and the one above it.
3
3
  *
4
- * `root` is the directory `ui/` sits under — `src/` — and the one above it is the package beside
4
+ * `root` is the app's root — `src/` — and the one above it is the package beside
5
5
  * it. The app's `package.json` and `bunfig.toml` are found in the first that has one.
6
6
  */
7
7
  export function appDirectories(root: string): string[] {
@@ -3,7 +3,7 @@ import { appDirectories } from './appDirectories.ts'
3
3
  /**
4
4
  * The app's own bundler plugins, loaded from `bunfig.toml`'s `[serve.static] plugins`.
5
5
  *
6
- * 38.44 — declared where bun already has an app declare them, the key `bun-plugin-tailwind`
6
+ * 38.43 — declared where bun already has an app declare them, the key `bun-plugin-tailwind`
7
7
  * documents. `Bun.build` reads no bunfig, so both client builds read this one — in
8
8
  * `appDirectories`, as `config` finds the app's `package.json`.
9
9
  */
@@ -0,0 +1,179 @@
1
+ // 23.14 — WHERE AN APP'S ADDRESSES ARE, read off the tree in one walk. Every directory below the root
2
+ // is a segment of an address, and a `page.abide`, `layout.abide` or `error.abide` in it answers there;
3
+ // a `server` or `public` directory is never one (23.15) — the first fences a graph and the second
4
+ // holds files answered as authored, at the path of the directories above it. So an app is organized
5
+ // by subject (`media/[id]/page.abide`, `media/server`) and the addresses follow.
6
+ //
7
+ // ONE WALK for the sources and the public files `abide build` copies and `abide dev` answers, and
8
+ // the refusals (23.16, 23.17, 23.18) — and ONE SKIP RULE, `skipped`, for every walk of the tree,
9
+ // beside `placeOf`, which answers the same question of one path.
10
+ //
11
+ // A WALK RATHER THAN A GLOB: a glob descends `node_modules` and the build output before a filter
12
+ // can drop them.
13
+
14
+ import { type Dirent, readdirSync } from 'node:fs' // bun has no directory listing of its own
15
+ import { ABIDE_ADDRESS } from '#shared/ADDRESSES.ts'
16
+ import {
17
+ PUBLIC_DIRECTORY,
18
+ RESERVED_DIRECTORIES,
19
+ } from '#shared/RESERVED_DIRECTORIES.ts'
20
+ import { fileKind, routeOf, skipped } from '#shared/routePattern.ts'
21
+
22
+ /** What one walk of the app finds, every path relative to the root. */
23
+ export type AppFiles = {
24
+ /** Every `.abide`, in path order. */
25
+ abide: string[]
26
+ /** Every file under a `public` directory, to the address it is answered at below the mount. */
27
+ published: Map<string, string>
28
+ }
29
+
30
+ /** Told of each thing the tree refuses, at the path relative to the root. */
31
+ type Refuse = (path: string, message: string) => void
32
+
33
+ const THROW: Refuse = (_path, message) => {
34
+ throw new Error(message)
35
+ }
36
+
37
+ /**
38
+ * The app's `.abide` sources and public files, refusing a tree 23.16, 23.17 or 23.18 forbids.
39
+ *
40
+ * `refuse` throws by default, which is the build's answer; `abide check` collects instead (38.33),
41
+ * and the checker's route table ignores them, since a refused tree is the build's to report.
42
+ *
43
+ * A missing root is an app with nothing in it, not a fault.
44
+ */
45
+ export function appFiles(root: string, refuse: Refuse = THROW): AppFiles {
46
+ const files: AppFiles = { abide: [], published: new Map() }
47
+ walk(root, '', undefined, files, refuse)
48
+ files.abide.sort()
49
+ return files
50
+ }
51
+
52
+ // `reserved` is the nearest reserved directory above `relative` — its path and its name — or
53
+ // `undefined` where none is.
54
+ function walk(
55
+ directory: string,
56
+ relative: string,
57
+ reserved: { path: string; name: string } | undefined,
58
+ files: AppFiles,
59
+ refuse: Refuse,
60
+ ): void {
61
+ let entries: Dirent[]
62
+ try {
63
+ entries = readdirSync(directory, { withFileTypes: true })
64
+ } catch (thrown) {
65
+ if ((thrown as { code?: unknown }).code === 'ENOENT') return
66
+ throw thrown
67
+ }
68
+ for (const entry of entries) {
69
+ if (skipped(entry.name)) continue
70
+ const path = `${relative}${entry.name}`
71
+ if (entry.isDirectory()) {
72
+ let within = reserved
73
+ if (RESERVED_DIRECTORIES.has(entry.name)) {
74
+ if (reserved !== undefined)
75
+ refuse(
76
+ path,
77
+ `abide: ${path} is a ${entry.name} directory under the ${reserved.name} directory ${reserved.path}. RULEBOOK 23.16.`,
78
+ )
79
+ within = { path, name: entry.name }
80
+ }
81
+ walk(
82
+ `${directory}/${entry.name}`,
83
+ `${path}/`,
84
+ within,
85
+ files,
86
+ refuse,
87
+ )
88
+ continue
89
+ }
90
+ if (path.endsWith('.abide')) files.abide.push(path)
91
+ if (reserved === undefined) {
92
+ // 23.17 — anything that could answer here would answer in abide's own namespace.
93
+ if (path.endsWith('.abide'))
94
+ refuseFrameworkRoute(routeOf(path), path, refuse)
95
+ } else if (fileKind(entry.name) !== 'component')
96
+ refuse(
97
+ path,
98
+ `abide: ${path} is a ${entry.name} under the ${reserved.name} directory ${reserved.path}, where nothing has an address. RULEBOOK 23.18.`,
99
+ )
100
+ else if (reserved.name === PUBLIC_DIRECTORY)
101
+ files.published.set(path, publicAddress(path, reserved.path))
102
+ }
103
+ }
104
+
105
+ /** Refuses `source` where `route` lies in the framework's own namespace. 23.17. */
106
+ export function refuseFrameworkRoute(
107
+ route: string,
108
+ source: string,
109
+ refuse: Refuse = THROW,
110
+ ): void {
111
+ if (route === ABIDE_ADDRESS || route.startsWith(`${ABIDE_ADDRESS}/`))
112
+ refuse(
113
+ source,
114
+ `abide: ${source} answers at ${route}, which is under ${ABIDE_ADDRESS}, where abide's own addresses are. RULEBOOK 23.17.`,
115
+ )
116
+ }
117
+
118
+ // 36.11 — the directories above `public`, then the file's own path below it.
119
+ function publicAddress(path: string, directory: string): string {
120
+ const above = directory.slice(0, -PUBLIC_DIRECTORY.length)
121
+ return `/${above}${path.slice(directory.length + 1)}`
122
+ }
123
+
124
+ /**
125
+ * The public files as the server reads them: each address, without its leading `/`, to the file.
126
+ *
127
+ * 38.19 — out of the source tree, where `abide dev` answers them.
128
+ */
129
+ export function publishedUnder(
130
+ root: string,
131
+ published: Map<string, string>,
132
+ ): Map<string, string> {
133
+ const paths = new Map<string, string>()
134
+ for (const [source, address] of published)
135
+ paths.set(address.slice(1), `${root}/${source}`)
136
+ return paths
137
+ }
138
+
139
+ /**
140
+ * Every `.abide` under `root`, relative to it, in path order — the app's own, and none a
141
+ * dependency ships.
142
+ *
143
+ * Refuses nothing: the language server lists them on every check, and a tree 23.16 forbids is the
144
+ * build's to refuse (`appFiles`).
145
+ */
146
+ export function abideUnder(root: string): string[] {
147
+ const found: string[] = []
148
+ for (const path of abideFilesUnder(root, '')) found.push(path)
149
+ return found.sort()
150
+ }
151
+
152
+ /**
153
+ * Whether a `.abide` lies anywhere under `root` — stopping at the first.
154
+ *
155
+ * 38.26's question, asked of a root before anything is built from it, so asking it of a large tree
156
+ * is as cheap as the tree allows.
157
+ */
158
+ export function holdsAbide(root: string): boolean {
159
+ return !abideFilesUnder(root, '').next().done
160
+ }
161
+
162
+ function* abideFilesUnder(
163
+ directory: string,
164
+ relative: string,
165
+ ): Generator<string> {
166
+ let entries: Dirent[]
167
+ try {
168
+ entries = readdirSync(directory, { withFileTypes: true })
169
+ } catch {
170
+ return
171
+ }
172
+ for (const entry of entries) {
173
+ if (skipped(entry.name)) continue
174
+ const path = `${relative}${entry.name}`
175
+ if (entry.isDirectory())
176
+ yield* abideFilesUnder(`${directory}/${entry.name}`, `${path}/`)
177
+ else if (path.endsWith('.abide')) yield path
178
+ }
179
+ }
@@ -2,7 +2,7 @@
2
2
  // off the wire: over http (16.2) and a `GET` followed over the socket mux (18.3). Each lane reads the
3
3
  // args off its own transport and answers a refusal in its own form.
4
4
 
5
- import { normalised } from '#shared/reactive/argsContract.ts'
5
+ import { normalized } from '#shared/reactive/argsContract.ts'
6
6
  import { validationError } from '#shared/refuse.ts'
7
7
  import { validateJson } from '#shared/validateJson.ts'
8
8
  import type { Mounted } from './answerRpc.ts'
@@ -31,7 +31,7 @@ export function argsFor(
31
31
  found.argsDocument,
32
32
  query,
33
33
  )
34
- const args = normalised(found.handler, coerced)
34
+ const args = normalized(found.handler, coerced)
35
35
  if (found.wireArgs !== undefined && args !== undefined) {
36
36
  const issues = validateJson(found.wireArgs, args)
37
37
  if (issues !== null) throw validationError(issues)
@@ -115,10 +115,10 @@ function appSurface(): McpSurface {
115
115
  throw new Error(
116
116
  'abide: assistant tools are the app’s MCP surface, and no app is answering this request. RULEBOOK 45.6.',
117
117
  )
118
- // 45.8, 20.7 — asking for tools over a closed surface is a mistake to name.
118
+ // 45.8, 20.7 — asking for tools where no handler is published to MCP is a mistake to name.
119
119
  if (!app.mcp.opened)
120
120
  throw new Error(
121
- 'abide: assistant was asked for tools, and ABIDE_MCP has not opened the MCP surface. RULEBOOK 45.8.',
121
+ 'abide: assistant was asked for tools, and this app publishes no handler to MCP. RULEBOOK 45.8.',
122
122
  )
123
123
  return app.mcp
124
124
  }
@@ -9,13 +9,12 @@
9
9
  //
10
10
  // A PAIRING IS TO A BROWSER — the `abide-caller` its page carries (26.5) — for as long as the bridge's
11
11
  // stream is open. HELD IN THIS PROCESS: an app served by several needs a visitor's requests to reach
12
- // the one its bridge dialled.
12
+ // the one its bridge dialed.
13
13
 
14
14
  import { refuse } from '#shared/refuse.ts'
15
15
  import { answered } from './answerFailure.ts'
16
16
  import { raiseTimeout } from './answerRpc.ts'
17
17
  import { framed } from './answerValue.ts'
18
- import { config } from './config.ts'
19
18
  import { Inbox } from './Inbox.ts'
20
19
  import { randomToken } from './randomToken.ts'
21
20
 
@@ -53,10 +52,8 @@ const BY_CALLER = new Map<string, Bridge>()
53
52
  const BY_TOKEN = new Map<string, Bridge>()
54
53
  const WATCHERS = new Map<string, Set<() => void>>()
55
54
 
56
- /** Whether `ABIDE_BRIDGE` has opened the relay. 45.19. */
57
- export function bridgeOpened(): boolean {
58
- return (config.peek()?.ABIDE_BRIDGE as unknown) === true
59
- }
55
+ // 45.19 — the relay answers once the app has handed out a pairing command, and never before.
56
+ let minted = false
60
57
 
61
58
  /** A single-use pairing code for `caller`, and when it stops standing. 45.14. */
62
59
  export function mintCode(caller: string): { code: string; expires: number } {
@@ -66,6 +63,7 @@ export function mintCode(caller: string): { code: string; expires: number } {
66
63
  const code = randomToken(12, 'base64url')
67
64
  const expires = now + CODE_LIFETIME
68
65
  CODES.set(code, { caller, expires })
66
+ minted = true
69
67
  return { code, expires }
70
68
  }
71
69
 
@@ -172,9 +170,9 @@ function dropBridge(bridge: Bridge): void {
172
170
  wakeWatchers(bridge.caller)
173
171
  }
174
172
 
175
- /** Answers `/__abide/bridge`, or `undefined` where `ABIDE_BRIDGE` is closed. 45.19. */
173
+ /** Answers `/__abide/bridge`, or `undefined` until a pairing code has been minted. 45.19. */
176
174
  export async function answerBridge(request: Request): Promise<unknown> {
177
- if (!bridgeOpened()) return undefined
175
+ if (!minted) return undefined
178
176
  if (request.method === 'GET') return pair(request)
179
177
  if (request.method === 'POST') return relay(request)
180
178
  return new Response(null, { status: 405, headers: { allow: 'GET, POST' } })
@@ -17,7 +17,6 @@ import {
17
17
  } from './assistant.ts'
18
18
  import { BRIDGE_VERSION } from './BRIDGE_VERSION.ts'
19
19
  import {
20
- bridgeOpened,
21
20
  mintCode,
22
21
  openTurn,
23
22
  pairedBridge,
@@ -110,11 +109,6 @@ function foldedPrompt(messages: AssistantMessage[]): string {
110
109
 
111
110
  /** The browser asking, which a pairing is to. 45.13. */
112
111
  function askingCaller(): string {
113
- // 45.19 — the relay is closed until `ABIDE_BRIDGE` opens it, and using it closed is a mistake to name.
114
- if (!bridgeOpened())
115
- throw new Error(
116
- 'abide: claudeCode reaches a visitor’s Claude Code through the bridge relay, and ABIDE_BRIDGE has not opened it. RULEBOOK 45.19.',
117
- )
118
112
  const caller = callerOf()
119
113
  if (caller === undefined)
120
114
  throw refuse(
@@ -1,4 +1,5 @@
1
1
  import { CLIENT_ADDRESS } from '#shared/ADDRESSES.ts'
2
+ import { tablePath } from './abideModules.ts'
2
3
  import { CLIENT_ENTRY } from './CLIENT_BUILD.ts'
3
4
 
4
5
  /**
@@ -9,13 +10,17 @@ import { CLIENT_ENTRY } from './CLIENT_BUILD.ts'
9
10
  * where the output goes and how it is named.
10
11
  */
11
12
  export function clientBuildOptions(
12
- entryDirectory: string,
13
+ root: string,
13
14
  plugins: Bun.BunPlugin[],
14
15
  publicAddresses: string[],
15
16
  ) {
16
17
  return {
17
- entrypoints: [`${entryDirectory}/${CLIENT_ENTRY}.ts`] as string[],
18
- // 16.28, 16.29 — a handler module is its wrapper here, and the rest of `server/` refuses.
18
+ // The entry is a table the plugin serves (`abideModules`), below the root.
19
+ entrypoints: [tablePath(root, `${CLIENT_ENTRY}.ts`)] as string[],
20
+ // The app's own root, because `Bun.build` opens the entry's directory otherwise, and the
21
+ // tables' is not on disk to open.
22
+ root,
23
+ // 16.28, 16.29 — a `<script server>` is its stand-ins here, and a `server/` module refuses.
19
24
  plugins,
20
25
  target: 'browser',
21
26
  format: 'esm',
@@ -30,7 +30,7 @@ export function argsDocumentOf(handler: Gated<unknown>): Document | undefined {
30
30
  /**
31
31
  * Name and value pairs as args: a name given more than once is a list, one given once its text.
32
32
  *
33
- * 16.18 — a query's and a form's members, and 38.62 — `abide call`'s `name=value` words, alike.
33
+ * 16.18 — a query's and a form's members, and 38.61 — `abide call`'s `name=value` words, alike.
34
34
  */
35
35
  export function collapsed(
36
36
  entries: URLSearchParams | FormData,
@@ -1,5 +1,5 @@
1
- // 38.13 — THE COMMAND LINE, for both hosts that have one: `abide`, over an app on disk, and a
2
- // compiled app's executable, over the app it holds (38.51). One list, so a command either has is a
1
+ // 38.12 — THE COMMAND LINE, for both hosts that have one: `abide`, over an app on disk, and a
2
+ // compiled app's executable, over the app it holds (38.50). One list, so a command either has is a
3
3
  // command both have — the executable answers the ones that need the source and the compiler with a
4
4
  // refusal naming `abide`, rather than not knowing them.
5
5
 
@@ -14,7 +14,6 @@ import { installServer } from './installServer.ts'
14
14
  import {
15
15
  appUrl,
16
16
  callHandler,
17
- followLogs,
18
17
  remoteHeaders,
19
18
  remoteUrl,
20
19
  watch as watchAddress,
@@ -30,7 +29,14 @@ type Command = (given: Given) => Promise<number | undefined>
30
29
 
31
30
  /** The commands that need the app's source and the compiler, which only `abide` holds. */
32
31
  type SourceCommands = Record<
33
- 'scaffold' | 'dev' | 'build' | 'check' | 'compile' | 'bundle' | 'lsp',
32
+ | 'scaffold'
33
+ | 'dev'
34
+ | 'build'
35
+ | 'check'
36
+ | 'sync'
37
+ | 'compile'
38
+ | 'bundle'
39
+ | 'lsp',
34
40
  Command
35
41
  >
36
42
 
@@ -41,7 +47,7 @@ export type Host = {
41
47
  /**
42
48
  * The handler table the shell lists and a call reads a handler's method off — so a call is
43
49
  * spelled the same whatever the handler's method, and `--method` is only for an app nobody here
44
- * knows the table of. Empty where there is none to read.
50
+ * knows the table of. Empty where there is none to read, and never built to be read.
45
51
  */
46
52
  handlers: (given: Given) => Promise<Mount[]>
47
53
  /** The identity a script `run` resolves its configuration with. 27.3. */
@@ -83,7 +89,7 @@ export async function runCommands(
83
89
  }
84
90
 
85
91
  // A command, and whether it is one of `SourceCommands` — which a host without them still answers
86
- // (38.52), and does not list (38.53).
92
+ // (38.51), and does not list (38.52).
87
93
  type Row = [usage: string, meaning: string, run: Command, sourced: boolean]
88
94
 
89
95
  function commandsOf(host: Host): Row[] {
@@ -133,12 +139,6 @@ function commandsOf(host: Host): Row[] {
133
139
  (given) => mcp(host, given),
134
140
  false,
135
141
  ],
136
- [
137
- 'logs',
138
- 'a live subscription to log.records',
139
- ({ values }) => followLogs(appUrl(values.url)),
140
- false,
141
- ],
142
142
  [
143
143
  'watch <address> [args]',
144
144
  "a live subscription to one of the app's followed GETs",
@@ -150,6 +150,11 @@ function commandsOf(host: Host): Row[] {
150
150
  'type-check the templates and the TypeScript, on the line you wrote',
151
151
  'check',
152
152
  ),
153
+ fromSource(
154
+ 'sync',
155
+ 'write the types a .ts reads a .abide by, into .abide/types',
156
+ 'sync',
157
+ ),
153
158
  fromSource(
154
159
  'compile',
155
160
  'the build, as one executable named after the app',
@@ -164,7 +169,7 @@ function commandsOf(host: Host): Row[] {
164
169
  ]
165
170
  }
166
171
 
167
- /** Refuses a command that needs the source, pointing at `abide`; returns the exit code. 38.52. */
172
+ /** Refuses a command that needs the source, pointing at `abide`; returns the exit code. 38.51. */
168
173
  async function needsSource(host: Host, name: string): Promise<number> {
169
174
  console.error(
170
175
  `${host.name} ${name}: needs the app's source and the compiler, which this executable does not hold. Run \`abide ${name}\` where the source is.`,
@@ -175,7 +180,7 @@ async function needsSource(host: Host, name: string): Promise<number> {
175
180
  /**
176
181
  * The usage text, listing what the host can run.
177
182
  *
178
- * 38.53 — an executable lists neither the commands it only refuses nor the flags only they read.
183
+ * 38.52 — an executable lists neither the commands it only refuses nor the flags only they read.
179
184
  */
180
185
  function usage(host: Host, commands: Row[]): string {
181
186
  const sourced = host.source !== undefined
@@ -189,16 +194,16 @@ function usage(host: Host, commands: Row[]): string {
189
194
  if (!sourced)
190
195
  return `${text}
191
196
  --port the listen port the app is served on
192
- --url the running app the shell, call, logs, openapi and mcp talk to
197
+ --url the running app the shell, call, watch, openapi and mcp talk to
193
198
  --out the file openapi writes
194
199
  `
195
200
  text += `
196
- --root the app's source directory, the one ui/ sits under —
201
+ --root the app's source directory, where its pages sit —
197
202
  src/ where you stand, or where you stand, whichever has one
198
203
  --out where the build goes, beside the app by default; the file openapi,
199
204
  compile and bundle write — named after the app, where you stand
200
205
  --port the listen port for dev, start and the shell's app
201
- --url the running app the shell, call, logs, openapi and mcp talk to
206
+ --url the running app the shell, call, watch, openapi and mcp talk to
202
207
  --target the platform compile builds for — linux-x64, darwin-arm64, windows-x64…
203
208
  --platforms several of them, comma-separated, one executable each
204
209
  `
@@ -215,7 +220,7 @@ function parsed(argv: string[]) {
215
220
  root: { type: 'string' },
216
221
  out: { type: 'string' },
217
222
  // NO DEFAULT, which is not an omission: `Bun.serve` given no port already reads
218
- // `PORT` and then falls back to 3000 — `abide dev` scanning on from there, 38.64 — and
223
+ // `PORT` and then falls back to 3000 — `abide dev` scanning on from there, 38.63 — and
219
224
  // a default written here would take that away from every host that sets it.
220
225
  port: { type: 'string' },
221
226
  url: { type: 'string' },
@@ -249,7 +254,7 @@ async function served(
249
254
  /**
250
255
  * `start`: serves the app.
251
256
  *
252
- * 38.68 — READS NO INPUT, and hands back no code for the process to exit on: the process is the
257
+ * 38.67 — READS NO INPUT, and hands back no code for the process to exit on: the process is the
253
258
  * server, for a container or a supervisor to stop.
254
259
  */
255
260
  async function start(host: Host, given: Given): Promise<number | undefined> {
@@ -263,18 +268,17 @@ async function start(host: Host, given: Given): Promise<number | undefined> {
263
268
  * The command line with no command: 38.7's shell, on the running app `--url` names or on the app
264
269
  * served here.
265
270
  *
266
- * 38.7 — WITH `--url`, BOOTS NOTHING. 38.54 — without, the app served and the shell on it, over http
267
- * as a remote one's is. 38.55 — `exit` stops the app. 38.56 — input that ends leaves it serving, and
271
+ * 38.7 — WITH `--url`, BOOTS NOTHING. 38.53 — without, the app served and the shell on it, over http
272
+ * as a remote one's is. 38.54 — `exit` stops the app. 38.55 — input that ends leaves it serving, and
268
273
  * hands back no code for the process to exit on.
269
274
  */
270
275
  async function shellOn(host: Host, given: Given): Promise<number | undefined> {
271
- const handlers = await host.handlers(given)
272
276
  const remote = remoteUrl(given.values.url)
273
277
  if (remote !== undefined) {
274
278
  const url = appUrl(remote)
275
279
  await shell({
276
280
  url,
277
- handlers,
281
+ handlers: await host.handlers(given),
278
282
  heading: `${host.name}: ${url}`,
279
283
  local: false,
280
284
  })
@@ -282,9 +286,10 @@ async function shellOn(host: Host, given: Given): Promise<number | undefined> {
282
286
  }
283
287
  const serving = await served(host, given)
284
288
  if (typeof serving === 'number') return serving
289
+ // After the serve, which built the app where nothing had.
285
290
  const ended = await shell({
286
291
  url: serving.url,
287
- handlers,
292
+ handlers: await host.handlers(given),
288
293
  heading: serving.heading,
289
294
  local: true,
290
295
  })
@@ -297,7 +302,7 @@ async function shellOn(host: Host, given: Given): Promise<number | undefined> {
297
302
  * `run`: runs a script under the abide runtime.
298
303
  *
299
304
  * 38.2 — the request scope installed, the configuration resolved against the app it stands in,
300
- * and the script run in a scope of its own — so it imports `#server` and `#shared` and reads
305
+ * and the script run in a scope of its own — so it imports through `#src/*` and reads
301
306
  * `config` without a build.
302
307
  *
303
308
  * HANDS BACK NO CODE ONCE THE SCRIPT IS RUNNING, as `start` does: the script's top level settling
@@ -316,7 +321,7 @@ async function run(
316
321
  }
317
322
  const path = resolve(file)
318
323
  process.argv = [process.argv[0] as string, path, ...rest]
319
- // 38.58 — RE-RESOLVED, as `bootApp` does: a module this process loaded before the script may already have
324
+ // 38.57 — RE-RESOLVED, as `bootApp` does: a module this process loaded before the script may already have
320
325
  // read the configuration, under no identity.
321
326
  installServer()
322
327
  APP_PACKAGE.current = host.appPackage()
@@ -326,7 +331,7 @@ async function run(
326
331
  }
327
332
 
328
333
  /** `call`: one call to one handler on a running app. 38.8. */
329
- /** `watch <address> [args]`: a `GET`'s value and each patch, as its event stream sends them (38.70). */
334
+ /** `watch <address> [args]`: a `GET`'s value and each patch, as its event stream sends them (38.69). */
330
335
  async function watch(host: Host, given: Given): Promise<number> {
331
336
  const {
332
337
  positionals: [, address, ...args],
@@ -370,13 +375,11 @@ async function call(host: Host, given: Given): Promise<number> {
370
375
  /**
371
376
  * A fetch against a generated surface, on the app `--url` names or on one booted here.
372
377
  *
373
- * 38.9 — the app booted in this process has that surface opened, and derives it from the same
374
- * route table the served one does.
378
+ * 38.9 — the app booted in this process derives it from the same route table the served one does.
375
379
  */
376
380
  async function surfaceAt(
377
381
  host: Host,
378
382
  url: string | undefined,
379
- surface: string,
380
383
  address: string,
381
384
  ): Promise<
382
385
  (
@@ -390,7 +393,6 @@ async function surfaceAt(
390
393
  ...init,
391
394
  headers: { ...init?.headers, ...remoteHeaders() },
392
395
  })
393
- process.env[surface] = '1'
394
396
  const app = await host.boot()
395
397
  return (init) =>
396
398
  app.fetch(new Request(`http://abide.local${address}`, init))
@@ -405,12 +407,10 @@ async function openapi(
405
407
  host: Host,
406
408
  { values: { url, out } }: Given,
407
409
  ): Promise<number> {
408
- const answer = await (
409
- await surfaceAt(host, url, 'ABIDE_OPENAPI', OPENAPI_ADDRESS)
410
- )()
410
+ const answer = await (await surfaceAt(host, url, OPENAPI_ADDRESS))()
411
411
  if (!answer.ok) {
412
412
  console.error(
413
- `${host.name} openapi: the app answered ${answer.status}. ABIDE_OPENAPI opens the document.`,
413
+ `${host.name} openapi: the app answered ${answer.status}. The document lists every handler with openapi in its clients; none is.`,
414
414
  )
415
415
  return 1
416
416
  }
@@ -423,11 +423,11 @@ async function openapi(
423
423
  /**
424
424
  * `mcp`: the app's MCP server over stdio.
425
425
  *
426
- * 38.9, 38.14 — a bridge a model client spawns and kills: each line in is a JSON-RPC message for
426
+ * 38.9, 38.13 — a bridge a model client spawns and kills: each line in is a JSON-RPC message for
427
427
  * `/__abide/mcp`, and each answer is a line out.
428
428
  */
429
429
  async function mcp(host: Host, { values: { url } }: Given): Promise<number> {
430
- const ask = await surfaceAt(host, url, 'ABIDE_MCP', MCP_ADDRESS)
430
+ const ask = await surfaceAt(host, url, MCP_ADDRESS)
431
431
  for await (const line of console) {
432
432
  if (line.trim() === '') continue
433
433
  const answer = await ask({
@@ -1,6 +1,8 @@
1
1
  // 35.4 — what a `.ts` holds where it imports a `.abide`: the component, called with its props to
2
- // bind them, answering the `Component` `render` takes (`componentImports`). A wildcard, so the props
2
+ // bind them, answering the `Component` `render` takes (`abideModules`). A wildcard, so the props
3
3
  // are not checked here; a declaration in a module would be an augmentation, which takes no wildcard.
4
+ // The names a `.abide` exports are typed by what 34.24 writes, which an app's tsconfig reaches
5
+ // before this.
4
6
  declare module '*.abide' {
5
7
  const component: (
6
8
  props?: Record<string, unknown>,