vela 0.10.13 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vela",
3
- "version": "0.10.13",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "description": "A CLI for creating and updating SvelteKit projects",
6
6
  "license": "MIT",
@@ -34,7 +34,7 @@
34
34
  "dependencies": {
35
35
  "@clack/prompts": "^1.7.0",
36
36
  "@faker-js/faker": "^10.6.0",
37
- "@velastack/patterns": "^0.1.9",
37
+ "@velastack/patterns": "^0.2.2",
38
38
  "@velastack/pocketbase-codegen": "^0.1.0",
39
39
  "annotate-json-schema": "^0.1.0",
40
40
  "commander": "^13.1.0",
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "$schema": "https://shadcn-svelte.com/schema.json",
3
+ "style": "vega",
3
4
  "tailwind": {
4
5
  "css": "src/app.css",
5
- "baseColor": "slate"
6
+ "baseColor": "neutral"
6
7
  },
7
8
  "aliases": {
8
9
  "components": "$lib/components",
@@ -12,5 +13,6 @@
12
13
  "lib": "$lib"
13
14
  },
14
15
  "typescript": true,
15
- "registry": "https://shadcn-svelte.com/registry"
16
+ "registry": "https://shadcn-svelte.com/registry",
17
+ "iconLibrary": "lucide"
16
18
  }
@@ -15,13 +15,14 @@
15
15
  "test:server": "vela test:server"
16
16
  },
17
17
  "devDependencies": {
18
- "@lucide/svelte": "^1.8.0",
18
+ "@lucide/svelte": "^1.40.0",
19
19
  "@sveltejs/adapter-auto": "^7.0.1",
20
20
  "@sveltejs/kit": "^2.70.3",
21
21
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
22
22
  "@tailwindcss/forms": "^0.5.11",
23
23
  "@tailwindcss/typography": "^0.5.19",
24
24
  "@tailwindcss/vite": "^4.3.3",
25
+ "@types/node": "^26.4.1",
25
26
  "@types/supertest": "^6.0.3",
26
27
  "@velastack/kit": "^0.3.0",
27
28
  "@velastack/pocketbase": "^0.2.2",
@@ -31,7 +32,7 @@
31
32
  "pocketbase-sveltekit": "^0.28.0",
32
33
  "prettier": "^3.9.6",
33
34
  "prettier-plugin-svelte": "^4.1.1",
34
- "shadcn-svelte": "^1.2.7",
35
+ "shadcn-svelte": "^1.6.0",
35
36
  "supertest": "^7.2.2",
36
37
  "svelte": "^5.56.10",
37
38
  "svelte-check": "^4.6.0",
@@ -40,7 +41,7 @@
40
41
  "sveltekit-flash-message": "^2.4.6",
41
42
  "sveltekit-superforms": "^2.30.2",
42
43
  "tailwind-merge": "^3.5.0",
43
- "tailwind-variants": "^3.2.2",
44
+ "tailwind-variants": "^3.3.0",
44
45
  "tailwindcss": "^4.3.3",
45
46
  "tw-animate-css": "^1.3.5",
46
47
  "typescript": "^6.0.3",
@@ -48,5 +49,8 @@
48
49
  "vite": "^8.2.2",
49
50
  "vitest": "^4.1.11",
50
51
  "zod": "^4.1.11"
52
+ },
53
+ "overrides": {
54
+ "vitest": "^4.1.11"
51
55
  }
52
56
  }
@@ -1,5 +1,6 @@
1
1
  @import 'tailwindcss';
2
2
  @import 'tw-animate-css';
3
+ @import 'shadcn-svelte/tailwind.css';
3
4
  @plugin '@tailwindcss/typography';
4
5
 
5
6
  @custom-variant dark (&:is(.dark *));
