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,140 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
/** Defaults outside the project tree; a relative `DATA_DIR` resolves against cwd. */
|
|
8
|
+
export const DATA_DIR = process.env.DATA_DIR
|
|
9
|
+
? isAbsolute(process.env.DATA_DIR)
|
|
10
|
+
? process.env.DATA_DIR
|
|
11
|
+
: resolve(process.cwd(), process.env.DATA_DIR)
|
|
12
|
+
: join(homedir(), '.codebay');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Without a stable key Mochi mints a random one per boot, so anything it signed
|
|
16
|
+
* stops verifying after a restart. Must run before `Mochi.serve()` reads the env.
|
|
17
|
+
*/
|
|
18
|
+
export function ensureMochiKey(): void {
|
|
19
|
+
if (process.env.MOCHI_KEY?.trim()) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const keyPath = join(DATA_DIR, 'mochi-key');
|
|
23
|
+
try {
|
|
24
|
+
if (existsSync(keyPath)) {
|
|
25
|
+
const existing = readFileSync(keyPath, 'utf8').trim();
|
|
26
|
+
if (existing) {
|
|
27
|
+
process.env.MOCHI_KEY = existing;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const key = randomBytes(32).toString('base64url');
|
|
32
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
33
|
+
writeFileSync(keyPath, key + '\n', { mode: 0o600 });
|
|
34
|
+
process.env.MOCHI_KEY = key;
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.warn(`⚠ Could not persist a MOCHI_KEY in ${DATA_DIR}: ${(err as Error).message}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Read relative to this module, not cwd, because under `bunx codebay` cwd is the user's folder. */
|
|
41
|
+
export const APP_VERSION: string = (() => {
|
|
42
|
+
try {
|
|
43
|
+
const path = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json');
|
|
44
|
+
const version = JSON.parse(readFileSync(path, 'utf8')).version;
|
|
45
|
+
return typeof version === 'string' ? version : 'unknown';
|
|
46
|
+
} catch {
|
|
47
|
+
return 'unknown';
|
|
48
|
+
}
|
|
49
|
+
})();
|
|
50
|
+
|
|
51
|
+
/** Per-instance working copies live here: <INSTANCES_DIR>/<id>/workspace. */
|
|
52
|
+
export const INSTANCES_DIR = join(DATA_DIR, 'instances');
|
|
53
|
+
|
|
54
|
+
export const DB_PATH = join(DATA_DIR, 'app.sqlite');
|
|
55
|
+
|
|
56
|
+
export const PORT_BASE = 8001;
|
|
57
|
+
export const PORT_MAX = 8999;
|
|
58
|
+
|
|
59
|
+
/** Inside the container, not on the host. */
|
|
60
|
+
export const CODE_SERVER_PORT = 8080;
|
|
61
|
+
|
|
62
|
+
/** `base:ubuntu` is multi-arch, unlike the `universal` images, which are amd64-only. */
|
|
63
|
+
export const DEFAULT_IMAGE = 'mcr.microsoft.com/devcontainers/base:ubuntu';
|
|
64
|
+
|
|
65
|
+
/** An unset/empty password disables the gate entirely (local dev). */
|
|
66
|
+
export const BASIC_AUTH_USERNAME = process.env.BASIC_AUTH_USERNAME || 'admin';
|
|
67
|
+
export const BASIC_AUTH_PASSWORD = process.env.BASIC_AUTH_PASSWORD || '';
|
|
68
|
+
|
|
69
|
+
/** Loopback by default so an instance with no password isn't exposed to the LAN. */
|
|
70
|
+
export const HOST = process.env.HOST || '127.0.0.1';
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Forwarded app ports follow the server's bind; code-server's own port never does
|
|
74
|
+
* (it runs with auth disabled). Anything but `0.0.0.0` degrades to loopback rather
|
|
75
|
+
* than risking an appPort string Docker rejects.
|
|
76
|
+
*/
|
|
77
|
+
export const PUBLISH_HOST = HOST === '0.0.0.0' ? '0.0.0.0' : '127.0.0.1';
|
|
78
|
+
|
|
79
|
+
export const PORT = Number(process.env.PORT) || 6969;
|
|
80
|
+
|
|
81
|
+
/** Mochi's CSRF origin check compares against these; override when behind a proxy. */
|
|
82
|
+
export const PUBLIC_ORIGIN = process.env.PUBLIC_ORIGIN?.trim() || `http://localhost:${PORT}`;
|
|
83
|
+
export const TRUSTED_ORIGINS = (process.env.TRUSTED_ORIGINS || '')
|
|
84
|
+
.split(',')
|
|
85
|
+
.map((o) => o.trim())
|
|
86
|
+
.filter(Boolean);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Set to skip host-credential discovery, e.g. on a server with no Keychain.
|
|
90
|
+
* A token entered in Settings still outranks these; see the credential injections.
|
|
91
|
+
*/
|
|
92
|
+
export const CLAUDE_CODE_TOKEN =
|
|
93
|
+
(process.env.CODEBAY_CLAUDE_CODE_TOKEN ?? process.env.DCM_CLAUDE_CODE_TOKEN)?.trim() || '';
|
|
94
|
+
export const GITHUB_TOKEN =
|
|
95
|
+
(process.env.CODEBAY_GITHUB_TOKEN ?? process.env.DCM_GITHUB_TOKEN)?.trim() || '';
|
|
96
|
+
|
|
97
|
+
/** macOS only — the Linux fallback (~/.claude/.credentials.json) has no service name. */
|
|
98
|
+
export const CLAUDE_KEYCHAIN_SERVICE =
|
|
99
|
+
process.env.CODEBAY_CLAUDE_KEYCHAIN_SERVICE?.trim() || 'Claude Code-credentials';
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* `.git` is deliberately absent so each instance keeps its history/remote.
|
|
103
|
+
* An explicit empty string (as opposed to unset) means "copy everything".
|
|
104
|
+
*/
|
|
105
|
+
export const DEFAULT_COPY_IGNORE = 'node_modules';
|
|
106
|
+
|
|
107
|
+
export function parseCopyIgnore(raw: string): Set<string> {
|
|
108
|
+
return new Set(
|
|
109
|
+
raw
|
|
110
|
+
.split(',')
|
|
111
|
+
.map((p) => p.trim())
|
|
112
|
+
.filter(Boolean)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** When unset, the docker/devcontainer CLIs resolve the current Docker context themselves. */
|
|
117
|
+
export const DOCKER_HOST = process.env.DOCKER_HOST?.trim() || '';
|
|
118
|
+
|
|
119
|
+
/** Keeps spawned `docker`/`devcontainer` processes pointed at the same daemon we use. */
|
|
120
|
+
export function dockerEnv(): Record<string, string | undefined> {
|
|
121
|
+
return DOCKER_HOST ? { ...process.env, DOCKER_HOST } : process.env;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Resolved from our own dependency tree, not cwd, because under `bunx codebay`
|
|
126
|
+
* cwd is the user's arbitrary folder.
|
|
127
|
+
*/
|
|
128
|
+
export function devcontainerBin(): string {
|
|
129
|
+
try {
|
|
130
|
+
const pkgJson = fileURLToPath(import.meta.resolve('@devcontainers/cli/package.json'));
|
|
131
|
+
const shim = join(dirname(pkgJson), '..', '..', '.bin', 'devcontainer');
|
|
132
|
+
if (existsSync(shim)) return shim;
|
|
133
|
+
const pkg = JSON.parse(readFileSync(pkgJson, 'utf8'));
|
|
134
|
+
const rel = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin?.devcontainer;
|
|
135
|
+
if (rel) return join(dirname(pkgJson), rel);
|
|
136
|
+
} catch {
|
|
137
|
+
// Fall through to the dev-checkout path below.
|
|
138
|
+
}
|
|
139
|
+
return join(process.cwd(), 'node_modules', '.bin', 'devcontainer');
|
|
140
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A plain `===` short-circuits on the first differing byte, leaking length and prefix
|
|
3
|
+
* through timing. Hashing first makes both sides fixed-width, so even the lengths don't leak.
|
|
4
|
+
*/
|
|
5
|
+
export function timingSafeEqualStr(a: string, b: string): boolean {
|
|
6
|
+
const ah = Bun.SHA256.hash(a, 'hex');
|
|
7
|
+
const bh = Bun.SHA256.hash(b, 'hex');
|
|
8
|
+
// No early exit — the loop must run its full length on every call.
|
|
9
|
+
let diff = ah.length ^ bh.length;
|
|
10
|
+
for (let i = 0; i < ah.length; i++) diff |= ah.charCodeAt(i) ^ bh.charCodeAt(i);
|
|
11
|
+
return diff === 0;
|
|
12
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { migrate, getMigrations } from '@zihaolam/bun-sqlite-migrations';
|
|
3
|
+
import { mkdirSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { DATA_DIR, DB_PATH } from './config.server.ts';
|
|
6
|
+
import type { FolderHistoryEntry } from '../types.ts';
|
|
7
|
+
|
|
8
|
+
// db.server.ts lives in src/lib, so ../../migrations resolves to the repo root.
|
|
9
|
+
const MIGRATIONS_DIR = join(import.meta.dir, '../../migrations');
|
|
10
|
+
|
|
11
|
+
export type InstanceStatus = 'creating' | 'running' | 'stopped' | 'error';
|
|
12
|
+
|
|
13
|
+
export interface InstanceRow {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
source_path: string;
|
|
17
|
+
workspace_path: string;
|
|
18
|
+
host_port: number;
|
|
19
|
+
container_id: string | null;
|
|
20
|
+
remote_workspace_folder: string | null;
|
|
21
|
+
status: InstanceStatus;
|
|
22
|
+
error: string | null;
|
|
23
|
+
created_at: number;
|
|
24
|
+
/** Per-instance secret the in-container Claude hook uses to authenticate to the bridge. */
|
|
25
|
+
bridge_token: string;
|
|
26
|
+
/** Container user the workspace runs as; needed to exec health checks in its home dir. */
|
|
27
|
+
remote_user: string | null;
|
|
28
|
+
/** `'local'` when the folder shipped its own config, else the injected image; null pre-dates this. */
|
|
29
|
+
image_source: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface PortForwardRow {
|
|
33
|
+
instance_id: string;
|
|
34
|
+
container_port: number;
|
|
35
|
+
host_port: number;
|
|
36
|
+
created_at: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Pin the connection to globalThis so dev-mode hot reload doesn't reopen it.
|
|
40
|
+
const globalForDb = globalThis as unknown as { __codebayDb?: Database };
|
|
41
|
+
|
|
42
|
+
function open(): Database {
|
|
43
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
44
|
+
const database = new Database(DB_PATH, { create: true });
|
|
45
|
+
database.run('PRAGMA journal_mode = WAL;');
|
|
46
|
+
migrate(database, getMigrations(MIGRATIONS_DIR));
|
|
47
|
+
return database;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const db: Database = (globalForDb.__codebayDb ??= open());
|
|
51
|
+
|
|
52
|
+
export function closeDb(): void {
|
|
53
|
+
globalForDb.__codebayDb?.close();
|
|
54
|
+
globalForDb.__codebayDb = undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function insertInstance(row: InstanceRow): void {
|
|
58
|
+
db.query(
|
|
59
|
+
`INSERT INTO instances
|
|
60
|
+
(id, name, source_path, workspace_path, host_port, container_id, remote_workspace_folder, status, error, created_at, bridge_token, remote_user, image_source)
|
|
61
|
+
VALUES ($id, $name, $source_path, $workspace_path, $host_port, $container_id, $remote_workspace_folder, $status, $error, $created_at, $bridge_token, $remote_user, $image_source)`
|
|
62
|
+
).run({
|
|
63
|
+
$id: row.id,
|
|
64
|
+
$name: row.name,
|
|
65
|
+
$source_path: row.source_path,
|
|
66
|
+
$workspace_path: row.workspace_path,
|
|
67
|
+
$host_port: row.host_port,
|
|
68
|
+
$container_id: row.container_id,
|
|
69
|
+
$remote_workspace_folder: row.remote_workspace_folder,
|
|
70
|
+
$status: row.status,
|
|
71
|
+
$error: row.error,
|
|
72
|
+
$created_at: row.created_at,
|
|
73
|
+
$bridge_token: row.bridge_token,
|
|
74
|
+
$remote_user: row.remote_user,
|
|
75
|
+
$image_source: row.image_source
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getInstance(id: string): InstanceRow | null {
|
|
80
|
+
return db.query('SELECT * FROM instances WHERE id = $id').get({ $id: id }) as InstanceRow | null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function allInstances(): InstanceRow[] {
|
|
84
|
+
return db.query('SELECT * FROM instances ORDER BY created_at DESC').all() as InstanceRow[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function usedPorts(): number[] {
|
|
88
|
+
// Both tables publish host ports, so allocation has to see the union of them.
|
|
89
|
+
const rows = db
|
|
90
|
+
.query('SELECT host_port FROM instances UNION SELECT host_port FROM port_forwards')
|
|
91
|
+
.all() as { host_port: number }[];
|
|
92
|
+
return rows.map((r) => r.host_port);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function listForwards(instanceId: string): PortForwardRow[] {
|
|
96
|
+
return db
|
|
97
|
+
.query('SELECT * FROM port_forwards WHERE instance_id = $id ORDER BY container_port')
|
|
98
|
+
.all({ $id: instanceId }) as PortForwardRow[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function allForwards(): PortForwardRow[] {
|
|
102
|
+
return db.query('SELECT * FROM port_forwards ORDER BY container_port').all() as PortForwardRow[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function insertForward(row: PortForwardRow): void {
|
|
106
|
+
db.query(
|
|
107
|
+
`INSERT INTO port_forwards (instance_id, container_port, host_port, created_at)
|
|
108
|
+
VALUES ($instance_id, $container_port, $host_port, $created_at)`
|
|
109
|
+
).run({
|
|
110
|
+
$instance_id: row.instance_id,
|
|
111
|
+
$container_port: row.container_port,
|
|
112
|
+
$host_port: row.host_port,
|
|
113
|
+
$created_at: row.created_at
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function deleteForward(instanceId: string, containerPort: number): void {
|
|
118
|
+
db.query('DELETE FROM port_forwards WHERE instance_id = $id AND container_port = $port').run({
|
|
119
|
+
$id: instanceId,
|
|
120
|
+
$port: containerPort
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function deleteForwards(instanceId: string): void {
|
|
125
|
+
db.query('DELETE FROM port_forwards WHERE instance_id = $id').run({ $id: instanceId });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The allowlist that keeps `updateInstance` from interpolating arbitrary column names. */
|
|
129
|
+
const UPDATABLE_COLUMNS = [
|
|
130
|
+
'name',
|
|
131
|
+
'container_id',
|
|
132
|
+
'remote_workspace_folder',
|
|
133
|
+
'status',
|
|
134
|
+
'error',
|
|
135
|
+
'remote_user',
|
|
136
|
+
'image_source'
|
|
137
|
+
] as const;
|
|
138
|
+
|
|
139
|
+
type UpdatableColumn = (typeof UPDATABLE_COLUMNS)[number];
|
|
140
|
+
|
|
141
|
+
export function updateInstance(
|
|
142
|
+
id: string,
|
|
143
|
+
patch: Partial<Pick<InstanceRow, UpdatableColumn>>
|
|
144
|
+
): void {
|
|
145
|
+
const sets: string[] = [];
|
|
146
|
+
const params: Record<string, string | number | null> = { $id: id };
|
|
147
|
+
for (const col of UPDATABLE_COLUMNS) {
|
|
148
|
+
if (col in patch) {
|
|
149
|
+
sets.push(`${col} = $${col}`);
|
|
150
|
+
params[`$${col}`] = patch[col] ?? null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (sets.length === 0) return;
|
|
154
|
+
db.query(`UPDATE instances SET ${sets.join(', ')} WHERE id = $id`).run(params);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function deleteInstanceRow(id: string): void {
|
|
158
|
+
db.query('DELETE FROM instances WHERE id = $id').run({ $id: id });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function recordFolder(source_path: string, name: string): void {
|
|
162
|
+
db.query(
|
|
163
|
+
`INSERT INTO folder_history (source_path, name, last_used_at)
|
|
164
|
+
VALUES ($source_path, $name, $last_used_at)
|
|
165
|
+
ON CONFLICT(source_path) DO UPDATE SET
|
|
166
|
+
name = excluded.name,
|
|
167
|
+
last_used_at = excluded.last_used_at`
|
|
168
|
+
).run({ $source_path: source_path, $name: name, $last_used_at: Date.now() });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function listFolderHistory(): FolderHistoryEntry[] {
|
|
172
|
+
return db
|
|
173
|
+
.query('SELECT * FROM folder_history ORDER BY last_used_at DESC')
|
|
174
|
+
.all() as FolderHistoryEntry[];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function deleteFolderHistory(source_path: string): void {
|
|
178
|
+
db.query('DELETE FROM folder_history WHERE source_path = $source_path').run({
|
|
179
|
+
$source_path: source_path
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function getOption(key: string): string | null {
|
|
184
|
+
const row = db.query('SELECT value FROM options WHERE key = $key').get({ $key: key }) as {
|
|
185
|
+
value: string;
|
|
186
|
+
} | null;
|
|
187
|
+
return row?.value ?? null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function setOption(key: string, value: string): void {
|
|
191
|
+
db.query(
|
|
192
|
+
`INSERT INTO options (key, value)
|
|
193
|
+
VALUES ($key, $value)
|
|
194
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
195
|
+
).run({ $key: key, $value: value });
|
|
196
|
+
}
|