@abide/abide 0.104.0 → 0.105.1
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 +22 -0
- package/bin/abide.ts +80 -173
- package/docs/REGISTRY.md +17 -17
- package/docs/RULEBOOK.md +50 -12
- package/docs/abide.md +9 -9
- package/package.json +1 -1
- package/src/compiler/Lowering.ts +15 -5
- package/src/compiler/Refusals.ts +57 -0
- package/src/compiler/TypeSession.ts +45 -86
- package/src/compiler/appLayout.ts +447 -0
- package/src/compiler/appMap.ts +21 -20
- package/src/compiler/checkApp.ts +61 -97
- package/src/compiler/checkedShadow.ts +11 -9
- package/src/compiler/classifyRead.ts +3 -6
- package/src/compiler/compileApp.ts +180 -512
- package/src/compiler/componentGraph.ts +2 -71
- package/src/compiler/declaredExports.ts +79 -60
- package/src/compiler/emitClient.ts +11 -13
- package/src/compiler/emitComponent.ts +21 -6
- package/src/compiler/emitSource.ts +20 -20
- package/src/compiler/handlersUnder.ts +46 -36
- package/src/compiler/hookCalls.ts +152 -0
- package/src/compiler/mountsOf.ts +31 -54
- package/src/compiler/readSources.ts +27 -42
- package/src/compiler/readsIn.ts +5 -16
- package/src/compiler/resolverIn.ts +77 -0
- package/src/server/abideModules.ts +92 -123
- package/src/server/answerRequest.ts +18 -4
- package/src/server/answerSocket.ts +136 -76
- package/src/server/answerValue.ts +28 -15
- package/src/server/appTree.ts +49 -71
- package/src/server/claudeApi.ts +2 -5
- package/src/server/clientBundle.ts +198 -0
- package/src/server/coerceArgs.ts +1 -1
- package/src/server/commands.ts +93 -57
- package/src/server/createApp.ts +90 -16
- package/src/server/deferredBuilds.ts +175 -0
- package/src/server/devWorker.ts +107 -319
- package/src/server/faceOf.ts +120 -0
- package/src/server/fenceServer.ts +10 -18
- package/src/server/index.ts +3 -0
- package/src/server/placement.ts +167 -0
- package/src/server/remote.ts +14 -42
- package/src/server/render.ts +25 -17
- package/src/server/renderComponent.ts +9 -5
- package/src/server/requestScope.ts +10 -4
- package/src/server/responseRelay.ts +389 -0
- package/src/server/runCompiled.ts +9 -12
- package/src/server/runDesktop.ts +6 -9
- package/src/server/serveBuild.ts +26 -73
- package/src/server/serveDev.ts +19 -176
- package/src/server/streamEvent.ts +29 -0
- package/src/server/withAbideSources.ts +2 -1
- package/src/shared/HandlerRoute.ts +68 -0
- package/src/shared/REFUSAL_KEY.ts +5 -0
- package/src/shared/answerOf.ts +105 -78
- package/src/shared/filledPath.ts +1 -9
- package/src/shared/framing.ts +127 -128
- package/src/shared/queryText.ts +15 -7
- package/src/shared/reactive/Cursor.ts +4 -2
- package/src/shared/reactive/Reactive.ts +285 -197
- package/src/shared/reactive/index.ts +11 -8
- package/src/shared/reactive/pairReactive.ts +1 -1
- package/src/shared/reactive/scoped.ts +4 -3
- package/src/shared/reactive/share.ts +11 -1
- package/src/shared/routePattern.ts +0 -123
- package/src/shared/rpc.ts +17 -24
- package/src/shared/rpcRequest.ts +18 -47
- package/src/shared/takeEarlyCall.ts +12 -37
- package/src/shared/url.ts +1 -1
- package/src/ui/Level.ts +276 -0
- package/src/ui/List.ts +17 -17
- package/src/ui/Router.ts +20 -87
- package/src/ui/applyHead.ts +92 -79
- package/src/ui/index.ts +4 -2
- package/src/ui/leave.ts +5 -0
- package/src/ui/reconcile.ts +23 -104
- package/src/ui/regions.ts +27 -2
- package/src/ui/rows.ts +123 -2
- package/src/ui/wire.ts +12 -27
- package/src/compiler/hooksCalled.ts +0 -35
- package/src/server/clientBuildOptions.ts +0 -54
- package/src/server/refuseAddressCollision.ts +0 -37
- package/src/shared/reactive/chunksOfStream.ts +0 -33
- package/src/ui/MOUNTING_LEVEL.ts +0 -18
package/src/server/claudeApi.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// Every block of an assistant turn goes back as it came — a `thinking` block with its signature
|
|
7
7
|
// included, which a turn continuing after a tool call is refused without.
|
|
8
8
|
|
|
9
|
-
import { framedStream
|
|
9
|
+
import { framedStream } from '#shared/framing.ts'
|
|
10
10
|
import { refuse } from '#shared/refuse.ts'
|
|
11
11
|
import type { AssistantEvent, Provider } from './assistant.ts'
|
|
12
12
|
|
|
@@ -112,10 +112,7 @@ export function claudeApi(config: ClaudeApiConfig): Provider {
|
|
|
112
112
|
const partials: string[] = []
|
|
113
113
|
const called: Block[] = []
|
|
114
114
|
let stop = 'end_turn'
|
|
115
|
-
for await (const chunk of framedStream(
|
|
116
|
-
answered.body,
|
|
117
|
-
framingPrefix('text/event-stream') as string,
|
|
118
|
-
)) {
|
|
115
|
+
for await (const chunk of framedStream(answered.body, 'sse')) {
|
|
119
116
|
const event = chunk as ApiEvent
|
|
120
117
|
switch (event.type) {
|
|
121
118
|
case 'message_start':
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// 38.5, 38.16 — THE CLIENT BUNDLE, for both lanes that build one: `abide build`, which writes it to
|
|
2
|
+
// disk for `abide start`, and `abide dev`'s worker, which holds it in memory. What decides what the
|
|
3
|
+
// bundle IS — its plugins and their order, whether there is a page to boot, how a stylesheet's
|
|
4
|
+
// addresses are written, the manifest — is here once, so dev resolves an address the way the
|
|
5
|
+
// built app does by construction; a lane picks only a `mode`. And both answer it at
|
|
6
|
+
// `/__abide/client/<name>` through `bundleServer`, differing in where a byte is read from and how
|
|
7
|
+
// long a browser may keep it.
|
|
8
|
+
|
|
9
|
+
import { CLIENT_ADDRESS } from '#shared/ADDRESSES.ts'
|
|
10
|
+
import { abideModules, type Compilation, tablePath } from './abideModules.ts'
|
|
11
|
+
import { appPlugins } from './appPlugins.ts'
|
|
12
|
+
import { CLIENT_ENTRY } from './CLIENT_BUILD.ts'
|
|
13
|
+
import { fenceServer } from './fenceServer.ts'
|
|
14
|
+
import { type Manifest, manifestOf, NO_MANIFEST } from './manifestOf.ts'
|
|
15
|
+
import { type Encoding, negotiateEncoding } from './negotiateEncoding.ts'
|
|
16
|
+
|
|
17
|
+
/** One file the bundle produced: its bytes — a stylesheet's already rewritten — and its kind. */
|
|
18
|
+
type BundledFile = {
|
|
19
|
+
bytes: Uint8Array<ArrayBuffer>
|
|
20
|
+
type: string
|
|
21
|
+
kind: Bun.BuildArtifact['kind']
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A client bundle: every file it produced, by its name below `/__abide/client`, and its manifest. */
|
|
25
|
+
export type ClientBundle = {
|
|
26
|
+
files: Map<string, BundledFile>
|
|
27
|
+
manifest: Manifest
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The client bundle of the app at `root`, out of `compilation`, built for `mode`.
|
|
32
|
+
*
|
|
33
|
+
* `published` is each public file's source path to its address. `'build'` is 38.5's — minified and
|
|
34
|
+
* every name hashed; `'dev'` is 38.16's — unminified, the entry named by its source, with linked
|
|
35
|
+
* maps. A bundle that fails throws, its logs the error's.
|
|
36
|
+
*
|
|
37
|
+
* AN APP OF HANDLERS ALONE has no page to boot, and is bundled as nothing with no manifest.
|
|
38
|
+
*/
|
|
39
|
+
export async function clientBundle(
|
|
40
|
+
root: string,
|
|
41
|
+
compilation: Compilation,
|
|
42
|
+
published: Map<string, string>,
|
|
43
|
+
mode: 'build' | 'dev',
|
|
44
|
+
): Promise<ClientBundle> {
|
|
45
|
+
const bundle: ClientBundle = { files: new Map(), manifest: NO_MANIFEST }
|
|
46
|
+
// A page among what compiled: the compiler writes the entry only where there is one to boot.
|
|
47
|
+
if (compilation.tables[`${CLIENT_ENTRY}.ts`] === undefined) return bundle
|
|
48
|
+
const built = await Bun.build({
|
|
49
|
+
// The entry is a table the plugin serves (`abideModules`), below the root.
|
|
50
|
+
entrypoints: [tablePath(root, `${CLIENT_ENTRY}.ts`)],
|
|
51
|
+
// The app's own root, because `Bun.build` opens the entry's directory otherwise, and the
|
|
52
|
+
// tables' is not on disk to open.
|
|
53
|
+
root,
|
|
54
|
+
// 16.28, 16.29 — the browser's view of the server: a `<script server>` is its stand-ins and a
|
|
55
|
+
// `server/` module refuses — FENCED FIRST, before the plugin that would serve the module. 38.43
|
|
56
|
+
// — then the app's own, as `bunfig.toml` declares them.
|
|
57
|
+
plugins: [
|
|
58
|
+
fenceServer(root, compilation.mounted),
|
|
59
|
+
abideModules(root, compilation),
|
|
60
|
+
...(await appPlugins(root)),
|
|
61
|
+
],
|
|
62
|
+
target: 'browser',
|
|
63
|
+
format: 'esm',
|
|
64
|
+
// So a destination's markup and bindings stay in the chunk its own route loads (30.11)
|
|
65
|
+
// rather than landing in the entry every page pays for.
|
|
66
|
+
splitting: true,
|
|
67
|
+
metafile: true,
|
|
68
|
+
// 36.15 — a stylesheet's `url('/fonts/…')` is the public file's address, not a path here,
|
|
69
|
+
// and `Bun.build` reads a leading `/` as a path on this disk and fails on it not being
|
|
70
|
+
// there. The list is the files that exist rather than every `/…`: a URL naming nothing the
|
|
71
|
+
// app serves is still worth a build error.
|
|
72
|
+
external: [...published.values()],
|
|
73
|
+
throw: false,
|
|
74
|
+
...(mode === 'build'
|
|
75
|
+
? {
|
|
76
|
+
// 38.5 — MINIFIED, and EVERY NAME HASHED, the entry's included: chunks and assets are
|
|
77
|
+
// already `[name]-[hash]` by default and the entry is not, which left the one file
|
|
78
|
+
// every page loads served for a year under a name the next deploy writes different
|
|
79
|
+
// bytes to. The manifest is how the document learns what the entry was called.
|
|
80
|
+
//
|
|
81
|
+
// NAMES KEPT, because a class's name is what an error reads to say what it was
|
|
82
|
+
// handed (`canonicalKey`), and a production error naming `a e` is one nobody can
|
|
83
|
+
// act on. It is free where it is paid: the JS the docs app's `/values` sends is
|
|
84
|
+
// 25 600 bytes brotli with the names and without, against 31 900 unminified.
|
|
85
|
+
minify: {
|
|
86
|
+
whitespace: true,
|
|
87
|
+
syntax: true,
|
|
88
|
+
identifiers: true,
|
|
89
|
+
keepNames: true,
|
|
90
|
+
},
|
|
91
|
+
naming: { entry: '[name]-[hash].[ext]' },
|
|
92
|
+
}
|
|
93
|
+
: {
|
|
94
|
+
// 38.97 — a debugger in the browser reads the `.abide` (`withAbideSources`).
|
|
95
|
+
sourcemap: 'linked' as const,
|
|
96
|
+
// 38.16 — the entry named by its source, so what a browser loads and what a stack
|
|
97
|
+
// trace names are the module the author wrote. A dev chunk that changed has to be
|
|
98
|
+
// fetched again, and a name that changes with it is a cache the reload cannot clear.
|
|
99
|
+
naming: {
|
|
100
|
+
entry: '[name].[ext]',
|
|
101
|
+
chunk: '[name]-[hash].[ext]',
|
|
102
|
+
asset: '[name]-[hash].[ext]',
|
|
103
|
+
},
|
|
104
|
+
}),
|
|
105
|
+
})
|
|
106
|
+
// THROWN RATHER THAN LOGGED: a build that emitted server modules and no bundle is an app that
|
|
107
|
+
// serves every page dead — the markup right and nothing wired — which is the one failure a
|
|
108
|
+
// reader cannot see. Dev catches it (38.17); the build stops on it.
|
|
109
|
+
if (!built.success)
|
|
110
|
+
throw new AggregateError(
|
|
111
|
+
built.logs,
|
|
112
|
+
`abide ${mode}: the client bundle failed`,
|
|
113
|
+
)
|
|
114
|
+
// The outputs are independent, so they are read at once rather than one after another.
|
|
115
|
+
const read = await Promise.all(
|
|
116
|
+
built.outputs.map((output) => output.arrayBuffer()),
|
|
117
|
+
)
|
|
118
|
+
for (let index = 0; index < built.outputs.length; index += 1) {
|
|
119
|
+
const output = built.outputs[index] as Bun.BuildArtifact
|
|
120
|
+
const name = output.path.slice(output.path.lastIndexOf('/') + 1)
|
|
121
|
+
let bytes = new Uint8Array(read[index] as ArrayBuffer)
|
|
122
|
+
if (name.endsWith('.css'))
|
|
123
|
+
bytes = new TextEncoder().encode(
|
|
124
|
+
stylesheetUnderMount(new TextDecoder().decode(bytes)),
|
|
125
|
+
)
|
|
126
|
+
bundle.files.set(name, { bytes, type: output.type, kind: output.kind })
|
|
127
|
+
}
|
|
128
|
+
bundle.manifest = manifestOf(built.metafile as Bun.BuildMetafile, root)
|
|
129
|
+
return bundle
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// From the bundle's own directory up to the app's root: `../../` below `/__abide/client`.
|
|
133
|
+
const UP_TO_ROOT = '../'.repeat(CLIENT_ADDRESS.split('/').length - 1)
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* A built stylesheet with every root-absolute `url()` written relative to the bundle's directory.
|
|
137
|
+
*
|
|
138
|
+
* 16.10, 36.15 — THE SAME MECHANISM AS THE HANDLER WRAPPERS: the address is written relative to
|
|
139
|
+
* where the bundle is served and the browser resolves it against the sheet, so one build serves
|
|
140
|
+
* at the root and below a mount. What survives the build as `url(/…)` is a public file's address
|
|
141
|
+
* (the `external` list above), and the browser would resolve it against the origin. `//` is
|
|
142
|
+
* another host's.
|
|
143
|
+
*/
|
|
144
|
+
function stylesheetUnderMount(css: string): string {
|
|
145
|
+
return css.replace(
|
|
146
|
+
/url\((["']?)\/(?!\/)/g,
|
|
147
|
+
(_whole, quote: string) => `url(${quote}${UP_TO_ROOT}`,
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** A bundle file as it is answered: its type, its bytes, and each precompressed copy of them. */
|
|
152
|
+
export type ServedFile = {
|
|
153
|
+
type: string
|
|
154
|
+
body: () => BodyInit
|
|
155
|
+
encoded: Partial<Record<Encoding, () => BodyInit>>
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Answers `/__abide/client/<name>` out of `files`, each with `cacheControl`; `undefined` for any
|
|
160
|
+
* other address, and for a name the bundle does not hold.
|
|
161
|
+
*
|
|
162
|
+
* THE BUNDLE, ENUMERATED RATHER THAN RESOLVED. A request names a file in this set or it names
|
|
163
|
+
* nothing — there is no path to normalize, so `..` is not a case that has to be got right, and what
|
|
164
|
+
* is servable is exactly what the bundle produced.
|
|
165
|
+
*/
|
|
166
|
+
export function bundleServer(
|
|
167
|
+
files: Map<string, ServedFile>,
|
|
168
|
+
cacheControl: string,
|
|
169
|
+
): (pathname: string, request: Request) => Response | undefined {
|
|
170
|
+
// Each file's encodings listed once, rather than on every request for it.
|
|
171
|
+
const offeredOf = new Map<string, Encoding[]>()
|
|
172
|
+
for (const [name, file] of files)
|
|
173
|
+
offeredOf.set(name, Object.keys(file.encoded) as Encoding[])
|
|
174
|
+
return (pathname, request) => {
|
|
175
|
+
if (!pathname.startsWith(`${CLIENT_ADDRESS}/`)) return undefined
|
|
176
|
+
const name = pathname.slice(CLIENT_ADDRESS.length + 1)
|
|
177
|
+
const file = files.get(name)
|
|
178
|
+
if (file === undefined) return undefined
|
|
179
|
+
const headers: Record<string, string> = {
|
|
180
|
+
'cache-control': cacheControl,
|
|
181
|
+
'content-type': file.type,
|
|
182
|
+
}
|
|
183
|
+
const offered = offeredOf.get(name) as Encoding[]
|
|
184
|
+
if (offered.length === 0) return new Response(file.body(), { headers })
|
|
185
|
+
headers.vary = 'accept-encoding'
|
|
186
|
+
// 38.29 — the precompressed copy where the request takes its encoding.
|
|
187
|
+
const encoding = negotiateEncoding(
|
|
188
|
+
request.headers.get('accept-encoding'),
|
|
189
|
+
offered,
|
|
190
|
+
)
|
|
191
|
+
const encoded =
|
|
192
|
+
encoding === undefined ? undefined : file.encoded[encoding]
|
|
193
|
+
if (encoding === undefined || encoded === undefined)
|
|
194
|
+
return new Response(file.body(), { headers })
|
|
195
|
+
headers['content-encoding'] = encoding
|
|
196
|
+
return new Response(encoded(), { headers })
|
|
197
|
+
}
|
|
198
|
+
}
|
package/src/server/coerceArgs.ts
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
// has declared no types, and its args stay the text they arrived as.
|
|
8
8
|
|
|
9
9
|
import { numberFromText } from '#shared/numberFromText.ts'
|
|
10
|
+
import { bareName } from '#shared/queryText.ts'
|
|
10
11
|
import { type Gated, gateOf } from '#shared/reactive/argsContract.ts'
|
|
11
|
-
import { bareName } from '#shared/rpcRequest.ts'
|
|
12
12
|
|
|
13
13
|
export type Document = {
|
|
14
14
|
type?: string | string[]
|
package/src/server/commands.ts
CHANGED
|
@@ -6,11 +6,9 @@
|
|
|
6
6
|
import { resolve } from 'node:path' // bun exposes no path helpers of its own
|
|
7
7
|
import { parseArgs } from 'node:util' // bun exposes no argv parser of its own yet
|
|
8
8
|
import { MCP_ADDRESS, OPENAPI_ADDRESS } from '#shared/ADDRESSES.ts'
|
|
9
|
-
import type { Mount } from './answerRpc.ts'
|
|
10
9
|
import { banner } from './banner.ts'
|
|
11
|
-
import {
|
|
12
|
-
import
|
|
13
|
-
import { installServer } from './installServer.ts'
|
|
10
|
+
import { type App, type AppHandle, enterApp } from './createApp.ts'
|
|
11
|
+
import { stopOnSignalOrFault } from './faultPolicy.ts'
|
|
14
12
|
import {
|
|
15
13
|
appUrl,
|
|
16
14
|
callHandler,
|
|
@@ -45,17 +43,10 @@ export type Host = {
|
|
|
45
43
|
/** What usage and every message speak as: `abide`, or the name the executable was run by. */
|
|
46
44
|
name: string
|
|
47
45
|
/**
|
|
48
|
-
* The
|
|
49
|
-
*
|
|
50
|
-
* knows the table of. Empty where there is none to read, and never built to be read.
|
|
46
|
+
* The app the invocation means, found once off its flags (38.25): every command that serves,
|
|
47
|
+
* boots, lists or announces it derives from this one handle.
|
|
51
48
|
*/
|
|
52
|
-
|
|
53
|
-
/** The identity a script `run` resolves its configuration with. 27.3. */
|
|
54
|
-
appPackage: () => AppPackage
|
|
55
|
-
/** The app booted in this process: `openapi` and `mcp`'s answer where no `--url` is given. */
|
|
56
|
-
boot: () => Promise<App>
|
|
57
|
-
/** The app served, or the exit code of a refusal to. 38.6. */
|
|
58
|
-
serve: (given: Given) => Promise<App | number>
|
|
49
|
+
app: (given: Given) => AppHandle
|
|
59
50
|
source: SourceCommands | undefined
|
|
60
51
|
}
|
|
61
52
|
|
|
@@ -79,7 +70,8 @@ export async function runCommands(
|
|
|
79
70
|
console.log(usage(host, commands))
|
|
80
71
|
return 0
|
|
81
72
|
}
|
|
82
|
-
if (command === undefined)
|
|
73
|
+
if (command === undefined)
|
|
74
|
+
return await shellOn(host, host.app(given), given)
|
|
83
75
|
for (const [name, , body] of commands)
|
|
84
76
|
if (name.split(' ')[0] === command) return await body(given)
|
|
85
77
|
console.error(
|
|
@@ -111,7 +103,7 @@ function commandsOf(host: Host): Row[] {
|
|
|
111
103
|
[
|
|
112
104
|
'run <file> [args…]',
|
|
113
105
|
'run a script under the abide runtime',
|
|
114
|
-
(given) => run(host, given),
|
|
106
|
+
(given) => run(host, host.app(given), given),
|
|
115
107
|
false,
|
|
116
108
|
],
|
|
117
109
|
fromSource('dev', 'serve with the compiler in the loop', 'dev'),
|
|
@@ -120,23 +112,28 @@ function commandsOf(host: Host): Row[] {
|
|
|
120
112
|
'compile every .abide under the app into .abide/',
|
|
121
113
|
'build',
|
|
122
114
|
),
|
|
123
|
-
[
|
|
115
|
+
[
|
|
116
|
+
'start',
|
|
117
|
+
'serve the app',
|
|
118
|
+
(given) => start(host, host.app(given), given),
|
|
119
|
+
false,
|
|
120
|
+
],
|
|
124
121
|
[
|
|
125
122
|
'call <address> [args]',
|
|
126
123
|
'one call to one handler',
|
|
127
|
-
(given) => call(host, given),
|
|
124
|
+
(given) => call(host, host.app(given), given),
|
|
128
125
|
false,
|
|
129
126
|
],
|
|
130
127
|
[
|
|
131
128
|
'openapi',
|
|
132
129
|
'the OpenAPI document, written rather than served',
|
|
133
|
-
(given) => openapi(host, given),
|
|
130
|
+
(given) => openapi(host, host.app(given), given),
|
|
134
131
|
false,
|
|
135
132
|
],
|
|
136
133
|
[
|
|
137
134
|
'mcp',
|
|
138
135
|
"the app's MCP server over stdio",
|
|
139
|
-
(given) => mcp(host, given),
|
|
136
|
+
(given) => mcp(host, host.app(given), given),
|
|
140
137
|
false,
|
|
141
138
|
],
|
|
142
139
|
[
|
|
@@ -195,13 +192,12 @@ function usage(host: Host, commands: Row[]): string {
|
|
|
195
192
|
return `${text}
|
|
196
193
|
--port the listen port the app is served on
|
|
197
194
|
--url the running app the shell, call, watch, openapi and mcp talk to
|
|
198
|
-
--out the file openapi writes
|
|
199
195
|
`
|
|
200
196
|
text += `
|
|
201
197
|
--root the app's source directory, where its pages sit —
|
|
202
198
|
src/ where you stand, or where you stand, whichever has one
|
|
203
|
-
--out where the build goes, beside the app by default; the file
|
|
204
|
-
|
|
199
|
+
--out where the build goes, beside the app by default; the file compile and
|
|
200
|
+
bundle write — named after the app, where you stand
|
|
205
201
|
--port the listen port for dev, start and the shell's app
|
|
206
202
|
--url the running app the shell, call, watch, openapi and mcp talk to
|
|
207
203
|
--target the platform compile builds for — linux-x64, darwin-arm64, windows-x64…
|
|
@@ -234,32 +230,63 @@ function parsed(argv: string[]) {
|
|
|
234
230
|
})
|
|
235
231
|
}
|
|
236
232
|
|
|
237
|
-
/**
|
|
233
|
+
/**
|
|
234
|
+
* The app served and listening, with the address it answers at and the banner it is announced by —
|
|
235
|
+
* or the exit code of a refusal to. 38.6.
|
|
236
|
+
*
|
|
237
|
+
* Stopped on a signal or an unawaited fault ONCE SERVING, and not before: a boot that fails is
|
|
238
|
+
* 37.6's to fail hard.
|
|
239
|
+
*/
|
|
238
240
|
async function served(
|
|
239
241
|
host: Host,
|
|
242
|
+
app: AppHandle,
|
|
240
243
|
given: Given,
|
|
241
244
|
): Promise<{ app: App; url: string; heading: string } | number> {
|
|
242
245
|
// 36.3 — `dev`, `build` and `start` each name the environment where the process was started with
|
|
243
246
|
// none, and leave one it was started with exactly as given.
|
|
244
247
|
process.env.NODE_ENV ??= 'production'
|
|
245
|
-
const
|
|
246
|
-
if (typeof
|
|
248
|
+
const booted = await booting(host, app, given)
|
|
249
|
+
if (typeof booted === 'number') return booted
|
|
250
|
+
const port = given.values.port
|
|
251
|
+
await booted.listen(port === undefined ? {} : { port: Number(port) })
|
|
252
|
+
stopOnSignalOrFault(booted)
|
|
247
253
|
const server = LISTENING.server as Bun.Server<unknown>
|
|
248
254
|
return {
|
|
249
|
-
app,
|
|
255
|
+
app: booted,
|
|
250
256
|
url: withoutTrailingSlash(server.url.href),
|
|
251
|
-
heading: banner(host.name, 'start',
|
|
257
|
+
heading: banner(host.name, 'start', app.package, server),
|
|
252
258
|
}
|
|
253
259
|
}
|
|
254
260
|
|
|
261
|
+
/** The app booted in this process, or the exit code of a refusal naming why there is none. */
|
|
262
|
+
async function booting(
|
|
263
|
+
host: Host,
|
|
264
|
+
app: AppHandle,
|
|
265
|
+
given: Given,
|
|
266
|
+
): Promise<App | number> {
|
|
267
|
+
const booted = await app.boot()
|
|
268
|
+
if (typeof booted === 'string') {
|
|
269
|
+
const command = given.positionals[0]
|
|
270
|
+
console.error(
|
|
271
|
+
`${command === undefined ? host.name : `${host.name} ${command}`}: ${booted}`,
|
|
272
|
+
)
|
|
273
|
+
return 1
|
|
274
|
+
}
|
|
275
|
+
return booted
|
|
276
|
+
}
|
|
277
|
+
|
|
255
278
|
/**
|
|
256
279
|
* `start`: serves the app.
|
|
257
280
|
*
|
|
258
281
|
* 38.67 — READS NO INPUT, and hands back no code for the process to exit on: the process is the
|
|
259
282
|
* server, for a container or a supervisor to stop.
|
|
260
283
|
*/
|
|
261
|
-
async function start(
|
|
262
|
-
|
|
284
|
+
async function start(
|
|
285
|
+
host: Host,
|
|
286
|
+
app: AppHandle,
|
|
287
|
+
given: Given,
|
|
288
|
+
): Promise<number | undefined> {
|
|
289
|
+
const serving = await served(host, app, given)
|
|
263
290
|
if (typeof serving === 'number') return serving
|
|
264
291
|
console.log(serving.heading)
|
|
265
292
|
return undefined
|
|
@@ -273,24 +300,28 @@ async function start(host: Host, given: Given): Promise<number | undefined> {
|
|
|
273
300
|
* as a remote one's is. 38.54 — `exit` stops the app. 38.55 — input that ends leaves it serving, and
|
|
274
301
|
* hands back no code for the process to exit on.
|
|
275
302
|
*/
|
|
276
|
-
async function shellOn(
|
|
303
|
+
async function shellOn(
|
|
304
|
+
host: Host,
|
|
305
|
+
app: AppHandle,
|
|
306
|
+
given: Given,
|
|
307
|
+
): Promise<number | undefined> {
|
|
277
308
|
const remote = remoteUrl(given.values.url)
|
|
278
309
|
if (remote !== undefined) {
|
|
279
310
|
const url = appUrl(remote)
|
|
280
311
|
await shell({
|
|
281
312
|
url,
|
|
282
|
-
handlers: await
|
|
313
|
+
handlers: await app.handlers(),
|
|
283
314
|
heading: `${host.name}: ${url}`,
|
|
284
315
|
local: false,
|
|
285
316
|
})
|
|
286
317
|
return 0
|
|
287
318
|
}
|
|
288
|
-
const serving = await served(host, given)
|
|
319
|
+
const serving = await served(host, app, given)
|
|
289
320
|
if (typeof serving === 'number') return serving
|
|
290
321
|
// After the serve, which built the app where nothing had.
|
|
291
322
|
const ended = await shell({
|
|
292
323
|
url: serving.url,
|
|
293
|
-
handlers: await
|
|
324
|
+
handlers: await app.handlers(),
|
|
294
325
|
heading: serving.heading,
|
|
295
326
|
local: true,
|
|
296
327
|
})
|
|
@@ -312,6 +343,7 @@ async function shellOn(host: Host, given: Given): Promise<number | undefined> {
|
|
|
312
343
|
*/
|
|
313
344
|
async function run(
|
|
314
345
|
host: Host,
|
|
346
|
+
app: AppHandle,
|
|
315
347
|
{ positionals: [, file, ...rest] }: Given,
|
|
316
348
|
): Promise<number | undefined> {
|
|
317
349
|
if (file === undefined) {
|
|
@@ -322,11 +354,9 @@ async function run(
|
|
|
322
354
|
}
|
|
323
355
|
const path = resolve(file)
|
|
324
356
|
process.argv = [process.argv[0] as string, path, ...rest]
|
|
325
|
-
// 38.57 —
|
|
326
|
-
//
|
|
327
|
-
|
|
328
|
-
APP_PACKAGE.current = host.appPackage()
|
|
329
|
-
config.invalidate()
|
|
357
|
+
// 38.57 — entered as `bootApp` enters it, its server blocks aside: a script is run against the
|
|
358
|
+
// app's source, which nothing built.
|
|
359
|
+
await enterApp({ hooks: undefined, package: app.package })
|
|
330
360
|
await inScope(undefined, () => import(Bun.pathToFileURL(path).href))
|
|
331
361
|
return undefined
|
|
332
362
|
}
|
|
@@ -350,7 +380,7 @@ async function watch(host: Host, given: Given): Promise<number> {
|
|
|
350
380
|
})
|
|
351
381
|
}
|
|
352
382
|
|
|
353
|
-
async function call(host: Host, given: Given): Promise<number> {
|
|
383
|
+
async function call(host: Host, app: AppHandle, given: Given): Promise<number> {
|
|
354
384
|
const {
|
|
355
385
|
positionals: [, address, ...args],
|
|
356
386
|
values: { url, method },
|
|
@@ -366,7 +396,7 @@ async function call(host: Host, given: Given): Promise<number> {
|
|
|
366
396
|
address,
|
|
367
397
|
args,
|
|
368
398
|
method,
|
|
369
|
-
await
|
|
399
|
+
await app.handlers(),
|
|
370
400
|
).catch((thrown: Error) => ({ code: 1, text: thrown.message }))
|
|
371
401
|
if (answered.text !== '')
|
|
372
402
|
(answered.code === 0 ? console.log : console.error)(answered.text)
|
|
@@ -374,50 +404,55 @@ async function call(host: Host, given: Given): Promise<number> {
|
|
|
374
404
|
}
|
|
375
405
|
|
|
376
406
|
/**
|
|
377
|
-
* A fetch against a generated surface, on the app `--url` names or on one booted here
|
|
407
|
+
* A fetch against a generated surface, on the app `--url` names or on one booted here — or the exit
|
|
408
|
+
* code of a refusal to boot one.
|
|
378
409
|
*
|
|
379
410
|
* 38.9 — the app booted in this process derives it from the same route table the served one does.
|
|
380
411
|
*/
|
|
381
412
|
async function surfaceAt(
|
|
382
413
|
host: Host,
|
|
383
|
-
|
|
414
|
+
app: AppHandle,
|
|
415
|
+
given: Given,
|
|
384
416
|
address: string,
|
|
385
417
|
): Promise<
|
|
386
|
-
(
|
|
387
|
-
|
|
388
|
-
|
|
418
|
+
| ((
|
|
419
|
+
init?: RequestInit & { headers?: Record<string, string> },
|
|
420
|
+
) => Promise<Response>)
|
|
421
|
+
| number
|
|
389
422
|
> {
|
|
390
|
-
const remote = remoteUrl(url)
|
|
423
|
+
const remote = remoteUrl(given.values.url)
|
|
391
424
|
if (remote !== undefined)
|
|
392
425
|
return (init) =>
|
|
393
426
|
fetch(`${appUrl(remote)}${address}`, {
|
|
394
427
|
...init,
|
|
395
428
|
headers: { ...init?.headers, ...remoteHeaders() },
|
|
396
429
|
})
|
|
397
|
-
const
|
|
430
|
+
const booted = await booting(host, app, given)
|
|
431
|
+
if (typeof booted === 'number') return booted
|
|
398
432
|
return (init) =>
|
|
399
|
-
|
|
433
|
+
booted.fetch(new Request(`http://abide.local${address}`, init))
|
|
400
434
|
}
|
|
401
435
|
|
|
402
436
|
/**
|
|
403
|
-
* `openapi`:
|
|
437
|
+
* `openapi`: prints the OpenAPI document — redirected to a file, where one is wanted.
|
|
404
438
|
*
|
|
405
439
|
* 38.9 — from `--url` where one is given and from the app in this process where not.
|
|
406
440
|
*/
|
|
407
441
|
async function openapi(
|
|
408
442
|
host: Host,
|
|
409
|
-
|
|
443
|
+
app: AppHandle,
|
|
444
|
+
given: Given,
|
|
410
445
|
): Promise<number> {
|
|
411
|
-
const
|
|
446
|
+
const ask = await surfaceAt(host, app, given, OPENAPI_ADDRESS)
|
|
447
|
+
if (typeof ask === 'number') return ask
|
|
448
|
+
const answer = await ask()
|
|
412
449
|
if (!answer.ok) {
|
|
413
450
|
console.error(
|
|
414
451
|
`${host.name} openapi: the app answered ${answer.status}. The document lists every handler with openapi in its clients; none is.`,
|
|
415
452
|
)
|
|
416
453
|
return 1
|
|
417
454
|
}
|
|
418
|
-
|
|
419
|
-
if (out === undefined) console.log(text)
|
|
420
|
-
else await Bun.write(resolve(out), `${text}\n`)
|
|
455
|
+
console.log(JSON.stringify(await answer.json(), null, 4))
|
|
421
456
|
return 0
|
|
422
457
|
}
|
|
423
458
|
|
|
@@ -427,8 +462,9 @@ async function openapi(
|
|
|
427
462
|
* 38.9, 38.13 — a bridge a model client spawns and kills: each line in is a JSON-RPC message for
|
|
428
463
|
* `/__abide/mcp`, and each answer is a line out.
|
|
429
464
|
*/
|
|
430
|
-
async function mcp(host: Host,
|
|
431
|
-
const ask = await surfaceAt(host,
|
|
465
|
+
async function mcp(host: Host, app: AppHandle, given: Given): Promise<number> {
|
|
466
|
+
const ask = await surfaceAt(host, app, given, MCP_ADDRESS)
|
|
467
|
+
if (typeof ask === 'number') return ask
|
|
432
468
|
for await (const line of console) {
|
|
433
469
|
if (line.trim() === '') continue
|
|
434
470
|
const answer = await ask({
|