@@ -7,70 +8,70 @@
7
8
  :root {
8
9
  --radius: 0.625rem;
9
10
  --background: oklch(1 0 0);
10
- --foreground: oklch(0.129 0.042 264.695);
11
+ --foreground: oklch(0.145 0 0);
11
12
  --card: oklch(1 0 0);
12
- --card-foreground: oklch(0.129 0.042 264.695);
13
+ --card-foreground: oklch(0.145 0 0);
13
14
  --popover: oklch(1 0 0);
14
- --popover-foreground: oklch(0.129 0.042 264.695);
15
- --primary: oklch(0.208 0.042 265.755);
16
- --primary-foreground: oklch(0.984 0.003 247.858);
17
- --secondary: oklch(0.968 0.007 247.896);
18
- --secondary-foreground: oklch(0.208 0.042 265.755);
19
- --muted: oklch(0.968 0.007 247.896);
20
- --muted-foreground: oklch(0.554 0.046 257.417);
21
- --accent: oklch(0.968 0.007 247.896);
22
- --accent-foreground: oklch(0.208 0.042 265.755);
15
+ --popover-foreground: oklch(0.145 0 0);
16
+ --primary: oklch(0.205 0 0);
17
+ --primary-foreground: oklch(0.985 0 0);
18
+ --secondary: oklch(0.97 0 0);
19
+ --secondary-foreground: oklch(0.205 0 0);
20
+ --muted: oklch(0.97 0 0);
21
+ --muted-foreground: oklch(0.556 0 0);
22
+ --accent: oklch(0.97 0 0);
23
+ --accent-foreground: oklch(0.205 0 0);
23
24
  --destructive: oklch(0.577 0.245 27.325);
24
- --border: oklch(0.929 0.013 255.508);
25
- --input: oklch(0.929 0.013 255.508);
26
- --ring: oklch(0.704 0.04 256.788);
27
- --chart-1: oklch(0.646 0.222 41.116);
28
- --chart-2: oklch(0.6 0.118 184.704);
29
- --chart-3: oklch(0.398 0.07 227.392);
30
- --chart-4: oklch(0.828 0.189 84.429);
31
- --chart-5: oklch(0.769 0.188 70.08);
32
- --sidebar: oklch(0.984 0.003 247.858);
33
- --sidebar-foreground: oklch(0.129 0.042 264.695);
34
- --sidebar-primary: oklch(0.208 0.042 265.755);
35
- --sidebar-primary-foreground: oklch(0.984 0.003 247.858);
36
- --sidebar-accent: oklch(0.968 0.007 247.896);
37
- --sidebar-accent-foreground: oklch(0.208 0.042 265.755);
38
- --sidebar-border: oklch(0.929 0.013 255.508);
39
- --sidebar-ring: oklch(0.704 0.04 256.788);
25
+ --border: oklch(0.922 0 0);
26
+ --input: oklch(0.922 0 0);
27
+ --ring: oklch(0.708 0 0);
28
+ --chart-1: oklch(0.87 0 0);
29
+ --chart-2: oklch(0.556 0 0);
30
+ --chart-3: oklch(0.439 0 0);
31
+ --chart-4: oklch(0.371 0 0);
32
+ --chart-5: oklch(0.269 0 0);
33
+ --sidebar: oklch(0.985 0 0);
34
+ --sidebar-foreground: oklch(0.145 0 0);
35
+ --sidebar-primary: oklch(0.205 0 0);
36
+ --sidebar-primary-foreground: oklch(0.985 0 0);
37
+ --sidebar-accent: oklch(0.97 0 0);
38
+ --sidebar-accent-foreground: oklch(0.205 0 0);
39
+ --sidebar-border: oklch(0.922 0 0);
40
+ --sidebar-ring: oklch(0.708 0 0);
40
41
  }
41
42
 
