@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.
- package/CHANGELOG.md +31 -0
- package/bin/abide.ts +227 -163
- package/docs/REGISTRY.md +726 -0
- package/docs/RULEBOOK.md +2753 -0
- package/docs/abide.md +12878 -0
- package/package.json +10 -3
- package/scaffold/AGENTS.md +31 -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 +262 -67
- package/src/compiler/argsSchemaOf.ts +1 -1
- package/src/compiler/checkApp.ts +43 -15
- package/src/compiler/checkedShadow.ts +39 -5
- package/src/compiler/classifyRead.ts +6 -4
- package/src/compiler/classifyTemplate.ts +1 -1
- package/src/compiler/compileApp.ts +433 -193
- package/src/compiler/compiledEntry.ts +43 -39
- package/src/compiler/componentGraph.ts +58 -71
- package/src/compiler/declaredExports.ts +24 -7
- package/src/compiler/emitClient.ts +141 -71
- package/src/compiler/emitComponent.ts +37 -8
- package/src/compiler/emitSource.ts +106 -13
- package/src/compiler/handlersUnder.ts +106 -56
- package/src/compiler/index.ts +7 -0
- package/src/compiler/mountsOf.ts +138 -0
- package/src/compiler/nearest.ts +14 -0
- package/src/compiler/parseAbide.ts +157 -20
- package/src/compiler/projectShadow.ts +213 -7
- package/src/compiler/reactiveTypes.ts +20 -7
- package/src/compiler/readSources.ts +15 -2
- package/src/compiler/readsIn.ts +61 -7
- package/src/compiler/renderMarkup.ts +3 -3
- package/src/compiler/scaffold.ts +30 -11
- package/src/compiler/serveLanguage.ts +125 -34
- package/src/compiler/sourceMapOf.ts +193 -0
- package/src/compiler/splitImports.ts +205 -83
- package/src/compiler/syncTypes.ts +32 -0
- package/src/compiler/tokenize.ts +14 -0
- package/src/compiler/writeDefaultProject.ts +1 -1
- package/src/compiler/writeTypes.ts +82 -0
- package/src/server/CLIENT_BUILD.ts +11 -13
- package/src/server/DEFAULT_PORT.ts +1 -1
- package/src/server/LAUNCHER_FILES.ts +9 -0
- package/src/server/MODULE_QUERIES.ts +18 -0
- package/src/server/TABLES.ts +7 -0
- package/src/server/VIRTUAL_DIRECTORY.ts +4 -5
- package/src/server/abideModules.ts +391 -0
- package/src/server/answerFailure.ts +16 -6
- package/src/server/answerRequest.ts +235 -106
- package/src/server/answerRpc.ts +124 -39
- package/src/server/answerSocket.ts +141 -48
- 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 +179 -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 +37 -37
- package/src/server/componentModules.d.ts +3 -1
- package/src/server/compressAnswer.ts +48 -14
- package/src/server/config.ts +25 -29
- package/src/server/configFields.ts +31 -25
- package/src/server/createApp.ts +59 -73
- package/src/server/devWorker.ts +144 -183
- package/src/server/executableName.ts +4 -0
- package/src/server/fenceServer.ts +37 -35
- package/src/server/generatedSurfaces.ts +99 -66
- package/src/server/hooks.ts +124 -52
- package/src/server/iconAccent.ts +191 -0
- package/src/server/index.ts +11 -7
- package/src/server/installSourceMaps.ts +88 -0
- package/src/server/launcherOptions.ts +34 -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 +5 -5
- package/src/server/negotiateEncoding.ts +1 -1
- 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 +52 -27
- package/src/server/serveBuild.ts +43 -52
- package/src/server/serveDev.ts +81 -39
- package/src/server/shell.ts +7 -22
- package/src/server/sourceMappings.ts +139 -0
- package/src/server/surfaceOf.ts +18 -3
- package/src/server/testBuild.ts +1 -19
- package/src/server/withAbideSources.ts +117 -0
- package/src/shared/ADDRESSES.ts +7 -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/GLOBAL_GETS.ts +9 -3
- package/src/shared/RESERVED_DIRECTORIES.ts +15 -0
- package/src/shared/RPC_SERVER.ts +2 -2
- 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 +1 -1
- package/src/shared/log.ts +27 -13
- package/src/shared/matchRoute.ts +23 -4
- package/src/shared/menuItem.ts +3 -3
- package/src/shared/reactive/Cursor.ts +3 -9
- package/src/shared/reactive/Keyed.ts +4 -3
- package/src/shared/reactive/NotLanded.ts +1 -1
- package/src/shared/reactive/Reactive.ts +81 -60
- 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 +155 -21
- package/src/shared/reactive/watch.ts +10 -6
- package/src/shared/refuse.ts +1 -1
- package/src/shared/routePattern.ts +82 -46
- package/src/shared/rpc.ts +47 -13
- 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 +38 -31
- 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/menuBridge.ts +1 -1
- package/src/ui/moveNode.ts +1 -1
- package/src/ui/reconcile.ts +2 -2
- package/src/ui/reportMenu.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 → 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
|
|
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
|
|
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
|
-
|
|
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
|
|
318
|
-
// ranges a file-backed
|
|
319
|
-
//
|
|
320
|
-
|
|
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
|
|
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[] {
|
package/src/server/appPlugins.ts
CHANGED
|
@@ -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.
|
|
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
|
+
}
|
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
|
+
| '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.
|
|
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.
|
|
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,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.
|
|
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.
|
|
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.
|
|
267
|
-
* as a remote one's is. 38.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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}.
|
|
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.
|
|
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,
|
|
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 (`
|
|
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>,
|