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,121 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// The theme bans border-radius globally, so "round" here means round in pixels.
|
|
3
|
+
import type { AvatarArt } from '../avatars/types.ts';
|
|
4
|
+
import Avatar from './Avatar.svelte';
|
|
5
|
+
|
|
6
|
+
let { href = '/avatars' }: { href?: string } = $props();
|
|
7
|
+
|
|
8
|
+
const coinArt: AvatarArt = {
|
|
9
|
+
name: 'coin',
|
|
10
|
+
pixels: [
|
|
11
|
+
'..####..',
|
|
12
|
+
'.#....#.',
|
|
13
|
+
'#..##..#',
|
|
14
|
+
'#.####.#',
|
|
15
|
+
'#.####.#',
|
|
16
|
+
'#..##..#',
|
|
17
|
+
'.#....#.',
|
|
18
|
+
'..####..'
|
|
19
|
+
]
|
|
20
|
+
};
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<div class="coin-row">
|
|
24
|
+
<span class="pixel-line" aria-hidden="true"></span>
|
|
25
|
+
<a class="coin-btn" {href} aria-label="Draw your own avatar" title="Draw your own avatar">
|
|
26
|
+
<span class="new" aria-hidden="true">New</span>
|
|
27
|
+
<span class="coin" aria-hidden="true">
|
|
28
|
+
<Avatar art={coinArt} name="coin" scale={3} />
|
|
29
|
+
</span>
|
|
30
|
+
</a>
|
|
31
|
+
<span class="pixel-line" aria-hidden="true"></span>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<style>
|
|
35
|
+
.coin-row {
|
|
36
|
+
width: 100%;
|
|
37
|
+
max-width: 560px;
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
gap: 16px;
|
|
41
|
+
padding: 8px 0;
|
|
42
|
+
}
|
|
43
|
+
/* Dotted LED rule: square dots, like a row of unlit pixels. */
|
|
44
|
+
.pixel-line {
|
|
45
|
+
flex: 1;
|
|
46
|
+
height: 2px;
|
|
47
|
+
background: repeating-linear-gradient(90deg, var(--rule) 0 2px, transparent 2px 6px);
|
|
48
|
+
}
|
|
49
|
+
.coin-btn {
|
|
50
|
+
display: flex;
|
|
51
|
+
flex-direction: column;
|
|
52
|
+
align-items: center;
|
|
53
|
+
gap: 4px;
|
|
54
|
+
flex: none;
|
|
55
|
+
padding: 4px;
|
|
56
|
+
border: none;
|
|
57
|
+
background: none;
|
|
58
|
+
cursor: pointer;
|
|
59
|
+
text-decoration: none;
|
|
60
|
+
}
|
|
61
|
+
.new {
|
|
62
|
+
font-family: var(--font-mono);
|
|
63
|
+
font-weight: 700;
|
|
64
|
+
font-size: 8px;
|
|
65
|
+
text-transform: uppercase;
|
|
66
|
+
letter-spacing: 0.14em;
|
|
67
|
+
color: var(--ink-soft);
|
|
68
|
+
animation: coin-blink 2.6s steps(1, end) infinite;
|
|
69
|
+
}
|
|
70
|
+
.coin {
|
|
71
|
+
display: block;
|
|
72
|
+
box-shadow: 2px 2px 0 var(--ink);
|
|
73
|
+
animation: coin-spin 2.6s ease-in-out infinite;
|
|
74
|
+
}
|
|
75
|
+
/* The spin has to stop, or it would override the press transform. */
|
|
76
|
+
.coin-btn:hover .coin {
|
|
77
|
+
animation: none;
|
|
78
|
+
transform: translate(1px, 1px);
|
|
79
|
+
box-shadow: 1px 1px 0 var(--ink);
|
|
80
|
+
}
|
|
81
|
+
.coin-btn:focus-visible {
|
|
82
|
+
outline: 2px solid var(--ink);
|
|
83
|
+
outline-offset: 2px;
|
|
84
|
+
}
|
|
85
|
+
/* The sprite is symmetric, so the mirrored "back" reads as the same coin. */
|
|
86
|
+
@keyframes coin-spin {
|
|
87
|
+
0%,
|
|
88
|
+
55% {
|
|
89
|
+
transform: scaleX(1);
|
|
90
|
+
}
|
|
91
|
+
66% {
|
|
92
|
+
transform: scaleX(0.12);
|
|
93
|
+
}
|
|
94
|
+
78% {
|
|
95
|
+
transform: scaleX(-1);
|
|
96
|
+
}
|
|
97
|
+
89% {
|
|
98
|
+
transform: scaleX(0.12);
|
|
99
|
+
}
|
|
100
|
+
100% {
|
|
101
|
+
transform: scaleX(1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
@keyframes coin-blink {
|
|
105
|
+
0%,
|
|
106
|
+
55%,
|
|
107
|
+
100% {
|
|
108
|
+
opacity: 1;
|
|
109
|
+
}
|
|
110
|
+
66%,
|
|
111
|
+
89% {
|
|
112
|
+
opacity: 0.25;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
@media (prefers-reduced-motion: reduce) {
|
|
116
|
+
.coin,
|
|
117
|
+
.new {
|
|
118
|
+
animation: none;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
</style>
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Link from '@lucide/svelte/icons/link';
|
|
3
|
+
import TriangleAlert from '@lucide/svelte/icons/triangle-alert';
|
|
4
|
+
import X from '@lucide/svelte/icons/x';
|
|
5
|
+
import toast from 'svelte-french-toast';
|
|
6
|
+
import type { AuthProvider } from '../types.ts';
|
|
7
|
+
|
|
8
|
+
let { auth }: { auth: AuthProvider[] } = $props();
|
|
9
|
+
|
|
10
|
+
let credOpen = $state(false);
|
|
11
|
+
|
|
12
|
+
const authedCount = $derived(auth.filter((a) => a.available).length);
|
|
13
|
+
const credState = $derived(
|
|
14
|
+
authedCount === auth.length ? 'ok' : authedCount === 0 ? 'error' : 'warn'
|
|
15
|
+
);
|
|
16
|
+
const credIcon = { ok: Link, warn: TriangleAlert, error: X } as const;
|
|
17
|
+
const CredIcon = $derived(credIcon[credState]);
|
|
18
|
+
|
|
19
|
+
function explainProvider(provider: AuthProvider) {
|
|
20
|
+
if (provider.available) {
|
|
21
|
+
toast.success(`${provider.label} credentials come from ${provider.source}.`);
|
|
22
|
+
} else {
|
|
23
|
+
const hint = provider.hint ? ` — ${provider.hint}` : '';
|
|
24
|
+
toast.error(`No ${provider.label} credentials found${hint}.`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
$effect(() => {
|
|
29
|
+
if (!credOpen) return;
|
|
30
|
+
const onClick = (e: MouseEvent) => {
|
|
31
|
+
if (!(e.target as Element)?.closest('.cred-menu')) credOpen = false;
|
|
32
|
+
};
|
|
33
|
+
const onKey = (e: KeyboardEvent) => {
|
|
34
|
+
if (e.key === 'Escape') credOpen = false;
|
|
35
|
+
};
|
|
36
|
+
// Defer registration so the click that opened the menu doesn't immediately close it.
|
|
37
|
+
window.addEventListener('click', onClick);
|
|
38
|
+
window.addEventListener('keydown', onKey);
|
|
39
|
+
return () => {
|
|
40
|
+
window.removeEventListener('click', onClick);
|
|
41
|
+
window.removeEventListener('keydown', onKey);
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<div class="cred-menu">
|
|
47
|
+
<button
|
|
48
|
+
class="cred {credState}"
|
|
49
|
+
onclick={() => (credOpen = !credOpen)}
|
|
50
|
+
aria-expanded={credOpen}
|
|
51
|
+
aria-haspopup="menu"
|
|
52
|
+
aria-label="Credentials"
|
|
53
|
+
title="Credentials"
|
|
54
|
+
>
|
|
55
|
+
<CredIcon size={16} />
|
|
56
|
+
</button>
|
|
57
|
+
{#if credOpen}
|
|
58
|
+
<div class="cred-dropdown panel" role="menu">
|
|
59
|
+
{#each auth as provider (provider.id)}
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
class="cred-row"
|
|
63
|
+
role="menuitem"
|
|
64
|
+
onclick={() => explainProvider(provider)}
|
|
65
|
+
title={provider.available
|
|
66
|
+
? `Credentials from ${provider.source}, copied into each new instance.`
|
|
67
|
+
: `No credentials found on this computer${provider.hint ? ` — ${provider.hint}` : ''}.`}
|
|
68
|
+
>
|
|
69
|
+
<span class="dot {provider.available ? 'on' : 'off'}"></span>
|
|
70
|
+
<span class="cred-label">{provider.label}</span>
|
|
71
|
+
<span class="cred-state">{provider.available ? 'Authorized' : 'Not found'}</span>
|
|
72
|
+
</button>
|
|
73
|
+
{/each}
|
|
74
|
+
</div>
|
|
75
|
+
{/if}
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<style>
|
|
79
|
+
.cred-menu {
|
|
80
|
+
position: relative;
|
|
81
|
+
display: inline-flex;
|
|
82
|
+
}
|
|
83
|
+
.cred {
|
|
84
|
+
display: inline-flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
justify-content: center;
|
|
87
|
+
width: 32px;
|
|
88
|
+
height: 32px;
|
|
89
|
+
font-size: 14px;
|
|
90
|
+
line-height: 1;
|
|
91
|
+
border: 1px solid var(--ink);
|
|
92
|
+
background: var(--ink);
|
|
93
|
+
color: var(--bg);
|
|
94
|
+
cursor: pointer;
|
|
95
|
+
}
|
|
96
|
+
.cred:hover {
|
|
97
|
+
background: var(--bg);
|
|
98
|
+
color: var(--ink);
|
|
99
|
+
}
|
|
100
|
+
/* Auth states: ok = muted green, warn = tasteful yellow, error = outline-only. */
|
|
101
|
+
.cred.ok {
|
|
102
|
+
background: var(--ok-bg);
|
|
103
|
+
border-color: var(--ok-line);
|
|
104
|
+
color: var(--ok-ink);
|
|
105
|
+
}
|
|
106
|
+
.cred.ok:hover {
|
|
107
|
+
background: var(--ok-ink);
|
|
108
|
+
border-color: var(--ok-ink);
|
|
109
|
+
color: var(--ok-bg);
|
|
110
|
+
}
|
|
111
|
+
.cred.warn {
|
|
112
|
+
background: var(--warn-bg);
|
|
113
|
+
border-color: var(--warn-line);
|
|
114
|
+
color: var(--warn-ink);
|
|
115
|
+
}
|
|
116
|
+
.cred.warn:hover {
|
|
117
|
+
background: var(--warn-ink);
|
|
118
|
+
border-color: var(--warn-ink);
|
|
119
|
+
color: var(--warn-bg);
|
|
120
|
+
}
|
|
121
|
+
.cred.error {
|
|
122
|
+
background: var(--bg);
|
|
123
|
+
color: var(--ink);
|
|
124
|
+
}
|
|
125
|
+
.cred-dropdown {
|
|
126
|
+
position: absolute;
|
|
127
|
+
right: 0;
|
|
128
|
+
top: calc(100% + 6px);
|
|
129
|
+
z-index: 20;
|
|
130
|
+
min-width: 220px;
|
|
131
|
+
padding: 6px;
|
|
132
|
+
background: var(--bg-card);
|
|
133
|
+
}
|
|
134
|
+
.cred-row {
|
|
135
|
+
display: flex;
|
|
136
|
+
align-items: center;
|
|
137
|
+
gap: 9px;
|
|
138
|
+
width: 100%;
|
|
139
|
+
padding: 7px 9px;
|
|
140
|
+
border: 0;
|
|
141
|
+
background: transparent;
|
|
142
|
+
color: var(--ink);
|
|
143
|
+
text-align: left;
|
|
144
|
+
font-family: var(--font-mono);
|
|
145
|
+
font-size: 12px;
|
|
146
|
+
cursor: pointer;
|
|
147
|
+
}
|
|
148
|
+
.cred-row:hover {
|
|
149
|
+
background: var(--ink);
|
|
150
|
+
color: var(--bg);
|
|
151
|
+
}
|
|
152
|
+
.cred-row:hover .cred-state {
|
|
153
|
+
color: var(--bg);
|
|
154
|
+
}
|
|
155
|
+
.dot {
|
|
156
|
+
flex: none;
|
|
157
|
+
width: 9px;
|
|
158
|
+
height: 9px;
|
|
159
|
+
border: 1px solid currentColor;
|
|
160
|
+
}
|
|
161
|
+
.dot.on {
|
|
162
|
+
background: currentColor;
|
|
163
|
+
}
|
|
164
|
+
.dot.off {
|
|
165
|
+
background: transparent;
|
|
166
|
+
}
|
|
167
|
+
.cred-state {
|
|
168
|
+
margin-left: auto;
|
|
169
|
+
color: var(--ink-soft);
|
|
170
|
+
}
|
|
171
|
+
</style>
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { type Instance, type Preflight } from '../types.ts';
|
|
3
|
+
import FolderBrowser from './FolderBrowser.svelte';
|
|
4
|
+
import InstanceCard from './InstanceCard.svelte';
|
|
5
|
+
import TopBar from './TopBar.svelte';
|
|
6
|
+
import Button from './Button.svelte';
|
|
7
|
+
import Plus from '@lucide/svelte/icons/plus';
|
|
8
|
+
import { apiPost } from '../api.ts';
|
|
9
|
+
|
|
10
|
+
// Presentational: AppShell owns the single live subscription both views share.
|
|
11
|
+
let {
|
|
12
|
+
preflight,
|
|
13
|
+
instances,
|
|
14
|
+
loaded
|
|
15
|
+
}: { preflight: Preflight; instances: Instance[]; loaded: boolean } = $props();
|
|
16
|
+
|
|
17
|
+
let browserOpen = $state(false);
|
|
18
|
+
let creating = $state(false);
|
|
19
|
+
let actionError = $state<string | null>(null);
|
|
20
|
+
let editingId = $state<string | null>(null);
|
|
21
|
+
let editingName = $state('');
|
|
22
|
+
|
|
23
|
+
const ready = $derived(preflight.docker && preflight.cli);
|
|
24
|
+
|
|
25
|
+
async function createFrom(sourcePath: string, opts?: { branch?: string }) {
|
|
26
|
+
browserOpen = false;
|
|
27
|
+
creating = true;
|
|
28
|
+
actionError = null;
|
|
29
|
+
try {
|
|
30
|
+
await apiPost(
|
|
31
|
+
'/api/instances',
|
|
32
|
+
{ sourcePath, branch: opts?.branch },
|
|
33
|
+
'Failed to create instance'
|
|
34
|
+
);
|
|
35
|
+
// The live stream delivers the new instance.
|
|
36
|
+
} catch (err) {
|
|
37
|
+
actionError = (err as Error).message;
|
|
38
|
+
} finally {
|
|
39
|
+
creating = false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function act(id: string, action: 'start' | 'stop' | 'delete' | 'rebuild') {
|
|
44
|
+
actionError = null;
|
|
45
|
+
if (action === 'delete' && !confirm('Delete this instance and its copied files?')) return;
|
|
46
|
+
// Rebuild discards the container; the workspace copy lives on the host and survives.
|
|
47
|
+
if (
|
|
48
|
+
action === 'rebuild' &&
|
|
49
|
+
!confirm('Recreate this container and re-run setup? The workspace is kept.')
|
|
50
|
+
)
|
|
51
|
+
return;
|
|
52
|
+
try {
|
|
53
|
+
await apiPost(`/api/instances/${id}/${action}`, undefined, `Failed to ${action}`);
|
|
54
|
+
// The live stream reflects the resulting state.
|
|
55
|
+
} catch (err) {
|
|
56
|
+
actionError = (err as Error).message;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function startRename(instance: Instance) {
|
|
61
|
+
editingId = instance.id;
|
|
62
|
+
editingName = instance.name;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function cancelRename() {
|
|
66
|
+
editingId = null;
|
|
67
|
+
editingName = '';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function commitRename(id: string) {
|
|
71
|
+
const name = editingName.trim();
|
|
72
|
+
const original = instances.find((i) => i.id === id)?.name;
|
|
73
|
+
// Nothing to do on an empty or unchanged name — just close the editor.
|
|
74
|
+
if (!name || name === original) {
|
|
75
|
+
cancelRename();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
cancelRename();
|
|
79
|
+
actionError = null;
|
|
80
|
+
try {
|
|
81
|
+
await apiPost(`/api/instances/${id}/rename`, { name }, 'Failed to rename');
|
|
82
|
+
// The live stream reflects the new name.
|
|
83
|
+
} catch (err) {
|
|
84
|
+
actionError = (err as Error).message;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function deleteAll() {
|
|
89
|
+
actionError = null;
|
|
90
|
+
if (!confirm(`Delete all ${instances.length} instances and their copied files?`)) return;
|
|
91
|
+
try {
|
|
92
|
+
await apiPost('/api/instances/delete-all', undefined, 'Failed to delete all');
|
|
93
|
+
// The live stream reflects the resulting empty state.
|
|
94
|
+
} catch (err) {
|
|
95
|
+
actionError = (err as Error).message;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
</script>
|
|
99
|
+
|
|
100
|
+
<TopBar
|
|
101
|
+
auth={preflight.auth}
|
|
102
|
+
canDelete={instances.length > 0}
|
|
103
|
+
{ready}
|
|
104
|
+
{creating}
|
|
105
|
+
onNew={() => (browserOpen = true)}
|
|
106
|
+
onDeleteAll={deleteAll}
|
|
107
|
+
/>
|
|
108
|
+
|
|
109
|
+
<main class="stage">
|
|
110
|
+
{#if !ready}
|
|
111
|
+
<div class="banner error">
|
|
112
|
+
<strong>Setup needed.</strong>
|
|
113
|
+
{#if !preflight.docker}<span>Docker daemon is not reachable.</span>{/if}
|
|
114
|
+
{#if !preflight.cli}<span>The devcontainer CLI is not available.</span>{/if}
|
|
115
|
+
</div>
|
|
116
|
+
{/if}
|
|
117
|
+
|
|
118
|
+
{#if actionError}
|
|
119
|
+
<div class="banner error"><strong>Error.</strong> <span>{actionError}</span></div>
|
|
120
|
+
{/if}
|
|
121
|
+
|
|
122
|
+
{#if !loaded}
|
|
123
|
+
<div class="empty"><p class="empty-sub">Loading…</p></div>
|
|
124
|
+
{:else if instances.length === 0}
|
|
125
|
+
<div class="empty">
|
|
126
|
+
<p class="empty-title">No instances yet</p>
|
|
127
|
+
<p class="empty-sub">Pick a project folder to spin up an isolated devcontainer.</p>
|
|
128
|
+
<Button variant="primary" icon={Plus} onclick={() => (browserOpen = true)} disabled={!ready}>
|
|
129
|
+
New instance
|
|
130
|
+
</Button>
|
|
131
|
+
</div>
|
|
132
|
+
{:else}
|
|
133
|
+
<ul class="grid">
|
|
134
|
+
{#each instances as instance (instance.id)}
|
|
135
|
+
<InstanceCard
|
|
136
|
+
{instance}
|
|
137
|
+
editing={editingId === instance.id}
|
|
138
|
+
bind:editingName
|
|
139
|
+
onact={(action) => act(instance.id, action)}
|
|
140
|
+
onstartrename={() => startRename(instance)}
|
|
141
|
+
oncommitrename={() => commitRename(instance.id)}
|
|
142
|
+
oncancelrename={cancelRename}
|
|
143
|
+
/>
|
|
144
|
+
{/each}
|
|
145
|
+
</ul>
|
|
146
|
+
{/if}
|
|
147
|
+
</main>
|
|
148
|
+
|
|
149
|
+
{#if browserOpen}
|
|
150
|
+
<FolderBrowser onpick={createFrom} onclose={() => (browserOpen = false)} />
|
|
151
|
+
{/if}
|
|
152
|
+
|
|
153
|
+
<style>
|
|
154
|
+
.stage {
|
|
155
|
+
max-width: 1080px;
|
|
156
|
+
margin: 0 auto;
|
|
157
|
+
padding: 28px 24px 80px;
|
|
158
|
+
}
|
|
159
|
+
.banner {
|
|
160
|
+
padding: 12px 16px;
|
|
161
|
+
margin-bottom: 20px;
|
|
162
|
+
display: flex;
|
|
163
|
+
gap: 8px;
|
|
164
|
+
flex-wrap: wrap;
|
|
165
|
+
font-family: var(--font-mono);
|
|
166
|
+
font-size: 13px;
|
|
167
|
+
}
|
|
168
|
+
.banner.error {
|
|
169
|
+
background: var(--bg-card);
|
|
170
|
+
color: var(--ink);
|
|
171
|
+
border: 2px solid var(--ink);
|
|
172
|
+
box-shadow: 4px 4px 0 var(--ink);
|
|
173
|
+
}
|
|
174
|
+
.empty {
|
|
175
|
+
text-align: center;
|
|
176
|
+
padding: 80px 20px;
|
|
177
|
+
}
|
|
178
|
+
.empty-title {
|
|
179
|
+
font-family: var(--font-display);
|
|
180
|
+
font-size: 26px;
|
|
181
|
+
font-weight: 700;
|
|
182
|
+
text-transform: uppercase;
|
|
183
|
+
letter-spacing: 0.06em;
|
|
184
|
+
margin: 0 0 8px;
|
|
185
|
+
}
|
|
186
|
+
.empty-sub {
|
|
187
|
+
color: var(--ink-soft);
|
|
188
|
+
margin: 0 0 22px;
|
|
189
|
+
}
|
|
190
|
+
.grid {
|
|
191
|
+
list-style: none;
|
|
192
|
+
margin: 0;
|
|
193
|
+
padding: 0;
|
|
194
|
+
display: grid;
|
|
195
|
+
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
196
|
+
gap: 18px;
|
|
197
|
+
}
|
|
198
|
+
</style>
|