42
43
  .dark {
43
- --background: oklch(0.129 0.042 264.695);
44
- --foreground: oklch(0.984 0.003 247.858);
45
- --card: oklch(0.208 0.042 265.755);
46
- --card-foreground: oklch(0.984 0.003 247.858);
47
- --popover: oklch(0.208 0.042 265.755);
48
- --popover-foreground: oklch(0.984 0.003 247.858);
49
- --primary: oklch(0.929 0.013 255.508);
50
- --primary-foreground: oklch(0.208 0.042 265.755);
51
- --secondary: oklch(0.279 0.041 260.031);
52
- --secondary-foreground: oklch(0.984 0.003 247.858);
53
- --muted: oklch(0.279 0.041 260.031);
54
- --muted-foreground: oklch(0.704 0.04 256.788);
55
- --accent: oklch(0.279 0.041 260.031);
56
- --accent-foreground: oklch(0.984 0.003 247.858);
44
+ --background: oklch(0.145 0 0);
45
+ --foreground: oklch(0.985 0 0);
46
+ --card: oklch(0.205 0 0);
47
+ --card-foreground: oklch(0.985 0 0);
48
+ --popover: oklch(0.205 0 0);
49
+ --popover-foreground: oklch(0.985 0 0);
50
+ --primary: oklch(0.922 0 0);
51
+ --primary-foreground: oklch(0.205 0 0);
52
+ --secondary: oklch(0.269 0 0);
53
+ --secondary-foreground: oklch(0.985 0 0);
54
+ --muted: oklch(0.269 0 0);
55
+ --muted-foreground: oklch(0.708 0 0);
56
+ --accent: oklch(0.269 0 0);
57
+ --accent-foreground: oklch(0.985 0 0);
57
58
  --destructive: oklch(0.704 0.191 22.216);
58
59
  --border: oklch(1 0 0 / 10%);
59
60
  --input: oklch(1 0 0 / 15%);
60
- --ring: oklch(0.551 0.027 264.364);
61
- --chart-1: oklch(0.488 0.243 264.376);
62
- --chart-2: oklch(0.696 0.17 162.48);
63
- --chart-3: oklch(0.769 0.188 70.08);
64
- --chart-4: oklch(0.627 0.265 303.9);
65
- --chart-5: oklch(0.645 0.246 16.439);
66
- --sidebar: oklch(0.208 0.042 265.755);
67
- --sidebar-foreground: oklch(0.984 0.003 247.858);
61
+ --ring: oklch(0.556 0 0);
62
+ --chart-1: oklch(0.87 0 0);
63
+ --chart-2: oklch(0.556 0 0);
64
+ --chart-3: oklch(0.439 0 0);
65
+ --chart-4: oklch(0.371 0 0);
66
+ --chart-5: oklch(0.269 0 0);
67
+ --sidebar: oklch(0.205 0 0);
68
+ --sidebar-foreground: oklch(0.985 0 0);
68
69
  --sidebar-primary: oklch(0.488 0.243 264.376);
69
- --sidebar-primary-foreground: oklch(0.984 0.003 247.858);
70
- --sidebar-accent: oklch(0.279 0.041 260.031);
71
- --sidebar-accent-foreground: oklch(0.984 0.003 247.858);
70
+ --sidebar-primary-foreground: oklch(0.985 0 0);
71
+ --sidebar-accent: oklch(0.269 0 0);
72
+ --sidebar-accent-foreground: oklch(0.985 0 0);
72
73
  --sidebar-border: oklch(1 0 0 / 10%);
73
- --sidebar-ring: oklch(0.551 0.027 264.364);
74
+ --sidebar-ring: oklch(0.556 0 0);
74
75
  }
75
76
 
76
77
  @theme inline {
@@ -109,6 +110,9 @@
109
110
  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
110
111
  --color-sidebar-border: var(--sidebar-border);
111
112
  --color-sidebar-ring: var(--sidebar-ring);
113
+ --radius-2xl: calc(var(--radius) * 1.8);
114
+ --radius-3xl: calc(var(--radius) * 2.2);
115
+ --radius-4xl: calc(var(--radius) * 2.6);
112
116
  }
113
117
 
