codebay 0.0.1 → 0.2.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/.mochi/import-css/index-0nwv3654.css +18 -0
- package/.mochi/import-css/index-xndx4980.css +54 -0
- package/.mochi/manifest.json +1612 -0
- package/.mochi/server-island.js +1 -0
- package/.mochi/svelte-client/HydratableIsland-wrq79y0w.js +1 -0
- package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-7xgbmxxg.js +2 -0
- package/.mochi/svelte-client/_hydrate-AvatarsView_257s9yq1wwr4f-3cnbjpz2.js +13 -0
- package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-2ntmdm3e.js +1 -0
- package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-t08spym1.js +23 -0
- package/.mochi/svelte-client/chunk-04qv4w6w.js +2 -0
- package/.mochi/svelte-client/chunk-5jnepvzx.js +2 -0
- package/.mochi/svelte-client/chunk-hcztrd75.js +1 -0
- package/.mochi/svelte-client/chunk-qxwt25hn.js +4 -0
- package/.mochi/svelte-client/chunk-sjmcgh7f.js +2 -0
- package/.mochi/svelte-client/chunk-tr67wtwn.js +2 -0
- package/.mochi/svelte-compile/App-f0q5n4rk.server.js +1168 -0
- package/.mochi/svelte-compile/Avatars-p8gfef7x.server.js +332 -0
- package/.mochi/svelte-compile/ClientStats-z3tn9ad6.server.js +99 -0
- package/.mochi/svelte-compile/DefaultError-wh77vebf.server.js +52 -0
- package/.mochi/svelte-compile/Instance-atjwsczx.server.js +356 -0
- package/.mochi/svelte-compile/Settings-q88xw6h8.server.js +1173 -0
- package/.mochi/svelte-compile/chunk-4kwy539a.js +14 -0
- package/.mochi/svelte-compile/chunk-9bjk7yx6.js +81 -0
- package/.mochi/svelte-compile/chunk-btmcxhqy.js +58 -0
- package/.mochi/svelte-compile/chunk-sxqys3nw.js +1668 -0
- package/.mochi/svelte-compile/chunk-v6796ga4.js +3295 -0
- package/.mochi/svelte-compile/chunk-we0ggtws.js +34 -0
- package/.mochi/svelte-css/AppBar-3bxinv4pabv8l.css +1 -0
- package/.mochi/svelte-css/AppShell-36mpg5smnbmuw.css +1 -0
- package/.mochi/svelte-css/Avatar-24th63awm9mna.css +1 -0
- package/.mochi/svelte-css/AvatarEditor-1ryrs1md5g36y.css +1 -0
- package/.mochi/svelte-css/AvatarsView-3hq37u4dgtljn.css +1 -0
- package/.mochi/svelte-css/BackLink-2wlu0eovz9j86.css +1 -0
- package/.mochi/svelte-css/BranchBox-1yqu1q2mgnjwz.css +1 -0
- package/.mochi/svelte-css/Brand-1la7zw5uhcbl4.css +1 -0
- package/.mochi/svelte-css/Button-2vtbrzb5kgu25.css +1 -0
- package/.mochi/svelte-css/CheckmarkIcon-3qlazj2oc50jc.css +1 -0
- package/.mochi/svelte-css/ClientStats-19qzckq40p2q9.css +1 -0
- package/.mochi/svelte-css/CoinButton-1rej3sh3tkjud.css +1 -0
- package/.mochi/svelte-css/CredMenu-9hwgeptp7crb.css +1 -0
- package/.mochi/svelte-css/DashboardView-1cy4xpqhja9sx.css +1 -0
- package/.mochi/svelte-css/DefaultError-2b0daamydxgb2.css +1 -0
- package/.mochi/svelte-css/ErrorIcon-2x0tzsqxskw28.css +1 -0
- package/.mochi/svelte-css/FolderBrowser-1x428zb3f2kti.css +1 -0
- package/.mochi/svelte-css/HealthBox-er2her0jukwl.css +1 -0
- package/.mochi/svelte-css/IdeBar-2qoqi2ya9zdn3.css +1 -0
- package/.mochi/svelte-css/IdeLoader-26rlko8e00j3g.css +1 -0
- package/.mochi/svelte-css/InstanceCard-16f77aysm7lgp.css +1 -0
- package/.mochi/svelte-css/InstanceView-wq85bxtusont.css +1 -0
- package/.mochi/svelte-css/LoaderIcon-soq5wt3p05ye.css +1 -0
- package/.mochi/svelte-css/PortsBox-1t25b5yg7jva3.css +1 -0
- package/.mochi/svelte-css/SettingsCog-jftyadvo64qo.css +1 -0
- package/.mochi/svelte-css/SettingsView-1nfdwnm16cpp2.css +1 -0
- package/.mochi/svelte-css/Skeleton-8k69usiju43h.css +1 -0
- package/.mochi/svelte-css/StatusBadge-3mq0j7z6aq4j.css +1 -0
- package/.mochi/svelte-css/ThemePicker-1ir8jyocm8mvi.css +1 -0
- package/.mochi/svelte-css/ToastBar-3havgkmtofntq.css +1 -0
- package/.mochi/svelte-css/ToastIcon-2z5wrkc95khg0.css +1 -0
- package/.mochi/svelte-css/ToastMessage-2jfwb2og2lfa8.css +1 -0
- package/.mochi/svelte-css/ToastWrapper-1fbnym10rm2nv.css +1 -0
- package/.mochi/svelte-css/Toaster-kk60j193854n.css +1 -0
- package/.mochi/svelte-css/TopBar-2q87li6yt8ids.css +1 -0
- package/LICENSE +21 -0
- package/README.md +34 -3
- package/bin/codebay.ts +22 -0
- package/migrations/0001_init.sql +20 -0
- package/migrations/0002_port_forwards.sql +9 -0
- package/migrations/0003_options.sql +4 -0
- package/migrations/0004_instance_image.sql +1 -0
- package/package.json +87 -9
- package/public/sounds/done.wav +0 -0
- package/public/sounds/waiting.wav +0 -0
- package/src/__fixtures__/HelloWorld.svelte +4 -0
- package/src/additional-svelte-typings.d.ts +1 -0
- package/src/api.ts +61 -0
- package/src/avatars/anchor.ts +17 -0
- package/src/avatars/bear.ts +17 -0
- package/src/avatars/bee.ts +17 -0
- package/src/avatars/bob-marley.ts +17 -0
- package/src/avatars/car.ts +17 -0
- package/src/avatars/carpathians.ts +17 -0
- package/src/avatars/cat-body.ts +17 -0
- package/src/avatars/cat-sits.ts +17 -0
- package/src/avatars/cat.ts +17 -0
- package/src/avatars/celtic-cross.ts +17 -0
- package/src/avatars/cherry.ts +17 -0
- package/src/avatars/cotton.ts +17 -0
- package/src/avatars/crab.ts +17 -0
- package/src/avatars/crown.ts +17 -0
- package/src/avatars/diamond.ts +17 -0
- package/src/avatars/dog.ts +17 -0
- package/src/avatars/elephant.ts +17 -0
- package/src/avatars/fish.ts +17 -0
- package/src/avatars/fleur-de-lis.ts +17 -0
- package/src/avatars/flower.ts +17 -0
- package/src/avatars/fox.ts +17 -0
- package/src/avatars/frog.ts +17 -0
- package/src/avatars/ghost.ts +17 -0
- package/src/avatars/giraffe.ts +17 -0
- package/src/avatars/glasses.ts +17 -0
- package/src/avatars/heart.ts +17 -0
- package/src/avatars/index.ts +128 -0
- package/src/avatars/invader.ts +17 -0
- package/src/avatars/kangaroo.ts +17 -0
- package/src/avatars/key.ts +17 -0
- package/src/avatars/lightning.ts +17 -0
- package/src/avatars/mushroom.ts +17 -0
- package/src/avatars/name-prompts.ts +30 -0
- package/src/avatars/octopus.ts +17 -0
- package/src/avatars/owl.ts +17 -0
- package/src/avatars/penguin.ts +17 -0
- package/src/avatars/planet.ts +17 -0
- package/src/avatars/rabbit.ts +17 -0
- package/src/avatars/rat.ts +17 -0
- package/src/avatars/robot.ts +17 -0
- package/src/avatars/rocket.ts +17 -0
- package/src/avatars/sea.ts +17 -0
- package/src/avatars/serialize.ts +65 -0
- package/src/avatars/skull.ts +17 -0
- package/src/avatars/snail.ts +17 -0
- package/src/avatars/snake.ts +17 -0
- package/src/avatars/snakegame.ts +17 -0
- package/src/avatars/star.ts +17 -0
- package/src/avatars/svelte.ts +17 -0
- package/src/avatars/target.ts +17 -0
- package/src/avatars/tree.ts +17 -0
- package/src/avatars/types.ts +24 -0
- package/src/avatars/ukraine.ts +17 -0
- package/src/avatars/whale.ts +17 -0
- package/src/avatars/wheat.ts +17 -0
- package/src/avatars/yinyan.ts +17 -0
- package/src/components/AppBar.svelte +35 -0
- package/src/components/AppShell.svelte +302 -0
- package/src/components/Avatar.svelte +235 -0
- package/src/components/AvatarEditor.svelte +297 -0
- package/src/components/AvatarsView.svelte +115 -0
- package/src/components/BranchBox.svelte +32 -0
- package/src/components/Brand.svelte +41 -0
- package/src/components/Button.svelte +161 -0
- package/src/components/CoinButton.svelte +121 -0
- package/src/components/CredMenu.svelte +171 -0
- package/src/components/DashboardView.svelte +198 -0
- package/src/components/FolderBrowser.svelte +635 -0
- package/src/components/HealthBox.svelte +161 -0
- package/src/components/IdeBar.svelte +186 -0
- package/src/components/IdeLoader.svelte +135 -0
- package/src/components/InstanceCard.svelte +220 -0
- package/src/components/InstanceView.svelte +567 -0
- package/src/components/PortsBox.svelte +74 -0
- package/src/components/SettingsCog.svelte +22 -0
- package/src/components/SettingsView.svelte +1494 -0
- package/src/components/Skeleton.svelte +43 -0
- package/src/components/StatusBadge.svelte +54 -0
- package/src/components/ThemePicker.svelte +77 -0
- package/src/components/TopBar.svelte +64 -0
- package/src/components/UiShowcase.svelte +578 -0
- package/src/components/ui-showcase/ComponentDemo.svelte +85 -0
- package/src/container-injections/attention-hooks.ts +117 -0
- package/src/container-injections/claude-aliases.ts +43 -0
- package/src/container-injections/claude-code-credentials.ts +145 -0
- package/src/container-injections/claude-code-custom.ts +125 -0
- package/src/container-injections/claude-no-coauthor.ts +35 -0
- package/src/container-injections/claude-skip-permissions.ts +38 -0
- package/src/container-injections/claude-statusline.ts +109 -0
- package/src/container-injections/git-identity.ts +63 -0
- package/src/container-injections/git-safe-directory.ts +18 -0
- package/src/container-injections/github-credentials.ts +162 -0
- package/src/container-injections/host-env-vars.ts +108 -0
- package/src/container-injections/tmux.ts +67 -0
- package/src/fontsource.d.ts +3 -0
- package/src/index.ts +81 -0
- package/src/lib/auth.server.ts +95 -0
- package/src/lib/bridge.server.ts +34 -0
- package/src/lib/config.server.ts +140 -0
- package/src/lib/crypto.server.ts +12 -0
- package/src/lib/db.server.ts +196 -0
- package/src/lib/devcontainer.server.ts +511 -0
- package/src/lib/docker-client.server.ts +61 -0
- package/src/lib/docker.server.ts +139 -0
- package/src/lib/exec.server.ts +92 -0
- package/src/lib/git.server.ts +68 -0
- package/src/lib/health.server.ts +105 -0
- package/src/lib/injections.server.ts +72 -0
- package/src/lib/instances.server.ts +552 -0
- package/src/lib/links.ts +9 -0
- package/src/lib/picker.server.ts +46 -0
- package/src/lib/ports.server.ts +9 -0
- package/src/lib/proxy.server.ts +170 -0
- package/src/lib/repo-url.ts +60 -0
- package/src/lib/spawn.server.ts +14 -0
- package/src/lib/theme.server.ts +22 -0
- package/src/live.ts +50 -0
- package/src/pages/App.svelte +14 -0
- package/src/pages/Avatars.svelte +9 -0
- package/src/pages/Instance.svelte +9 -0
- package/src/pages/Settings.svelte +76 -0
- package/src/pages/UI.svelte +10 -0
- package/src/routes.ts +444 -0
- package/src/settings.ts +14 -0
- package/src/shell.html +100 -0
- package/src/sound.ts +69 -0
- package/src/theme.ts +24 -0
- package/src/types.ts +79 -0
- package/svelte.config.js +5 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { Mochi, type MochiApiEvent, type MochiRouteValue, type MochiWsData } from 'mochi-framework';
|
|
2
|
+
import type { ServerWebSocket } from 'bun';
|
|
3
|
+
import { getInstance } from './db.server.ts';
|
|
4
|
+
|
|
5
|
+
export const PROXY_PREFIX = '/p';
|
|
6
|
+
|
|
7
|
+
/** Internal sentinel, never hit directly: proxied upgrades tag themselves with it so
|
|
8
|
+
* Mochi's shared websocket dispatcher routes them to the relay below. */
|
|
9
|
+
const PROXY_WS_PATTERN = '/__codebay_proxy_ws';
|
|
10
|
+
|
|
11
|
+
export function proxyPathFor(id: string): string {
|
|
12
|
+
return `${PROXY_PREFIX}/${id}/`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Null unless the instance is running, since a stopped container publishes nothing. */
|
|
16
|
+
function upstreamPort(id: string): number | null {
|
|
17
|
+
const row = getInstance(id);
|
|
18
|
+
if (!row || row.status !== 'running') return null;
|
|
19
|
+
return row.host_port;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Bun's wildcard routes expose `:id` but not the `*` capture, so strip the prefix by hand. */
|
|
23
|
+
function restOf(pathname: string, id: string): string {
|
|
24
|
+
return pathname.slice(`${PROXY_PREFIX}/${id}`.length) || '/';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type Frame = string | Buffer;
|
|
28
|
+
|
|
29
|
+
/** The cast gets past the DOM `send` overload's strict typing. */
|
|
30
|
+
function sendFrame(socket: { send: (data: never) => unknown }, frame: Frame): void {
|
|
31
|
+
socket.send(frame as never);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Only 1000 and 3000–4999 may be passed to `.close()`; anything else throws. */
|
|
35
|
+
function safeClose(
|
|
36
|
+
ws: { close: (code?: number, reason?: string) => void },
|
|
37
|
+
code: number,
|
|
38
|
+
reason: string
|
|
39
|
+
): void {
|
|
40
|
+
try {
|
|
41
|
+
if (code === 1000 || (code >= 3000 && code <= 4999)) ws.close(code, reason);
|
|
42
|
+
else ws.close();
|
|
43
|
+
} catch {
|
|
44
|
+
/* already closed */
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface RelayState {
|
|
49
|
+
upstreamWsUrl: string;
|
|
50
|
+
client?: WebSocket;
|
|
51
|
+
/** Frames received from the browser before the upstream socket is open. */
|
|
52
|
+
pending: Frame[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function proxyHttp(event: MochiApiEvent, port: number, rest: string): Promise<Response> {
|
|
56
|
+
const headers = new Headers(event.request.headers);
|
|
57
|
+
headers.set('host', `127.0.0.1:${port}`);
|
|
58
|
+
headers.delete('accept-encoding'); // ask for an unencoded body so we can stream it verbatim
|
|
59
|
+
// code-server runs with `--auth none`, so forwarding this would only leak the app password.
|
|
60
|
+
// `cookie` is deliberately kept: the manager sets none, so every cookie here is code-server's.
|
|
61
|
+
headers.delete('authorization');
|
|
62
|
+
const hasBody = event.method !== 'GET' && event.method !== 'HEAD';
|
|
63
|
+
let upstream: Response;
|
|
64
|
+
try {
|
|
65
|
+
upstream = await fetch(`http://127.0.0.1:${port}${rest}${event.url.search}`, {
|
|
66
|
+
method: event.method,
|
|
67
|
+
headers,
|
|
68
|
+
body: hasBody ? event.request.body : undefined,
|
|
69
|
+
redirect: 'manual',
|
|
70
|
+
// @ts-expect-error Bun streams request bodies with duplex: 'half'.
|
|
71
|
+
duplex: 'half'
|
|
72
|
+
});
|
|
73
|
+
} catch (err) {
|
|
74
|
+
// Usually just code-server not having bound its port yet; letting it escape would
|
|
75
|
+
// render a 500 stack trace inside the IDE iframe.
|
|
76
|
+
console.warn(`[proxy] upstream 127.0.0.1:${port}${rest} unreachable:`, (err as Error).message);
|
|
77
|
+
return new Response('code-server is not accepting connections yet', {
|
|
78
|
+
status: 503,
|
|
79
|
+
headers: { 'retry-after': '1', 'cache-control': 'no-store' }
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const resHeaders = new Headers(upstream.headers);
|
|
84
|
+
resHeaders.delete('content-encoding'); // body is already decoded by fetch
|
|
85
|
+
resHeaders.delete('transfer-encoding'); // Bun re-frames the streamed body
|
|
86
|
+
resHeaders.delete('content-length'); // may not match after re-framing; let Bun set it
|
|
87
|
+
return new Response(upstream.body, {
|
|
88
|
+
status: upstream.status,
|
|
89
|
+
statusText: upstream.statusText,
|
|
90
|
+
headers: resHeaders
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function proxyUpgrade(event: MochiApiEvent, port: number, rest: string): Response {
|
|
95
|
+
const data: MochiWsData<RelayState> = {
|
|
96
|
+
__mochiRoutePattern: PROXY_WS_PATTERN,
|
|
97
|
+
__mochiOpenedAt: performance.now(),
|
|
98
|
+
__mochiPath: event.url.pathname,
|
|
99
|
+
user: { upstreamWsUrl: `ws://127.0.0.1:${port}${rest}${event.url.search}`, pending: [] }
|
|
100
|
+
};
|
|
101
|
+
// Bun's Server is typed with a fixed WebSocketData; Mochi casts the same way for its own upgrades.
|
|
102
|
+
const ok = (
|
|
103
|
+
event.server as unknown as {
|
|
104
|
+
upgrade: (req: Request, opts: { data: MochiWsData<RelayState> }) => boolean;
|
|
105
|
+
}
|
|
106
|
+
).upgrade(event.request, { data });
|
|
107
|
+
if (!ok) return new Response('WebSocket upgrade failed', { status: 426 });
|
|
108
|
+
// Bun already sent the real 101; this is ignored, but Mochi's resolve chain reads a status.
|
|
109
|
+
return new Response(null, { status: 101 });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Ordinary routes, so the global Basic Auth handle wraps them — WS upgrades included. */
|
|
113
|
+
export const proxyRoutes: Record<string, MochiRouteValue> = {
|
|
114
|
+
// code-server emits relative URLs, so the mount has to end in a slash.
|
|
115
|
+
[`${PROXY_PREFIX}/:id`]: Mochi.api(({ url }) => {
|
|
116
|
+
return new Response(null, {
|
|
117
|
+
status: 308,
|
|
118
|
+
headers: { Location: `${url.pathname}/${url.search}` }
|
|
119
|
+
});
|
|
120
|
+
}),
|
|
121
|
+
|
|
122
|
+
[`${PROXY_PREFIX}/:id/*`]: Mochi.api(async (event) => {
|
|
123
|
+
const port = upstreamPort(event.params.id!);
|
|
124
|
+
if (port === null) return new Response('Instance not running', { status: 502 });
|
|
125
|
+
|
|
126
|
+
const rest = restOf(event.url.pathname, event.params.id!);
|
|
127
|
+
|
|
128
|
+
// An upgrade is a GET carrying `Upgrade: websocket`, so Bun routes it here too.
|
|
129
|
+
if (event.request.headers.get('upgrade')?.toLowerCase() === 'websocket') {
|
|
130
|
+
return proxyUpgrade(event, port, rest);
|
|
131
|
+
}
|
|
132
|
+
return proxyHttp(event, port, rest);
|
|
133
|
+
}),
|
|
134
|
+
|
|
135
|
+
// `upgrade: () => false` rejects anyone who navigates to the sentinel directly.
|
|
136
|
+
[PROXY_WS_PATTERN]: Mochi.ws<RelayState>({
|
|
137
|
+
upgrade: () => false,
|
|
138
|
+
open(ws: ServerWebSocket<MochiWsData<RelayState>>) {
|
|
139
|
+
const state = ws.data.user;
|
|
140
|
+
const client = new WebSocket(state.upstreamWsUrl);
|
|
141
|
+
client.binaryType = 'arraybuffer';
|
|
142
|
+
state.client = client;
|
|
143
|
+
|
|
144
|
+
client.onopen = () => {
|
|
145
|
+
for (const frame of state.pending) sendFrame(client, frame);
|
|
146
|
+
state.pending = [];
|
|
147
|
+
};
|
|
148
|
+
client.onmessage = (e) => {
|
|
149
|
+
try {
|
|
150
|
+
ws.send(e.data);
|
|
151
|
+
} catch {
|
|
152
|
+
/* browser socket closed */
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
client.onclose = (e) => safeClose(ws, e.code, e.reason);
|
|
156
|
+
client.onerror = () => safeClose(ws, 1011, 'upstream error');
|
|
157
|
+
},
|
|
158
|
+
message(ws: ServerWebSocket<MochiWsData<RelayState>>, message) {
|
|
159
|
+
const state = ws.data.user;
|
|
160
|
+
if (state.client && state.client.readyState === WebSocket.OPEN)
|
|
161
|
+
sendFrame(state.client, message);
|
|
162
|
+
else state.pending.push(message);
|
|
163
|
+
},
|
|
164
|
+
close(ws: ServerWebSocket<MochiWsData<RelayState>>) {
|
|
165
|
+
// `client` is always set in open() before any close is relayed.
|
|
166
|
+
const client = ws.data.user.client;
|
|
167
|
+
if (client) safeClose(client, 1000, '');
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Pure string logic with no server imports, because the client bundle uses it too.
|
|
2
|
+
|
|
3
|
+
export interface ParsedRepo {
|
|
4
|
+
host: string;
|
|
5
|
+
owner: string;
|
|
6
|
+
/** `.git` suffix stripped. */
|
|
7
|
+
repo: string;
|
|
8
|
+
/** Normalized to `https://<host>/<owner>/<repo>.git` regardless of the input form. */
|
|
9
|
+
cloneUrl: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const SEGMENT = /^[A-Za-z0-9._-]+$/;
|
|
13
|
+
|
|
14
|
+
/** Null for anything unrecognized, which is how a local folder path is told apart from a URL. */
|
|
15
|
+
export function parseRepoUrl(input: string): ParsedRepo | null {
|
|
16
|
+
const raw = input.trim();
|
|
17
|
+
if (!raw) return null;
|
|
18
|
+
|
|
19
|
+
let host: string | undefined;
|
|
20
|
+
let path: string | undefined;
|
|
21
|
+
|
|
22
|
+
const scp = raw.match(/^[A-Za-z0-9._-]+@([A-Za-z0-9.-]+):(.+)$/);
|
|
23
|
+
if (scp) {
|
|
24
|
+
host = scp[1];
|
|
25
|
+
path = scp[2];
|
|
26
|
+
} else if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) {
|
|
27
|
+
let url: URL;
|
|
28
|
+
try {
|
|
29
|
+
url = new URL(raw);
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:' && url.protocol !== 'ssh:') {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
host = url.hostname;
|
|
37
|
+
path = url.pathname;
|
|
38
|
+
} else {
|
|
39
|
+
// Schemeless: require a dotted host as the first segment so bare/relative paths don't match.
|
|
40
|
+
const m = raw.match(/^([A-Za-z0-9-]+\.[A-Za-z0-9.-]+)\/(.+)$/);
|
|
41
|
+
if (!m) return null;
|
|
42
|
+
host = m[1];
|
|
43
|
+
path = m[2];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!host || !path) return null;
|
|
47
|
+
|
|
48
|
+
const segments = path
|
|
49
|
+
.replace(/\.git$/i, '')
|
|
50
|
+
.split('/')
|
|
51
|
+
.filter(Boolean);
|
|
52
|
+
const [owner, repo] = segments;
|
|
53
|
+
if (!owner || !repo || !SEGMENT.test(owner) || !SEGMENT.test(repo)) return null;
|
|
54
|
+
|
|
55
|
+
return { host, owner, repo, cloneUrl: `https://${host}/${owner}/${repo}.git` };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function isRepoUrl(input: string): boolean {
|
|
59
|
+
return parseRepoUrl(input) !== null;
|
|
60
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Null on anything short of a clean run with output, so callers need no error branch. */
|
|
2
|
+
export async function spawnCapture(
|
|
3
|
+
cmd: string[],
|
|
4
|
+
opts?: { env?: Record<string, string | undefined> }
|
|
5
|
+
): Promise<string | null> {
|
|
6
|
+
try {
|
|
7
|
+
const proc = Bun.spawn(cmd, { stdout: 'pipe', stderr: 'ignore', env: opts?.env });
|
|
8
|
+
const [out, code] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
|
9
|
+
const value = out.trim();
|
|
10
|
+
return code === 0 && value ? value : null;
|
|
11
|
+
} catch {
|
|
12
|
+
return null; // binary not installed on the host
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { getRequestContext, type Handle } from 'mochi-framework';
|
|
2
|
+
|
|
3
|
+
/** Anything but an explicit choice is left untouched, so the CSS falls through to `prefers-color-scheme`. */
|
|
4
|
+
export function injectThemeAttribute(html: string, cookieValue: string | undefined): string {
|
|
5
|
+
if (cookieValue !== 'dark' && cookieValue !== 'light') return html;
|
|
6
|
+
return html.replace('<html', `<html data-theme="${cookieValue}"`);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Rewrites the tag server-side so the first paint has no flash of the wrong theme. */
|
|
10
|
+
export const themeHandle: Handle = ({ event, resolve }) =>
|
|
11
|
+
resolve(event, {
|
|
12
|
+
transformPage: ({ html }) => {
|
|
13
|
+
// Mochi's fetch-fallback path also lands here, and it runs outside requestContext.
|
|
14
|
+
let cookieValue: string | undefined;
|
|
15
|
+
try {
|
|
16
|
+
cookieValue = getRequestContext().cookies.get('theme');
|
|
17
|
+
} catch {
|
|
18
|
+
return html;
|
|
19
|
+
}
|
|
20
|
+
return injectThemeAttribute(html, cookieValue);
|
|
21
|
+
}
|
|
22
|
+
});
|
package/src/live.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { StreamEvent } from './lib/instances.server.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WebSockets don't auto-reconnect the way EventSource does, hence the backoff loop.
|
|
5
|
+
* Browser-only — call it from inside an `$effect`.
|
|
6
|
+
*/
|
|
7
|
+
export function liveSocket(
|
|
8
|
+
path: string,
|
|
9
|
+
onMessage: (data: string) => void,
|
|
10
|
+
onOpen?: () => void
|
|
11
|
+
): () => void {
|
|
12
|
+
let ws: WebSocket | null = null;
|
|
13
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
14
|
+
let closed = false;
|
|
15
|
+
|
|
16
|
+
const connect = () => {
|
|
17
|
+
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
18
|
+
ws = new WebSocket(`${proto}//${location.host}${path}`);
|
|
19
|
+
ws.onopen = () => onOpen?.();
|
|
20
|
+
ws.onmessage = (e) => onMessage(e.data as string);
|
|
21
|
+
ws.onclose = () => {
|
|
22
|
+
if (!closed) timer = setTimeout(connect, 1000);
|
|
23
|
+
};
|
|
24
|
+
ws.onerror = () => ws?.close();
|
|
25
|
+
};
|
|
26
|
+
connect();
|
|
27
|
+
|
|
28
|
+
return () => {
|
|
29
|
+
closed = true;
|
|
30
|
+
if (timer) clearTimeout(timer);
|
|
31
|
+
ws?.close();
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Malformed frames are dropped silently rather than surfaced to the caller. */
|
|
36
|
+
export function liveStream(onEvent: (event: StreamEvent) => void, onOpen?: () => void): () => void {
|
|
37
|
+
return liveSocket(
|
|
38
|
+
'/api/stream',
|
|
39
|
+
(raw) => {
|
|
40
|
+
let msg: StreamEvent;
|
|
41
|
+
try {
|
|
42
|
+
msg = JSON.parse(raw) as StreamEvent;
|
|
43
|
+
} catch {
|
|
44
|
+
return; // ignore malformed frame
|
|
45
|
+
}
|
|
46
|
+
onEvent(msg);
|
|
47
|
+
},
|
|
48
|
+
onOpen
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import '@fontsource-variable/doto';
|
|
3
|
+
import '@fontsource-variable/jetbrains-mono';
|
|
4
|
+
import AppShell from '../components/AppShell.svelte';
|
|
5
|
+
import type { Instance, Preflight } from '../types.ts';
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
preflight,
|
|
9
|
+
initialPath,
|
|
10
|
+
snapshot
|
|
11
|
+
}: { preflight: Preflight; initialPath: string; snapshot: Instance[] } = $props();
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<AppShell {preflight} {initialPath} {snapshot} mochi:hydrate />
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import '@fontsource-variable/doto';
|
|
3
|
+
import '@fontsource-variable/jetbrains-mono';
|
|
4
|
+
import AvatarsView from '../components/AvatarsView.svelte';
|
|
5
|
+
|
|
6
|
+
let { namePlaceholder }: { namePlaceholder: string } = $props();
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<AvatarsView {namePlaceholder} mochi:hydrate />
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import '@fontsource-variable/doto';
|
|
3
|
+
import '@fontsource-variable/jetbrains-mono';
|
|
4
|
+
import InstanceView from '../components/InstanceView.svelte';
|
|
5
|
+
|
|
6
|
+
let { id, injectionChecks }: { id: string; injectionChecks: number } = $props();
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<InstanceView {id} {injectionChecks} mochi:hydrate />
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import '@fontsource-variable/doto';
|
|
3
|
+
import '@fontsource-variable/jetbrains-mono';
|
|
4
|
+
import SettingsView from '../components/SettingsView.svelte';
|
|
5
|
+
|
|
6
|
+
let {
|
|
7
|
+
defaultImage,
|
|
8
|
+
builtinImage,
|
|
9
|
+
disableBuildCache,
|
|
10
|
+
copyIgnorePatterns,
|
|
11
|
+
builtinCopyIgnore,
|
|
12
|
+
dockerArch,
|
|
13
|
+
manualTokensEnabled,
|
|
14
|
+
githubTokenSet,
|
|
15
|
+
claudeTokenSet,
|
|
16
|
+
customEndpointEnabled,
|
|
17
|
+
customEndpointBaseUrl,
|
|
18
|
+
customEndpointTokenSet,
|
|
19
|
+
customEndpointOpusModel,
|
|
20
|
+
customEndpointSonnetModel,
|
|
21
|
+
customEndpointHaikuModel,
|
|
22
|
+
customEndpointSmallFastModel,
|
|
23
|
+
customEndpointModel,
|
|
24
|
+
hostEnvVarsEnabled,
|
|
25
|
+
hostEnvVarNames,
|
|
26
|
+
hostEnvVarPresence,
|
|
27
|
+
version
|
|
28
|
+
}: {
|
|
29
|
+
defaultImage: string;
|
|
30
|
+
builtinImage: string;
|
|
31
|
+
disableBuildCache: boolean;
|
|
32
|
+
copyIgnorePatterns: string;
|
|
33
|
+
builtinCopyIgnore: string;
|
|
34
|
+
dockerArch: string | null;
|
|
35
|
+
manualTokensEnabled: boolean;
|
|
36
|
+
githubTokenSet: boolean;
|
|
37
|
+
claudeTokenSet: boolean;
|
|
38
|
+
customEndpointEnabled: boolean;
|
|
39
|
+
customEndpointBaseUrl: string;
|
|
40
|
+
customEndpointTokenSet: boolean;
|
|
41
|
+
customEndpointOpusModel: string;
|
|
42
|
+
customEndpointSonnetModel: string;
|
|
43
|
+
customEndpointHaikuModel: string;
|
|
44
|
+
customEndpointSmallFastModel: string;
|
|
45
|
+
customEndpointModel: string;
|
|
46
|
+
hostEnvVarsEnabled: boolean;
|
|
47
|
+
hostEnvVarNames: string[];
|
|
48
|
+
hostEnvVarPresence: Record<string, boolean>;
|
|
49
|
+
version: string;
|
|
50
|
+
} = $props();
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<SettingsView
|
|
54
|
+
{defaultImage}
|
|
55
|
+
{builtinImage}
|
|
56
|
+
{disableBuildCache}
|
|
57
|
+
{copyIgnorePatterns}
|
|
58
|
+
{builtinCopyIgnore}
|
|
59
|
+
{dockerArch}
|
|
60
|
+
{manualTokensEnabled}
|
|
61
|
+
{githubTokenSet}
|
|
62
|
+
{claudeTokenSet}
|
|
63
|
+
{customEndpointEnabled}
|
|
64
|
+
{customEndpointBaseUrl}
|
|
65
|
+
{customEndpointTokenSet}
|
|
66
|
+
{customEndpointOpusModel}
|
|
67
|
+
{customEndpointSonnetModel}
|
|
68
|
+
{customEndpointHaikuModel}
|
|
69
|
+
{customEndpointSmallFastModel}
|
|
70
|
+
{customEndpointModel}
|
|
71
|
+
{hostEnvVarsEnabled}
|
|
72
|
+
{hostEnvVarNames}
|
|
73
|
+
{hostEnvVarPresence}
|
|
74
|
+
{version}
|
|
75
|
+
mochi:hydrate
|
|
76
|
+
/>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import '@fontsource-variable/doto';
|
|
3
|
+
import '@fontsource-variable/jetbrains-mono';
|
|
4
|
+
import type { Preflight } from '../types.ts';
|
|
5
|
+
import UiShowcase from '../components/UiShowcase.svelte';
|
|
6
|
+
|
|
7
|
+
let { preflight }: { preflight: Preflight } = $props();
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<UiShowcase {preflight} mochi:hydrate />
|