@abide/abide 0.101.0 → 0.103.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.
- package/CHANGELOG.md +26 -0
- package/bin/abide.ts +226 -164
- package/docs/REGISTRY.md +730 -0
- package/docs/RULEBOOK.md +2768 -0
- package/docs/abide.md +13127 -0
- package/package.json +6 -3
- package/scaffold/AGENTS.md +38 -0
- package/scaffold/CLAUDE.md +1 -0
- package/scaffold/src/layout.abide +18 -0
- package/src/compiler/Lowering.ts +78 -9
- package/src/compiler/ShadowMap.ts +2 -2
- package/src/compiler/TypeSession.ts +341 -71
- package/src/compiler/appMap.ts +96 -0
- package/src/compiler/argsSchemaOf.ts +1 -1
- package/src/compiler/checkApp.ts +53 -8
- package/src/compiler/checkedShadow.ts +73 -6
- package/src/compiler/classifyRead.ts +6 -4
- package/src/compiler/classifyTemplate.ts +1 -1
- package/src/compiler/compileApp.ts +598 -200
- package/src/compiler/compiledEntry.ts +48 -40
- package/src/compiler/componentGraph.ts +57 -71
- package/src/compiler/declaredExports.ts +26 -9
- package/src/compiler/emitClient.ts +142 -72
- package/src/compiler/emitComponent.ts +41 -12
- package/src/compiler/emitSource.ts +102 -13
- package/src/compiler/handlersUnder.ts +164 -76
- package/src/compiler/hooksCalled.ts +35 -0
- package/src/compiler/index.ts +7 -0
- package/src/compiler/mountsOf.ts +150 -0
- package/src/compiler/nearest.ts +14 -0
- package/src/compiler/parseAbide.ts +157 -20
- package/src/compiler/projectShadow.ts +250 -19
- package/src/compiler/reactiveTypes.ts +20 -7
- package/src/compiler/readSources.ts +15 -2
- package/src/compiler/readsIn.ts +62 -8
- package/src/compiler/renderMarkup.ts +3 -3
- package/src/compiler/scaffold.ts +37 -15
- package/src/compiler/serveLanguage.ts +101 -34
- package/src/compiler/sharedModuleWrites.ts +2 -2
- package/src/compiler/sourceMapOf.ts +193 -0
- package/src/compiler/splitImports.ts +205 -83
- package/src/compiler/tokenize.ts +14 -0
- package/src/compiler/writeDefaultProject.ts +29 -2
- package/src/server/CLIENT_BUILD.ts +11 -13
- package/src/server/DEFAULT_PORT.ts +1 -1
- package/src/server/MODULE_QUERIES.ts +27 -0
- package/src/server/TABLES.ts +7 -0
- package/src/server/VIRTUAL_DIRECTORY.ts +4 -5
- package/src/server/abideModules.ts +457 -0
- package/src/server/answerFailure.ts +16 -6
- package/src/server/answerRequest.ts +241 -107
- package/src/server/answerRpc.ts +107 -33
- package/src/server/answerSocket.ts +65 -20
- package/src/server/answerValue.ts +22 -9
- package/src/server/appDirectories.ts +1 -1
- package/src/server/appPlugins.ts +1 -1
- package/src/server/appTree.ts +238 -0
- package/src/server/argsFor.ts +2 -2
- package/src/server/assistant.ts +2 -2
- package/src/server/bridgeRelay.ts +6 -8
- package/src/server/claudeCode.ts +0 -6
- package/src/server/clientBuildOptions.ts +8 -3
- package/src/server/coerceArgs.ts +1 -1
- package/src/server/commands.ts +38 -37
- package/src/server/componentModules.d.ts +2 -1
- package/src/server/compressAnswer.ts +4 -4
- package/src/server/config.ts +25 -29
- package/src/server/configFields.ts +31 -25
- package/src/server/createApp.ts +77 -79
- package/src/server/devWorker.ts +118 -181
- package/src/server/executableName.ts +4 -0
- package/src/server/fenceServer.ts +57 -32
- package/src/server/generatedSurfaces.ts +99 -66
- package/src/server/hooks.ts +137 -52
- package/src/server/iconAccent.ts +191 -0
- package/src/server/index.ts +12 -7
- package/src/server/installSourceMaps.ts +93 -0
- package/src/server/launcherOptions.ts +33 -31
- package/src/server/launcherPage.ts +107 -64
- package/src/server/manifestOf.ts +16 -13
- package/src/server/namesUnder.ts +2 -2
- package/src/server/nativeMenu.ts +6 -6
- package/src/server/negotiateEncoding.ts +1 -1
- package/src/server/preload.ts +13 -0
- package/src/server/principal.ts +10 -5
- package/src/server/realOrAsGiven.ts +10 -0
- package/src/server/refuseAddressCollision.ts +21 -14
- package/src/server/remote.ts +49 -52
- package/src/server/render.ts +65 -94
- package/src/server/renderComponent.ts +5 -0
- package/src/server/reportHealth.ts +2 -2
- package/src/server/requestScope.ts +12 -1
- package/src/server/runBuild.ts +24 -0
- package/src/server/runCompiled.ts +5 -5
- package/src/server/runDesktop.ts +48 -28
- package/src/server/serveBuild.ts +43 -52
- package/src/server/serveDev.ts +79 -37
- package/src/server/shell.ts +7 -22
- package/src/server/sourceMappings.ts +139 -0
- package/src/server/surfaceOf.ts +20 -3
- package/src/server/testBuild.ts +1 -19
- package/src/server/withAbideSources.ts +117 -0
- package/src/shared/ADDRESSES.ts +10 -18
- package/src/shared/APP_META.ts +7 -0
- package/src/shared/CopyOptions.ts +1 -1
- package/src/shared/DEBUG_META.ts +1 -1
- package/src/shared/LAUNCHER_FILES.ts +9 -0
- package/src/shared/RESERVED_DIRECTORIES.ts +24 -0
- package/src/shared/cookies.ts +2 -2
- package/src/shared/csp.ts +1 -1
- package/src/shared/escapeText.ts +1 -1
- package/src/shared/filledPath.ts +85 -0
- package/src/shared/framing.ts +79 -6
- package/src/shared/headKeyOf.ts +2 -0
- package/src/shared/index.ts +2 -1
- package/src/shared/log.ts +27 -13
- package/src/shared/matchRoute.ts +23 -4
- package/src/{ui → shared}/menuBridge.ts +2 -2
- package/src/shared/menuItem.ts +4 -4
- package/src/shared/onMenu.ts +58 -0
- package/src/shared/reactive/Keyed.ts +2 -2
- package/src/shared/reactive/NotLanded.ts +1 -1
- package/src/shared/reactive/Reactive.ts +43 -43
- package/src/shared/reactive/argsContract.ts +4 -4
- package/src/shared/reactive/canonicalKey.ts +4 -4
- package/src/shared/reactive/chunksOfStream.ts +6 -0
- package/src/shared/reactive/index.ts +1 -1
- package/src/shared/reactive/recordWrites.ts +103 -5
- package/src/shared/reactive/structural.ts +1 -1
- package/src/shared/reactive/tracking.ts +1 -1
- package/src/shared/reactive/watch.ts +3 -3
- package/src/shared/refuse.ts +1 -1
- package/src/{ui → shared}/reportMenu.ts +8 -10
- package/src/shared/routePattern.ts +122 -48
- package/src/shared/rpc.ts +37 -8
- package/src/shared/rpcRequest.ts +27 -3
- package/src/shared/url.ts +4 -44
- package/src/shared/writePath.ts +1 -1
- package/src/ui/LAYER_NAMES.ts +1 -1
- package/src/ui/List.ts +3 -3
- package/src/ui/MOUNTING_LEVEL.ts +18 -0
- package/src/ui/Mismatch.ts +1 -1
- package/src/ui/RawRun.ts +1 -1
- package/src/ui/Router.ts +37 -36
- package/src/ui/Scope.ts +1 -1
- package/src/ui/TryRegion.ts +2 -2
- package/src/ui/applyHead.ts +33 -0
- package/src/ui/index.ts +1 -1
- package/src/ui/moveNode.ts +1 -1
- package/src/ui/reconcile.ts +2 -2
- package/src/ui/routing.ts +2 -2
- package/src/ui/setAttribute.ts +1 -1
- package/src/ui/setText.ts +1 -1
- package/src/ui/socketWire.ts +9 -7
- package/src/ui/wire.ts +1 -1
- package/scaffold/src/server/app.ts +0 -6
- package/scaffold/src/shared/.gitkeep +0 -0
- package/scaffold/src/ui/app.html +0 -8
- package/src/server/componentImports.ts +0 -56
- /package/scaffold/src/{ui/pages/page.abide → index.abide} +0 -0
- /package/src/{compiler → shared}/SPECIFIERS.ts +0 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// 23.14 — WHERE AN APP'S ADDRESSES ARE, read off the tree in one walk. A file directly in a directory
|
|
2
|
+
// below the root is an address — a page, a layout, an error page or a handler file — and nothing
|
|
3
|
+
// else is there (23.22); a fence is never one (23.15): `server` fences a graph, `public` holds files
|
|
4
|
+
// answered as authored at the path of the directories above it, and `lib` and `tests` hold the rest.
|
|
5
|
+
// So listing a directory lists its routes.
|
|
6
|
+
//
|
|
7
|
+
// ONE WALK for the sources, the handler files, the server and lib modules, and the public files
|
|
8
|
+
// `abide build` copies and `abide dev` answers, and the refusals (23.16, 23.17, 23.18, 23.22) — and
|
|
9
|
+
// ONE SKIP RULE, `skipped`, for every walk of the tree, beside `placeOf`, which answers the same
|
|
10
|
+
// question of one path.
|
|
11
|
+
//
|
|
12
|
+
// A WALK RATHER THAN A GLOB: a glob descends `node_modules` and the build output before a filter
|
|
13
|
+
// can drop them.
|
|
14
|
+
|
|
15
|
+
import { type Dirent, existsSync, readdirSync } from 'node:fs' // bun has no directory listing or sync existence check of its own
|
|
16
|
+
import { ABIDE_ADDRESS } from '#shared/ADDRESSES.ts'
|
|
17
|
+
import {
|
|
18
|
+
LIB_DIRECTORY,
|
|
19
|
+
PUBLIC_DIRECTORY,
|
|
20
|
+
RESERVED_DIRECTORIES,
|
|
21
|
+
SERVER_DIRECTORY,
|
|
22
|
+
} from '#shared/RESERVED_DIRECTORIES.ts'
|
|
23
|
+
import { DIRECTORY_FILES, routeOf, skipped } from '#shared/routePattern.ts'
|
|
24
|
+
|
|
25
|
+
/** What one walk of the app finds, every path relative to the root. */
|
|
26
|
+
type AppFiles = {
|
|
27
|
+
/** Every `.abide`, in path order. */
|
|
28
|
+
abide: string[]
|
|
29
|
+
/** Every handler file, in path order. */
|
|
30
|
+
handlers: string[]
|
|
31
|
+
/** Every module under a `server` or a `lib` directory, in path order — run as it is imported. */
|
|
32
|
+
modules: string[]
|
|
33
|
+
/** Every file under a `public` directory, to the address it is answered at below the mount. */
|
|
34
|
+
published: Map<string, string>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Told of each thing the tree refuses, at the path relative to the root. */
|
|
38
|
+
type Refuse = (path: string, message: string) => void
|
|
39
|
+
|
|
40
|
+
const THROW: Refuse = (_path, message) => {
|
|
41
|
+
throw new Error(message)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The app's `.abide` sources, handler files, server and lib modules and public files, refusing a
|
|
46
|
+
* tree 23.16, 23.17, 23.18 or 23.22 forbids.
|
|
47
|
+
*
|
|
48
|
+
* `refuse` throws by default, which is the build's answer; `abide check` collects instead (38.33),
|
|
49
|
+
* and the checker's route table ignores them, since a refused tree is the build's to report.
|
|
50
|
+
*
|
|
51
|
+
* A missing root is an app with nothing in it, not a fault.
|
|
52
|
+
*/
|
|
53
|
+
export function appFiles(root: string, refuse: Refuse = THROW): AppFiles {
|
|
54
|
+
const files: AppFiles = {
|
|
55
|
+
abide: [],
|
|
56
|
+
handlers: [],
|
|
57
|
+
modules: [],
|
|
58
|
+
published: new Map(),
|
|
59
|
+
}
|
|
60
|
+
// 23.22, 38.25 — a root that is its package's directory holds the package's own files beside its
|
|
61
|
+
// pages.
|
|
62
|
+
const packaged = existsSync(`${root}/package.json`)
|
|
63
|
+
walk(root, '', undefined, files, refuse, packaged)
|
|
64
|
+
files.abide.sort()
|
|
65
|
+
files.handlers.sort()
|
|
66
|
+
files.modules.sort()
|
|
67
|
+
return files
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// `reserved` is the nearest reserved directory above `relative` — its path and its name — or
|
|
71
|
+
// `undefined` where none is.
|
|
72
|
+
function walk(
|
|
73
|
+
directory: string,
|
|
74
|
+
relative: string,
|
|
75
|
+
reserved: { path: string; name: string } | undefined,
|
|
76
|
+
files: AppFiles,
|
|
77
|
+
refuse: Refuse,
|
|
78
|
+
packaged = false,
|
|
79
|
+
): void {
|
|
80
|
+
let entries: Dirent[]
|
|
81
|
+
try {
|
|
82
|
+
entries = readdirSync(directory, { withFileTypes: true })
|
|
83
|
+
} catch (thrown) {
|
|
84
|
+
if ((thrown as { code?: unknown }).code === 'ENOENT') return
|
|
85
|
+
throw thrown
|
|
86
|
+
}
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
if (skipped(entry.name)) continue
|
|
89
|
+
const path = `${relative}${entry.name}`
|
|
90
|
+
if (entry.isDirectory()) {
|
|
91
|
+
let within = reserved
|
|
92
|
+
if (RESERVED_DIRECTORIES.has(entry.name)) {
|
|
93
|
+
if (reserved !== undefined)
|
|
94
|
+
refuse(
|
|
95
|
+
path,
|
|
96
|
+
`abide: ${path} is a ${entry.name} directory under the ${reserved.name} directory ${reserved.path}. RULEBOOK 23.16.`,
|
|
97
|
+
)
|
|
98
|
+
within = { path, name: entry.name }
|
|
99
|
+
}
|
|
100
|
+
walk(
|
|
101
|
+
`${directory}/${entry.name}`,
|
|
102
|
+
`${path}/`,
|
|
103
|
+
within,
|
|
104
|
+
files,
|
|
105
|
+
refuse,
|
|
106
|
+
)
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
const abide = path.endsWith('.abide')
|
|
110
|
+
if (abide) files.abide.push(path)
|
|
111
|
+
if (reserved === undefined) {
|
|
112
|
+
if (!abide && !isModule(path)) {
|
|
113
|
+
if (packaged && relative === '' && !path.endsWith('.ts'))
|
|
114
|
+
continue
|
|
115
|
+
refuse(
|
|
116
|
+
path,
|
|
117
|
+
`abide: ${path} is directly in an address directory, which holds pages, layouts, error pages and handler files alone. Move it into a lib directory beside it. RULEBOOK 23.22.`,
|
|
118
|
+
)
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
if (!abide) files.handlers.push(path)
|
|
122
|
+
// 23.17 — anything that could answer here would answer in abide's own namespace.
|
|
123
|
+
refuseFrameworkRoute(routeOf(path), path, refuse)
|
|
124
|
+
} else if (DIRECTORY_FILES.has(entry.name))
|
|
125
|
+
refuse(
|
|
126
|
+
path,
|
|
127
|
+
`abide: ${path} is under the ${reserved.name} directory ${reserved.path}, where nothing has an address, and ${entry.name} is a route's name. RULEBOOK 23.18.`,
|
|
128
|
+
)
|
|
129
|
+
else if (reserved.name === PUBLIC_DIRECTORY)
|
|
130
|
+
files.published.set(path, publicAddress(path, reserved.path))
|
|
131
|
+
else if (
|
|
132
|
+
(reserved.name === SERVER_DIRECTORY ||
|
|
133
|
+
reserved.name === LIB_DIRECTORY) &&
|
|
134
|
+
isModule(path)
|
|
135
|
+
)
|
|
136
|
+
files.modules.push(path)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Whether `path` names a module the app runs — neither a declaration nor a test: directly in an
|
|
142
|
+
* address directory, a handler file rather than a stray.
|
|
143
|
+
*/
|
|
144
|
+
function isModule(path: string): boolean {
|
|
145
|
+
return (
|
|
146
|
+
path.endsWith('.ts') &&
|
|
147
|
+
!path.endsWith('.d.ts') &&
|
|
148
|
+
!path.endsWith('.test.ts')
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Refuses `source` where `route` lies in the framework's own namespace. 23.17. */
|
|
153
|
+
export function refuseFrameworkRoute(
|
|
154
|
+
route: string,
|
|
155
|
+
source: string,
|
|
156
|
+
refuse: Refuse = THROW,
|
|
157
|
+
): void {
|
|
158
|
+
if (route === ABIDE_ADDRESS || route.startsWith(`${ABIDE_ADDRESS}/`))
|
|
159
|
+
refuse(
|
|
160
|
+
source,
|
|
161
|
+
`abide: ${source} answers at ${route}, which is under ${ABIDE_ADDRESS}, where abide's own addresses are. RULEBOOK 23.17.`,
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 36.11 — the directories above `public`, then the file's own path below it.
|
|
166
|
+
function publicAddress(path: string, directory: string): string {
|
|
167
|
+
const above = directory.slice(0, -PUBLIC_DIRECTORY.length)
|
|
168
|
+
return `/${above}${path.slice(directory.length + 1)}`
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The public files as the server reads them: each address, without its leading `/`, to the file.
|
|
173
|
+
*
|
|
174
|
+
* 38.19 — out of the source tree, where `abide dev` answers them.
|
|
175
|
+
*/
|
|
176
|
+
export function publishedUnder(
|
|
177
|
+
root: string,
|
|
178
|
+
published: Map<string, string>,
|
|
179
|
+
): Map<string, string> {
|
|
180
|
+
const paths = new Map<string, string>()
|
|
181
|
+
for (const [source, address] of published)
|
|
182
|
+
paths.set(address.slice(1), `${root}/${source}`)
|
|
183
|
+
return paths
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Every `.abide` under `root`, relative to it, in path order — the app's own, and none a
|
|
188
|
+
* dependency ships.
|
|
189
|
+
*
|
|
190
|
+
* Refuses nothing: the language server lists them on every check, and a tree 23.16 forbids is the
|
|
191
|
+
* build's to refuse (`appFiles`).
|
|
192
|
+
*/
|
|
193
|
+
export function abideUnder(root: string): string[] {
|
|
194
|
+
const found: string[] = []
|
|
195
|
+
for (const path of abideFilesUnder(root, '')) found.push(path)
|
|
196
|
+
return found.sort()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Whether an app lies under `root`: a `.abide` anywhere or a handler file, stopping at the first.
|
|
201
|
+
*
|
|
202
|
+
* 38.26's question, asked of a root before anything is built from it, so asking it of a large tree
|
|
203
|
+
* is as cheap as the tree allows.
|
|
204
|
+
*/
|
|
205
|
+
export function holdsApp(root: string): boolean {
|
|
206
|
+
return !abideFilesUnder(root, '', true).next().done
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Every `.abide` under `directory` — and, where `handlers`, every handler file outside a fence.
|
|
210
|
+
function* abideFilesUnder(
|
|
211
|
+
directory: string,
|
|
212
|
+
relative: string,
|
|
213
|
+
handlers = false,
|
|
214
|
+
fenced = false,
|
|
215
|
+
): Generator<string> {
|
|
216
|
+
let entries: Dirent[]
|
|
217
|
+
try {
|
|
218
|
+
entries = readdirSync(directory, { withFileTypes: true })
|
|
219
|
+
} catch {
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
for (const entry of entries) {
|
|
223
|
+
if (skipped(entry.name)) continue
|
|
224
|
+
const path = `${relative}${entry.name}`
|
|
225
|
+
if (entry.isDirectory())
|
|
226
|
+
yield* abideFilesUnder(
|
|
227
|
+
`${directory}/${entry.name}`,
|
|
228
|
+
`${path}/`,
|
|
229
|
+
handlers,
|
|
230
|
+
fenced || RESERVED_DIRECTORIES.has(entry.name),
|
|
231
|
+
)
|
|
232
|
+
else if (
|
|
233
|
+
path.endsWith('.abide') ||
|
|
234
|
+
(handlers && !fenced && isModule(path))
|
|
235
|
+
)
|
|
236
|
+
yield path
|
|
237
|
+
}
|
|
238
|
+
}
|
package/src/server/argsFor.ts
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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)
|
package/src/server/assistant.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
57
|
-
|
|
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`
|
|
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 (!
|
|
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' } })
|
package/src/server/claudeCode.ts
CHANGED
|
@@ -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
|
-
|
|
13
|
+
root: string,
|
|
13
14
|
plugins: Bun.BunPlugin[],
|
|
14
15
|
publicAddresses: string[],
|
|
15
16
|
) {
|
|
16
17
|
return {
|
|
17
|
-
|
|
18
|
-
|
|
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',
|
package/src/server/coerceArgs.ts
CHANGED
|
@@ -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.
|
|
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,
|
package/src/server/commands.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// 38.
|
|
2
|
-
// compiled app's executable, over the app it holds (38.
|
|
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
|
-
|
|
32
|
+
| 'scaffold'
|
|
33
|
+
| 'dev'
|
|
34
|
+
| 'build'
|
|
35
|
+
| 'check'
|
|
36
|
+
| 'routes'
|
|
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.
|
|
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
|
+
'routes [--json]',
|
|
155
|
+
'every address the app answers, its file, and where each hook is filed',
|
|
156
|
+
'routes',
|
|
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.
|
|
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.
|
|
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,
|
|
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,
|
|
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,
|
|
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,13 +220,14 @@ 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.
|
|
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' },
|
|
222
227
|
method: { type: 'string' },
|
|
223
228
|
target: { type: 'string' },
|
|
224
229
|
platforms: { type: 'string' },
|
|
230
|
+
json: { type: 'boolean' },
|
|
225
231
|
help: { type: 'boolean', short: 'h' },
|
|
226
232
|
},
|
|
227
233
|
allowPositionals: true,
|
|
@@ -249,7 +255,7 @@ async function served(
|
|
|
249
255
|
/**
|
|
250
256
|
* `start`: serves the app.
|
|
251
257
|
*
|
|
252
|
-
* 38.
|
|
258
|
+
* 38.67 — READS NO INPUT, and hands back no code for the process to exit on: the process is the
|
|
253
259
|
* server, for a container or a supervisor to stop.
|
|
254
260
|
*/
|
|
255
261
|
async function start(host: Host, given: Given): Promise<number | undefined> {
|
|
@@ -263,18 +269,17 @@ async function start(host: Host, given: Given): Promise<number | undefined> {
|
|
|
263
269
|
* The command line with no command: 38.7's shell, on the running app `--url` names or on the app
|
|
264
270
|
* served here.
|
|
265
271
|
*
|
|
266
|
-
* 38.7 — WITH `--url`, BOOTS NOTHING. 38.
|
|
267
|
-
* as a remote one's is. 38.
|
|
272
|
+
* 38.7 — WITH `--url`, BOOTS NOTHING. 38.53 — without, the app served and the shell on it, over http
|
|
273
|
+
* as a remote one's is. 38.54 — `exit` stops the app. 38.55 — input that ends leaves it serving, and
|
|
268
274
|
* hands back no code for the process to exit on.
|
|
269
275
|
*/
|
|
270
276
|
async function shellOn(host: Host, given: Given): Promise<number | undefined> {
|
|
271
|
-
const handlers = await host.handlers(given)
|
|
272
277
|
const remote = remoteUrl(given.values.url)
|
|
273
278
|
if (remote !== undefined) {
|
|
274
279
|
const url = appUrl(remote)
|
|
275
280
|
await shell({
|
|
276
281
|
url,
|
|
277
|
-
handlers,
|
|
282
|
+
handlers: await host.handlers(given),
|
|
278
283
|
heading: `${host.name}: ${url}`,
|
|
279
284
|
local: false,
|
|
280
285
|
})
|
|
@@ -282,9 +287,10 @@ async function shellOn(host: Host, given: Given): Promise<number | undefined> {
|
|
|
282
287
|
}
|
|
283
288
|
const serving = await served(host, given)
|
|
284
289
|
if (typeof serving === 'number') return serving
|
|
290
|
+
// After the serve, which built the app where nothing had.
|
|
285
291
|
const ended = await shell({
|
|
286
292
|
url: serving.url,
|
|
287
|
-
handlers,
|
|
293
|
+
handlers: await host.handlers(given),
|
|
288
294
|
heading: serving.heading,
|
|
289
295
|
local: true,
|
|
290
296
|
})
|
|
@@ -297,7 +303,7 @@ async function shellOn(host: Host, given: Given): Promise<number | undefined> {
|
|
|
297
303
|
* `run`: runs a script under the abide runtime.
|
|
298
304
|
*
|
|
299
305
|
* 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
|
|
306
|
+
* and the script run in a scope of its own — so it imports through `#src/*` and reads
|
|
301
307
|
* `config` without a build.
|
|
302
308
|
*
|
|
303
309
|
* HANDS BACK NO CODE ONCE THE SCRIPT IS RUNNING, as `start` does: the script's top level settling
|
|
@@ -316,7 +322,7 @@ async function run(
|
|
|
316
322
|
}
|
|
317
323
|
const path = resolve(file)
|
|
318
324
|
process.argv = [process.argv[0] as string, path, ...rest]
|
|
319
|
-
// 38.
|
|
325
|
+
// 38.57 — RE-RESOLVED, as `bootApp` does: a module this process loaded before the script may already have
|
|
320
326
|
// read the configuration, under no identity.
|
|
321
327
|
installServer()
|
|
322
328
|
APP_PACKAGE.current = host.appPackage()
|
|
@@ -326,7 +332,7 @@ async function run(
|
|
|
326
332
|
}
|
|
327
333
|
|
|
328
334
|
/** `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.
|
|
335
|
+
/** `watch <address> [args]`: a `GET`'s value and each patch, as its event stream sends them (38.69). */
|
|
330
336
|
async function watch(host: Host, given: Given): Promise<number> {
|
|
331
337
|
const {
|
|
332
338
|
positionals: [, address, ...args],
|
|
@@ -370,13 +376,11 @@ async function call(host: Host, given: Given): Promise<number> {
|
|
|
370
376
|
/**
|
|
371
377
|
* A fetch against a generated surface, on the app `--url` names or on one booted here.
|
|
372
378
|
*
|
|
373
|
-
* 38.9 — the app booted in this process
|
|
374
|
-
* route table the served one does.
|
|
379
|
+
* 38.9 — the app booted in this process derives it from the same route table the served one does.
|
|
375
380
|
*/
|
|
376
381
|
async function surfaceAt(
|
|
377
382
|
host: Host,
|
|
378
383
|
url: string | undefined,
|
|
379
|
-
surface: string,
|
|
380
384
|
address: string,
|
|
381
385
|
): Promise<
|
|
382
386
|
(
|
|
@@ -390,7 +394,6 @@ async function surfaceAt(
|
|
|
390
394
|
...init,
|
|
391
395
|
headers: { ...init?.headers, ...remoteHeaders() },
|
|
392
396
|
})
|
|
393
|
-
process.env[surface] = '1'
|
|
394
397
|
const app = await host.boot()
|
|
395
398
|
return (init) =>
|
|
396
399
|
app.fetch(new Request(`http://abide.local${address}`, init))
|
|
@@ -405,12 +408,10 @@ async function openapi(
|
|
|
405
408
|
host: Host,
|
|
406
409
|
{ values: { url, out } }: Given,
|
|
407
410
|
): Promise<number> {
|
|
408
|
-
const answer = await (
|
|
409
|
-
await surfaceAt(host, url, 'ABIDE_OPENAPI', OPENAPI_ADDRESS)
|
|
410
|
-
)()
|
|
411
|
+
const answer = await (await surfaceAt(host, url, OPENAPI_ADDRESS))()
|
|
411
412
|
if (!answer.ok) {
|
|
412
413
|
console.error(
|
|
413
|
-
`${host.name} openapi: the app answered ${answer.status}.
|
|
414
|
+
`${host.name} openapi: the app answered ${answer.status}. The document lists every handler with openapi in its clients; none is.`,
|
|
414
415
|
)
|
|
415
416
|
return 1
|
|
416
417
|
}
|
|
@@ -423,11 +424,11 @@ async function openapi(
|
|
|
423
424
|
/**
|
|
424
425
|
* `mcp`: the app's MCP server over stdio.
|
|
425
426
|
*
|
|
426
|
-
* 38.9, 38.
|
|
427
|
+
* 38.9, 38.13 — a bridge a model client spawns and kills: each line in is a JSON-RPC message for
|
|
427
428
|
* `/__abide/mcp`, and each answer is a line out.
|
|
428
429
|
*/
|
|
429
430
|
async function mcp(host: Host, { values: { url } }: Given): Promise<number> {
|
|
430
|
-
const ask = await surfaceAt(host, url,
|
|
431
|
+
const ask = await surfaceAt(host, url, MCP_ADDRESS)
|
|
431
432
|
for await (const line of console) {
|
|
432
433
|
if (line.trim() === '') continue
|
|
433
434
|
const answer = await ask({
|
|
@@ -1,6 +1,7 @@
|
|
|
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 (`
|
|
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 component is all a `.abide` exports (34.17).
|
|
4
5
|
declare module '*.abide' {
|
|
5
6
|
const component: (
|
|
6
7
|
props?: Record<string, unknown>,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// 38.
|
|
1
|
+
// 38.29 — AN ANSWER, COMPRESSED WHOLE: a document, and a JSON value at least `COMPRESSION_FLOOR`
|
|
2
2
|
// bytes long. Both are in hand before a byte goes out — `render` sends nothing until the page is
|
|
3
3
|
// built (41.1), and a value is stringified once — so one compression is the whole answer, with its
|
|
4
4
|
// length known, where a stream compressed as it arrived had to be flushed at every chunk.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// WHAT EACH LANE SENDS BESIDE THE BYTES is here too, so the two cannot drift. A document names
|
|
7
7
|
// `accept-encoding` in `vary` through `DOCUMENT_HEADERS`, which its `HEAD` and error answers carry as
|
|
8
8
|
// well; a JSON answer names it here. Both say their length: `abide dev` relays a body chunk by chunk
|
|
9
|
-
// from its worker (38.
|
|
9
|
+
// from its worker (38.35), and a relayed body with no declared length goes out chunked.
|
|
10
10
|
// BUN'S GZIP parts from zlib's past ~130 KB, which only the off-loop lane reaches — so that lane takes
|
|
11
11
|
// zlib's, the loop keeps bun's, and the two make the same bytes.
|
|
12
12
|
import { brotliCompress, brotliCompressSync, constants, gzip } from 'node:zlib' // bun has no brotli, and no compression off the loop
|
|
@@ -21,12 +21,12 @@ import {
|
|
|
21
21
|
// already under gzip's size at twice gzip's 70 µs.
|
|
22
22
|
const BROTLI_OPTIONS = { params: { [constants.BROTLI_PARAM_QUALITY]: 5 } }
|
|
23
23
|
|
|
24
|
-
// 38.
|
|
24
|
+
// 38.68 — the bytes under which a JSON answer goes out as it is. Under it an answer saves tens of
|
|
25
25
|
// bytes that leave in the same packet anyway, for 3–8 µs of a ~57 µs request; at 8 KB the same CPU
|
|
26
26
|
// saves seven kilobytes.
|
|
27
27
|
const COMPRESSION_FLOOR = 1024
|
|
28
28
|
|
|
29
|
-
// 38.
|
|
29
|
+
// 38.95 — the bytes from which an answer is compressed off the event loop. Brotli 5 holds the loop
|
|
30
30
|
// ~360 µs at 64 KB and ~7 ms at 700 KB — every other request and socket waits that long — where
|
|
31
31
|
// the thread pool's hop adds ~20 µs to the answer it carries: 6% at 64 KB, 27% at 16 KB.
|
|
32
32
|
const OFF_LOOP_FLOOR = 64 * 1024
|