114
118
  @layer base {
@@ -1,26 +1,35 @@
1
1
  <script lang="ts" module>
2
- import { cn, type WithElementRef } from '$lib/utils';
3
- import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
4
2
  import { type VariantProps, tv } from 'tailwind-variants';
3
+ import { cn, type WithElementRef } from '$lib/utils.js';
4
+ import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
5
5
 
6
6
  export const buttonVariants = tv({
7
- base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
7
+ base: "rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 group/button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
8
8
  variants: {
9
9
  variant: {
10
- default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
11
- destructive:
12
- 'bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white',
10
+ default: 'bg-primary text-primary-foreground hover:bg-primary/80',
13
11
  outline:
14
- 'bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border',
15
- secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
16
- ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
12
+ 'border-border bg-background shadow-xs hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
13
+ secondary:
14
+ 'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
15
+ ghost:
16
+ 'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',
17
+ destructive:
18
+ 'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
17
19
  link: 'text-primary underline-offset-4 hover:underline'
18
20
  },
19
21
  size: {
20
- default: 'h-9 px-4 py-2 has-[>svg]:px-3',
21
- sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5',
22
- lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
23
- icon: 'size-9'
22
+ default:
23
+ 'h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
24
+ xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
25
+ sm: 'h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5',
26
+ lg: 'h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
27
+ icon: 'size-9',
28
+ 'icon-xs':
29
+ "size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3",
30
+ 'icon-sm':
31
+ 'size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md',
32
+ 'icon-lg': 'size-10'
24
33
  }
25
34
  },
26
35
  defaultVariants: {
@@ -1,6 +1,11 @@
1
1
  <script lang="ts">
2
- import { Toaster as Sonner, type ToasterProps as SonnerProps } from 'svelte-sonner';
3
2
  import { mode } from 'mode-watcher';
3
+ import { Toaster as Sonner, type ToasterProps as SonnerProps } from 'svelte-sonner';
4
+ import Loader2Icon from '@lucide/svelte/icons/loader-2';
5
+ import CircleCheckIcon from '@lucide/svelte/icons/circle-check';
6
+ import OctagonXIcon from '@lucide/svelte/icons/octagon-x';
7
+ import InfoIcon from '@lucide/svelte/icons/info';
8
+ import TriangleAlertIcon from '@lucide/svelte/icons/triangle-alert';
4
9
 
5
10
  let { ...restProps }: SonnerProps = $props();
6
11
  </script>
@@ -10,4 +15,20 @@
10
15
  class="toaster group"
11
16
  style="--normal-bg: var(--color-popover); --normal-text: var(--color-popover-foreground); --normal-border: var(--color-border);"
12
17
  {...restProps}
13
- />
18
+ >
19
+ {#snippet loadingIcon()}
20
+ <Loader2Icon class="size-4 animate-spin" />
21
+ {/snippet}
22
+ {#snippet successIcon()}
23
+ <CircleCheckIcon class="size-4" />
24
+ {/snippet}
25
+ {#snippet errorIcon()}
26
+ <OctagonXIcon class="size-4" />
27
+ {/snippet}
28
+ {#snippet infoIcon()}
29
+ <InfoIcon class="size-4" />
30
+ {/snippet}
31
+ {#snippet warningIcon()}
32
+ <TriangleAlertIcon class="size-4" />
33
+ {/snippet}
34
+ </Sonner>
@@ -22,8 +22,9 @@
22
22
  <p class="text-muted-foreground mt-4 text-lg">
23
23
  A SvelteKit app with a PocketBase backend already migrated behind it, and types synced from
24
24
  your schema as it changes. This page is
25
- <code class="bg-muted rounded px-1.5 py-0.5 font-mono text-base">src/routes/(public)/+page.svelte</code>,
26
- and it is yours to delete.
25
+ <code class="bg-muted rounded px-1.5 py-0.5 font-mono text-base"
26
+ >src/routes/(public)/+page.svelte</code
27
+ >, and it is yours to delete.
27
28
  </p>
28
29
 
29
30
  <div class="mt-8 flex items-center justify-center gap-3">
@@ -68,8 +69,8 @@
68
69
  <div class="bg-card text-card-foreground rounded-lg border p-6">
69
70
  <h3 class="font-medium">Your interface</h3>
70
71
  <p class="text-muted-foreground mt-2">
71
- shadcn components, copied into your project. Yours to restyle, with no dependency
72
- to fight.
72
+ shadcn components, copied into your project. Yours to restyle, with no dependency to
73
+ fight.
73
74
  </p>
74
75
  <code
75
76
  class="bg-muted text-muted-foreground mt-4 block overflow-x-auto rounded-md px-3 py-2 font-mono text-xs"
@@ -12,9 +12,7 @@ export default defineConfig({
12
12
  filename.split(/[/\\]/).includes('node_modules') ? undefined : true
13
13
  },
14
14
  adapter: adapter(),
15
- ...(process.env.VELA_ORIGIN
16
- ? { prerender: { origin: process.env.VELA_ORIGIN } }
17
- : {})
15
+ ...(process.env.VELA_ORIGIN ? { prerender: { origin: process.env.VELA_ORIGIN } } : {})
18
16
  })
19
17
  ]
20
18
  });
@@ -24,6 +24,7 @@ APP_NAME=$INSTANCE
24
24
  APP_ID=$INSTANCE
25
25
  ENV_TAG=prod
26
26
  DOMAIN=""
27
+ MANAGED=""
27
28
  HEALTH_PATH=/
28
29
  KEEP=5
29
30
  PB_VERSION=""
@@ -37,6 +38,7 @@ while [ $# -gt 0 ]; do
37
38
  --app-id) APP_ID=$2; shift 2 ;;
38
39
  --env) ENV_TAG=$2; shift 2 ;;
39
40
  --domain) DOMAIN=$2; shift 2 ;;
41
+ --managed) MANAGED=$2; shift 2 ;;
40
42
  --health-path) HEALTH_PATH=$2; shift 2 ;;
41
43
  --keep) KEEP=$2; shift 2 ;;
