feat(rhema-code): add the colors setting with four palettes
The stored `colors` key names a palette — rhema (the default, Jadeite), aquamarine, black-and-white, or dracula — and the Appearance page of /settings cycles it live, like the theme and icon toggles beside it. Each palette carries a dark and a light appearance under the existing `theme` key; dracula's light side is derived, the official palette ships only dark. The appearance decision moves off token comparison: Theme now answers is_dark() from the ground/text luminance, so palettes beyond the original two get the right per-profile fallback tones.
This commit is contained in:
commit
81da26b955
159 files changed
+50628
No files matched your search
@@ -0,0 +1,11 @@
|
||||
# Patterns to ignore when building the package.
|
||||
.DS_Store
|
||||
.git/
|
||||
.gitignore
|
||||
.nojekyll
|
||||
*.swp
|
||||
*.tmp
|
||||
*.orig
|
||||
*.rej
|
||||
*.tgz
|
||||
vault
|
||||
@@ -0,0 +1,7 @@
|
||||
apiVersion: v2
|
||||
name: rhema-claw
|
||||
description: "The resident forge-watching agent daemon: forge watches and a
|
||||
chat HTTP surface over one SQLite store."
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "0.1.0"
|
||||
@@ -0,0 +1,31 @@
|
||||
rhema-claw {{ .Chart.AppVersion }} deployed as {{ include "rhema-claw.fullname" . }}.
|
||||
|
||||
The daemon is single-node by design: one SQLite store, one dispatcher
|
||||
(specs/rhema-framework-v1.md §7) — do not scale the Deployment.
|
||||
|
||||
1. Talk to it (port-forward; the surface speaks OpenAI chat-completions too):
|
||||
|
||||
kubectl port-forward svc/{{ include "rhema-claw.fullname" . }} {{ .Values.service.port }}:{{ .Values.service.port }}
|
||||
curl http://127.0.0.1:{{ .Values.service.port }}/healthz
|
||||
|
||||
2. Config: values `config` renders into ConfigMap {{ include "rhema-claw.fullname" . }}
|
||||
(claw.yaml, mounted at /config/claw.yaml). db and workspace_root are pinned
|
||||
under /data; edit them through values, not the ConfigMap.
|
||||
{{- if not (or .Values.secret.existingSecret .Values.secret.stringData) }}
|
||||
|
||||
3. No credentials Secret is configured — set values secret.stringData
|
||||
(e.g. ANTHROPIC_API_KEY, GITHUB_TOKEN) or secret.existingSecret, or the
|
||||
daemon refuses to start when a watch or dispatch needs the key.
|
||||
{{- else }}
|
||||
|
||||
3. Credentials: injected from the Secret into the environment.
|
||||
{{- end }}
|
||||
|
||||
4. Token: /v1 routes are protected by the token set in values; /healthz
|
||||
stays open either way. The render refuses an empty token — the daemon
|
||||
binds all interfaces in-cluster and will not serve unguarded.
|
||||
{{- if not .Values.persistence.enabled }}
|
||||
|
||||
WARNING: persistence.enabled=false — the SQLite store and workspaces live in
|
||||
an emptyDir and are lost when the pod restarts.
|
||||
{{- end }}
|
||||
@@ -0,0 +1,49 @@
|
||||
{{/* Chart-wide names and labels. */}}
|
||||
|
||||
{{- define "rhema-claw.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "rhema-claw.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "rhema-claw.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | quote }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "rhema-claw.labels" -}}
|
||||
helm.sh/chart: {{ include "rhema-claw.chart" . }}
|
||||
{{ include "rhema-claw.selectorLabels" . }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "rhema-claw.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "rhema-claw.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "rhema-claw.imageTag" -}}
|
||||
{{- default .Chart.AppVersion .Values.image.tag }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
The single-node invariant, enforced at render time: one daemon owns every
|
||||
session in one SQLite store, with one dispatcher — scaling past one replica
|
||||
would corrupt the store, not share the load (specs/rhema-framework-v1.md §7).
|
||||
*/}}
|
||||
{{- define "rhema-claw.assertSingleReplica" -}}
|
||||
{{- if ne (int .Values.replicaCount) 1 }}
|
||||
{{- fail (printf "replicaCount must be 1, got %v: rhema-claw is single-node by design (one SQLite store, one dispatcher; specs/rhema-framework-v1.md §7)" .Values.replicaCount) }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,20 @@
|
||||
{{- $cfg := deepCopy (.Values.config | default dict) }}
|
||||
{{- /* The state volume is fixed at /data and the Service needs the process
|
||||
bound to all interfaces, so these three win over whatever values say. */ -}}
|
||||
{{- $_ := set $cfg "db" "/data/claw.db" }}
|
||||
{{- $_ := set $cfg "workspace_root" "/data/workspaces" }}
|
||||
{{- $_ := set $cfg "listen" (printf "0.0.0.0:%v" .Values.service.port) }}
|
||||
{{- if .Values.token }}
|
||||
{{- $_ := set $cfg "token" .Values.token }}
|
||||
{{- else }}
|
||||
{{- $_ := unset $cfg "token" }}
|
||||
{{- end }}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "rhema-claw.fullname" . }}
|
||||
labels:
|
||||
{{- include "rhema-claw.labels" . | nindent 4 }}
|
||||
data:
|
||||
claw.yaml: |
|
||||
{{ toYaml $cfg | indent 4 }}
|
||||
@@ -0,0 +1,99 @@
|
||||
{{- include "rhema-claw.assertSingleReplica" . }}
|
||||
{{- /* The daemon binds 0.0.0.0 in-cluster (the Service needs every
|
||||
interface) and refuses to serve a non-loopback surface without a
|
||||
bearer token — fail the render here, before the pod does. */ -}}
|
||||
{{- if not .Values.token }}
|
||||
{{- fail "token is required: the daemon binds all interfaces in-cluster, and it refuses to serve a non-loopback surface without a bearer token (set `token` in values)" }}
|
||||
{{- end }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "rhema-claw.fullname" . }}
|
||||
labels:
|
||||
{{- include "rhema-claw.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "rhema-claw.selectorLabels" . | nindent 6 }}
|
||||
app.kubernetes.io/component: server
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "rhema-claw.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: server
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
# The image runs as uid 1000; fsGroup makes the PVC writable by it.
|
||||
fsGroup: 1000
|
||||
containers:
|
||||
- name: claw
|
||||
image: "{{ .Values.image.repository }}:{{ include "rhema-claw.imageTag" . }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
args:
|
||||
- --config
|
||||
- /config/claw.yaml
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.port }}
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
initialDelaySeconds: 2
|
||||
periodSeconds: 5
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
runAsGroup: 1000
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
{{- if or .Values.secret.existingSecret .Values.secret.stringData }}
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: {{ .Values.secret.existingSecret | default (include "rhema-claw.fullname" .) }}
|
||||
{{- end }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /config
|
||||
readOnly: true
|
||||
- name: data
|
||||
mountPath: /data
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: {{ include "rhema-claw.fullname" . }}
|
||||
- name: data
|
||||
{{- if .Values.persistence.enabled }}
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ include "rhema-claw.fullname" . }}
|
||||
{{- else }}
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,68 @@
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- if not .Values.ingress.host }}
|
||||
{{- fail "ingress.host is required when ingress.enabled: the claw is single-user and bearer-gated, and an unnamed host has no TLS story" }}
|
||||
{{- end }}
|
||||
{{- if and .Values.ingress.tls.enabled (not .Values.ingress.tls.secretName) }}
|
||||
{{- fail "ingress.tls.secretName is required when ingress.tls.enabled: bearer tokens cross this ingress in the clear without TLS" }}
|
||||
{{- end }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "rhema-claw.fullname" . }}
|
||||
labels:
|
||||
{{- include "rhema-claw.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with .Values.ingress.className }}
|
||||
ingressClassName: {{ . }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.tls.enabled }}
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ .Values.ingress.host | quote }}
|
||||
secretName: {{ .Values.ingress.tls.secretName }}
|
||||
{{- end }}
|
||||
rules:
|
||||
- host: {{ .Values.ingress.host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- if .Values.ui.enabled }}
|
||||
# One address, two services: the browser gets the UI on /,
|
||||
# the daemon answers /v1 and /healthz (longest prefix wins),
|
||||
# and the phone app points at the same host. One origin, so
|
||||
# no CORS anywhere; the bearer token is the auth.
|
||||
- path: /v1
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "rhema-claw.fullname" . }}
|
||||
port:
|
||||
name: http
|
||||
- path: /healthz
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "rhema-claw.fullname" . }}
|
||||
port:
|
||||
name: http
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "rhema-claw.fullname" . }}-ui
|
||||
port:
|
||||
name: http
|
||||
{{- else }}
|
||||
# No UI in the release: the daemon owns the whole host.
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "rhema-claw.fullname" . }}
|
||||
port:
|
||||
name: http
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,17 @@
|
||||
{{- if .Values.persistence.enabled }}
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ include "rhema-claw.fullname" . }}
|
||||
labels:
|
||||
{{- include "rhema-claw.labels" . | nindent 4 }}
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
{{- with .Values.persistence.storageClass }}
|
||||
storageClassName: {{ . | quote }}
|
||||
{{- end }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.persistence.size }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,10 @@
|
||||
{{- if and (not .Values.secret.existingSecret) .Values.secret.stringData }}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ include "rhema-claw.fullname" . }}
|
||||
labels:
|
||||
{{- include "rhema-claw.labels" . | nindent 4 }}
|
||||
stringData:
|
||||
{{- toYaml .Values.secret.stringData | nindent 2 }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,18 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "rhema-claw.fullname" . }}
|
||||
labels:
|
||||
{{- include "rhema-claw.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
selector:
|
||||
{{- include "rhema-claw.selectorLabels" . | nindent 4 }}
|
||||
# The UI pods share the name/instance labels; without this the
|
||||
# daemon's service load-balances chat traffic to nginx.
|
||||
app.kubernetes.io/component: server
|
||||
@@ -0,0 +1,45 @@
|
||||
{{- if .Values.ui.enabled }}
|
||||
{{- if not .Values.ui.image.repository }}
|
||||
{{- fail "ui.image.repository is required when ui.enabled: the UI image is built from deploy/docker/ui.Dockerfile" }}
|
||||
{{- end }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "rhema-claw.fullname" . }}-ui
|
||||
labels:
|
||||
{{- include "rhema-claw.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: ui
|
||||
spec:
|
||||
# Stateless static files: scaling the UI is safe. The single-node
|
||||
# invariant belongs to the daemon, not to nginx.
|
||||
replicas: {{ .Values.ui.replicas | default 1 }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "rhema-claw.selectorLabels" . | nindent 6 }}
|
||||
app.kubernetes.io/component: ui
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "rhema-claw.labels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: ui
|
||||
spec:
|
||||
containers:
|
||||
- name: ui
|
||||
image: "{{ .Values.ui.image.repository }}:{{ default .Chart.AppVersion .Values.ui.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.ui.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 80
|
||||
# The shell answering is the whole contract: the bundle is
|
||||
# static, the API calls happen browser-side against /v1.
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
resources:
|
||||
{{- toYaml .Values.ui.resources | nindent 12 }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,17 @@
|
||||
{{- if .Values.ui.enabled }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "rhema-claw.fullname" . }}-ui
|
||||
labels:
|
||||
{{- include "rhema-claw.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: ui
|
||||
spec:
|
||||
selector:
|
||||
{{- include "rhema-claw.selectorLabels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: ui
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: http
|
||||
{{- end }}
|
||||
@@ -0,0 +1,93 @@
|
||||
# rhema-claw is single-node by design: one daemon owns every session in one
|
||||
# SQLite store, with one dispatcher (specs/rhema-framework-v1.md §7). The
|
||||
# templates fail the render when this is anything but 1 — it is an
|
||||
# invariant, not a starting point.
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: rhema-claw
|
||||
# Defaults to Chart appVersion when empty.
|
||||
tag: ""
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
imagePullSecrets: []
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
# Bearer token for the /v1 routes, rendered into claw.yaml. Required:
|
||||
# the daemon binds all interfaces in-cluster (the Service needs it) and
|
||||
# refuses to serve a non-loopback surface without a token, so the render
|
||||
# fails when this is empty. /healthz stays open either way.
|
||||
token: ""
|
||||
|
||||
# The claw.yaml content. db, workspace_root, and listen are pinned by the
|
||||
# templates (the PVC is fixed at /data and the container must bind all
|
||||
# interfaces for the Service); whatever these say here is overridden.
|
||||
config:
|
||||
poll_interval_secs: 60
|
||||
# Seed watches: imported once, while the watch table is still empty.
|
||||
# Example:
|
||||
# watches:
|
||||
# - forge: github
|
||||
# repo: acme/widgets
|
||||
watches: []
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 7799
|
||||
|
||||
ingress:
|
||||
# Off by default: the claw is single-user and bearer-gated, and bearer
|
||||
# tokens must not cross the public internet in the clear. When enabled,
|
||||
# a host is required (the render refuses without one), TLS is strongly
|
||||
# recommended, and the whole surface — chat, /v1, /healthz — is one
|
||||
# origin. A UI on the same host's / path composes: put the UI's
|
||||
# ingress first, or route /v1 here from its own host.
|
||||
enabled: false
|
||||
# Required when enabled; the render refuses an ingress without one.
|
||||
host: ""
|
||||
# The ingress controller's class (nginx, traefik, ...); empty means
|
||||
# the cluster default.
|
||||
className: ""
|
||||
annotations: {}
|
||||
tls:
|
||||
enabled: false
|
||||
# The cert-manager (or manually provisioned) secret carrying the
|
||||
# certificate; required when tls.enabled.
|
||||
secretName: ""
|
||||
|
||||
ui:
|
||||
# The web UI on the same host as the daemon: the ingress routes /
|
||||
# here and /v1 (+ /healthz) to the daemon, so one address serves the
|
||||
# browser and the phone app alike, with no CORS anywhere. Stateless
|
||||
# static files — replicas may scale freely.
|
||||
enabled: true
|
||||
image:
|
||||
# Built by deploy/docker/ui.Dockerfile; published by the ui job in
|
||||
# the publish workflow.
|
||||
repository: git.vhaudiquet.fr/vhaudiquet/rhema-claw-ui
|
||||
# Defaults to the chart's appVersion when empty.
|
||||
tag: ""
|
||||
pullPolicy: IfNotPresent
|
||||
resources: {}
|
||||
|
||||
persistence:
|
||||
enabled: true
|
||||
storageClass: ""
|
||||
size: 5Gi
|
||||
|
||||
# Provider and forge credentials reach the daemon as env vars (the
|
||||
# provider keys and GITHUB_TOKEN/GITEA_TOKEN/GITLAB_TOKEN fallbacks).
|
||||
# One mechanism: either name an existingSecret, or list key/value pairs
|
||||
# in stringData and the chart creates the Secret.
|
||||
secret:
|
||||
existingSecret: ""
|
||||
stringData: {}
|
||||
# stringData:
|
||||
# ANTHROPIC_API_KEY: sk-ant-...
|
||||
# GITHUB_TOKEN: ghp_...
|
||||
|
||||
resources: {}
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
Reference in new issue
Block a user