42
44
  --pb-version) PB_VERSION=$2; shift 2 ;;
@@ -88,7 +90,10 @@ PORTS=$(allocate_ports "$INSTANCE")
88
90
  WEB_PORT=$(printf '%s' "$PORTS" | jq -r .web)
89
91
  PB_PORT=$(printf '%s' "$PORTS" | jq -r .pb)
90
92
 
93
+ # The canonical host: a domain the user's own DNS points here outranks a
94
+ # managed velastack.app name, which then redirects to it at the edge.
91
95
  PRIMARY_DOMAIN=$(printf '%s' "$DOMAIN" | cut -d, -f1 | tr -d ' ')
96
+ [ -n "$PRIMARY_DOMAIN" ] || PRIMARY_DOMAIN=$(printf '%s' "$MANAGED" | cut -d, -f1 | tr -d ' ')
92
97
  if [ -n "$PRIMARY_DOMAIN" ]; then
93
98
  ORIGIN="https://$PRIMARY_DOMAIN"
94
99
  else
@@ -325,17 +330,28 @@ ACTIVATED=1
325
330
  state_merge "$INSTANCE" "$(jq -c -n \
326
331
  --arg app "$APP_ID" --arg name "$APP_NAME" --arg env "$ENV_TAG" \
327
332
  --arg instance "$INSTANCE" --arg release "$RELEASE" --arg previous "$PREVIOUS" \
328
- --arg domain "$DOMAIN" --arg health "$HEALTH_PATH" --arg pb "$PB_VERSION" \
333
+ --arg domain "$DOMAIN" --arg managed "$MANAGED" --arg url "$ORIGIN" \
334
+ --arg health "$HEALTH_PATH" --arg pb "$PB_VERSION" \
329
335
  --arg sha "$GIT_SHA" --arg at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
330
336
  --argjson web "$WEB_PORT" --argjson pbport "$PB_PORT" --argjson backend "$BACKEND" \
331
337
  '{appId: $app, name: $name, env: $env, instance: $instance,
332
338
  activeRelease: $release, previousRelease: $previous, domain: $domain,
339
+ managed: $managed, url: $url,
333
340
  healthCheckPath: $health, pocketbaseVersion: $pb, gitSha: $sha,
334
341
  webPort: $web, pbPort: $pbport, backend: ($backend == 1), deployedAt: $at}')"
335
342
 
336
343
  # ------------------------------------------------------------------ routing
344
+ #
345
+ # Two files, written independently and each only if the whole Caddy config
346
+ # still validates with it in place (`caddy_install` keeps the previous one
347
+ # otherwise). Hosts the user's DNS points here get a site block of their own
348
+ # and a certificate each; managed velastack.app hosts arrive through the
349
+ # Worker at this server's origin site and are picked out by header.
337
350
 
338
351
  CADDY_SNIPPET="$VELA_ETC/caddy/$INSTANCE.caddy"
352
+ ROUTE_SNIPPET="$VELA_ETC/caddy/routes/$INSTANCE.route"
353
+ ROUTING_CHANGED=0
354
+
339
355
  if [ -n "$DOMAIN" ]; then
340
356
  hosts=$(printf '%s' "$DOMAIN" | tr ',' '\n' | sed 's/^ *//; s/ *$//' | grep -v '^$' | paste -sd, - | sed 's/,/, /g')
341
357
  tmp=$(mktemp "$VELA_ETC/caddy/.snippet.XXXXXX")
@@ -343,18 +359,42 @@ if [ -n "$DOMAIN" ]; then
343
359
  printf '# Managed by vela - app %s (%s)\n' "$APP_NAME" "$INSTANCE"
344
360
  printf '%s {\n\treverse_proxy 127.0.0.1:%s\n}\n' "$hosts" "$WEB_PORT"
345
361
  } > "$tmp"
346
- chmod 0644 "$tmp"
347
- mv -f "$tmp" "$CADDY_SNIPPET"
348
- if ! caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile >/dev/null 2>&1; then
349
- rm -f "$CADDY_SNIPPET"
350
- die "generated Caddy config for $DOMAIN is invalid"
351
- fi
352
- systemctl reload caddy
362
+ caddy_install "$tmp" "$CADDY_SNIPPET" || die "generated Caddy config for $DOMAIN is invalid"
363
+ ROUTING_CHANGED=1
353
364
  elif [ -f "$CADDY_SNIPPET" ]; then
354
365
  rm -f "$CADDY_SNIPPET"
355
- systemctl reload caddy || true
366
+ ROUTING_CHANGED=1
356
367
  fi
357
368
 
369
+ if [ -n "$MANAGED" ]; then
370
+ [ -f "$VELA_ETC/caddy/00-origin.caddy" ] \
371
+ || die "managed hostnames need this server's origin site - the CLI runs origin.sh first"
372
+ # A matcher name is an identifier; instance ids carry dashes.
373
+ matcher=vela_$(printf '%s' "$INSTANCE" | tr -c 'a-zA-Z0-9_' '_')
374
+ tmp=$(mktemp "$VELA_ETC/caddy/routes/.route.XXXXXX")
375
+ {
376
+ printf '# Managed by vela - app %s (%s)\n' "$APP_NAME" "$INSTANCE"
377
+ printf '@%s {\n' "$matcher"
378
+ printf '%s' "$MANAGED" | tr ',' '\n' | sed 's/^ *//; s/ *$//' | grep -v '^$' \
379
+ | while IFS= read -r host; do printf '\theader X-Velastack-Host %s\n' "$host"; done
380
+ printf '}\n'
381
+ # The app should see the public host and the visitor, not the origin
382
+ # name and the Cloudflare edge that carried the request here.
383
+ printf 'handle @%s {\n' "$matcher"
384
+ printf '\treverse_proxy 127.0.0.1:%s {\n' "$WEB_PORT"
385
+ printf '\t\theader_up Host {http.request.header.X-Velastack-Host}\n'
386
+ printf '\t\theader_up X-Forwarded-For {http.request.header.X-Velastack-Client-IP}\n'
387
+ printf '\t}\n}\n'
388
+ } > "$tmp"
389
+ caddy_install "$tmp" "$ROUTE_SNIPPET" || die "generated Caddy route for $MANAGED is invalid"
390
+ ROUTING_CHANGED=1
391
+ elif [ -f "$ROUTE_SNIPPET" ]; then
392
+ rm -f "$ROUTE_SNIPPET"
393
+ ROUTING_CHANGED=1
394
+ fi
395
+
396
+ [ "$ROUTING_CHANGED" = 0 ] || caddy_reload
397
+
358
398
 
359
399
  # ------------------------------------------------------------------- pruning
360
400
 
@@ -379,7 +419,7 @@ if [ "$KEEP" -gt 0 ] 2>/dev/null; then
379
419
  fi
380
420
 
381
421
  emit_result \
382
- --arg instance "$INSTANCE" --arg release "$RELEASE" --arg domain "$DOMAIN" \
422
+ --arg instance "$INSTANCE" --arg release "$RELEASE" --arg domain "$DOMAIN" --arg managed "$MANAGED" \
383
423
  --arg url "$ORIGIN" --argjson web "$WEB_PORT" --argjson pb "$PB_PORT" --argjson su "$SU_CREATED" \
384
- '{instance: $instance, release: $release, domain: $domain, url: $url,
424
+ '{instance: $instance, release: $release, domain: $domain, managed: $managed, url: $url,
385
425
  webPort: $web, pbPort: $pb, superuserCreated: ($su == 1)}'
@@ -34,8 +34,8 @@ for unit in "$(unit_web "$INSTANCE")" "$(unit_pb "$INSTANCE")"; do
34
34
  done
35
35
 
36
36
  log "removing routing"
37
- rm -f "$VELA_ETC/caddy/$INSTANCE.caddy"
38
- systemctl reload caddy >/dev/null 2>&1 || true
37
+ rm -f "$VELA_ETC/caddy/$INSTANCE.caddy" "$VELA_ETC/caddy/routes/$INSTANCE.route"
38
+ caddy_reload || true
39
39
 
40
40
  log "removing releases"
41
41
  rm -rf "$APP/releases" "$APP/deps" "$APP/current" "$APP/bin"
@@ -228,3 +228,41 @@ assert_superuser_auth() {
228
228
  -X POST -H 'content-type: application/json' --data-binary @- \
229
229
  "http://127.0.0.1:$port/api/collections/_superusers/auth-with-password"
230
230
  }
231
+
232
+ # ------------------------------------------------------------------- caddy
233
+
234
+ CADDYFILE=${CADDYFILE:-/etc/caddy/Caddyfile}
235
+
236
+ caddy_valid() {
237
+ caddy validate --config "$CADDYFILE" --adapter caddyfile >/dev/null 2>&1
238
+ }
239
+
240
+ # Put a generated snippet in place only if the whole Caddy config still
241
+ # validates with it there. The file it replaces is kept and restored on
242
+ # failure, so a bad generation cannot take a working route down.
243
+ #
244
+ # usage: caddy_install <tmp> <dest> [mode] [owner]
245
+ caddy_install() {
246
+ local tmp=$1 dest=$2 mode=${3:-0644} owner=${4:-root:root} backup=""
247
+ chmod "$mode" "$tmp"
248
+ chown "$owner" "$tmp"
249
+ if [ -f "$dest" ]; then
250
+ backup=$(mktemp "$(dirname "$dest")/.previous.XXXXXX")
251
+ cp -p "$dest" "$backup"
252
+ fi
253
+ mv -f "$tmp" "$dest"
254
+ if caddy_valid; then
255
+ [ -z "$backup" ] || rm -f "$backup"
256
+ return 0
257
+ fi
258
+ if [ -n "$backup" ]; then
259
+ mv -f "$backup" "$dest"
260
+ else
261
+ rm -f "$dest"
262
+ fi
263
+ return 1
264
+ }
265
+
266
+ caddy_reload() {
267
+ systemctl reload caddy >/dev/null 2>&1 || systemctl restart caddy
268
+ }
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Publish this server's origin site: the one hostname the velastack.app Worker
4
+ # proxies managed hostnames to.
5
+ #
6
+ # Reads /etc/vela/origin.json, which the CLI writes after registering the
7
+ # server with velastack.dev. The site block refuses anything without the
8
+ # server's token, then hands matching requests to per-instance route files
9
+ # that apply.sh writes under routes/. Idempotent, and quiet when nothing has
10
+ # changed, so a deploy does not reload Caddy for no reason.
11
+ #
12
+ # usage: origin.sh
13
+ set -Eeuo pipefail
14
+
15
+ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
16
+ # shellcheck source=lib.sh
17
+ . "$SCRIPT_DIR/lib.sh"
18
+
19
+ require_provisioned
20
+ [ "$(id -u)" -eq 0 ] || die "origin must run as root"
21
+
22
+ ORIGIN_FILE="$VELA_ETC/origin.json"
23
+ [ -f "$ORIGIN_FILE" ] || die "$ORIGIN_FILE is missing - the CLI registers the server before running this"
24
+
25
+ HOST=$(jq -er .originHost "$ORIGIN_FILE") || die "$ORIGIN_FILE has no originHost"
26
+ TOKEN=$(jq -er .token "$ORIGIN_FILE") || die "$ORIGIN_FILE has no token"
27
+
28
+ ROUTES="$VELA_ETC/caddy/routes"
29
+ mkdir -p "$ROUTES"
30
+ chmod 0755 "$ROUTES"
31
+ # An import glob that matches nothing is a hard error in Caddy, so keep a
32
+ # comment-only file in place before the first managed instance is deployed.
33
+ if [ ! -f "$ROUTES/00-vela.route" ]; then
34
+ printf '# Managed by vela. Instance routes are written alongside this file.\n' \
35
+ > "$ROUTES/00-vela.route"
36
+ fi
37
+
38
+ SNIPPET="$VELA_ETC/caddy/00-origin.caddy"
39
+ desired=$(
40
+ printf '# Managed by vela - origin for velastack.app managed hostnames\n'
41
+ printf '%s {\n' "$HOST"
42
+ printf '\t@untrusted not header X-Velastack-Origin-Token "%s"\n' "$TOKEN"
43
+ printf '\thandle @untrusted {\n\t\trespond 403\n\t}\n'
44
+ printf '\timport %s/*.route\n' "$ROUTES"
45
+ printf '\thandle {\n\t\trespond 404\n\t}\n'
46
+ printf '}\n'
47
+ )
48
+
49
+ if [ -f "$SNIPPET" ] && [ "$(cat "$SNIPPET")" = "$desired" ]; then
50
+ emit_result --arg host "$HOST" '{originHost: $host, changed: false}'
51
+ exit 0
52
+ fi
53
+
54
+ tmp=$(mktemp "$VELA_ETC/caddy/.origin.XXXXXX")
55
+ printf '%s\n' "$desired" > "$tmp"
56
+ # Readable by the caddy user (which is what `caddy reload` runs as) and no one
57
+ # else: the token must not leak to the apps, which run as $VELA_USER.
58
+ caddy_install "$tmp" "$SNIPPET" 0640 root:caddy \
59
+ || die "generated origin config for $HOST is invalid"
60
+ caddy_reload
61
+
62
+ emit_result --arg host "$HOST" '{originHost: $host, changed: true}'
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "$schema": "https://shadcn-svelte.com/schema.json",
3
+ "style": "vega",
3
4
  "tailwind": {
4
5
  "css": "src/app.css",
5
- "baseColor": "slate"
6
+ "baseColor": "neutral"
6
7
  },
7
8
  "aliases": {
8
9
  "components": "$lib/components",
@@ -12,5 +13,6 @@
12
13
  "lib": "$lib"
13
14
  },
14
15
  "typescript": true,
15
- "registry": "https://shadcn-svelte.com/registry"
16
+ "registry": "https://shadcn-svelte.com/registry",
17
+ "iconLibrary": "lucide"
16
18
  }
@@ -14,25 +14,26 @@
14
14
  "format": "prettier --write ."
15
15
  },
16
16
  "devDependencies": {
17
- "@lucide/svelte": "^1.8.0",
17
+ "@lucide/svelte": "^1.40.0",
18
18
  "@sveltejs/adapter-static": "^3.0.10",
19
19
  "@sveltejs/kit": "^2.70.3",
20
20
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
21
21
  "@tailwindcss/forms": "^0.5.11",
22
22
  "@tailwindcss/typography": "^0.5.19",
23
23
  "@tailwindcss/vite": "^4.3.3",
24
+ "@types/node": "^26.4.1",
24
25
  "@velastack/kit": "^0.3.0",
25
26
  "clsx": "^2.1.1",
26
27
  "mode-watcher": "^1.1.0",
27
28
  "prettier": "^3.9.6",
28
29
  "prettier-plugin-svelte": "^4.1.1",
29
- "shadcn-svelte": "^1.2.7",
30
+ "shadcn-svelte": "^1.6.0",
30
31
  "svelte": "^5.56.10",
31
32
  "svelte-check": "^4.6.0",
32
33
  "svelte-meta-tags": "^5.0.2",
33
34
  "svelte-sonner": "^1.1.0",
34
35
  "tailwind-merge": "^3.5.0",
35
- "tailwind-variants": "^3.2.2",
36
+ "tailwind-variants": "^3.3.0",
36
37
  "tailwindcss": "^4.3.3",
37
38
  "tw-animate-css": "^1.3.5",
38
39
  "typescript": "^6.0.3",