feat: v2 — queue matchmaking, procedural audio, sprite archetypes, auth
- Add queue-based matchmaking with Elo-proximity and 10s timeout - Procedural sound engine (SFX, voice announcer, 4-track music) - Sprite system refactored into 6 archetypes (standard, lobster, sheep, cyborg, blob, tank) - 42+ fight choreographies with themed/generic/wild card selection - 4 KO finish styles, super-speed mode, hyperdetail close-ups - Auth routes, JoinBout page, bot profile with stats - 7-tier ranking system (Baby through Legend) - Arena and challenge system expansions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
335c148866
commit
47d20fbe66
@@ -0,0 +1,49 @@
|
||||
---
|
||||
name: add-app
|
||||
description: Step-by-step guide for adding a new containerized app to Archipelago
|
||||
disable-model-invocation: true
|
||||
allowed-tools: Bash, Read, Write, Edit, Glob, Grep
|
||||
argument-hint: "[app-name]"
|
||||
---
|
||||
|
||||
Add a new containerized app ($ARGUMENTS) to Archipelago.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Create the manifest
|
||||
|
||||
Create `apps/{app-id}/manifest.yml` following the spec in `docs/app-manifest-spec.md`:
|
||||
- `app.id` (kebab-case), `app.name`, `app.version` (SemVer)
|
||||
- `container.image` (pinned version, **NEVER** `latest`)
|
||||
- `security`: readonly_root, dropped capabilities, non-root UID > 1000
|
||||
- `health_check`, `dependencies`
|
||||
|
||||
### 2. Add app icon
|
||||
|
||||
Place icon at `neode-ui/public/assets/img/app-icons/{app-id}.{png|webp|svg}`
|
||||
|
||||
### 3. Create status UI (if no native web UI)
|
||||
|
||||
For apps without their own web interface, create a UI container in `docker/{app-id}-ui/` following the patterns in `.cursor/rules/APP-UI-STANDARDS.md`.
|
||||
|
||||
Reference implementations:
|
||||
- Bitcoin UI: `docker/bitcoin-ui/`
|
||||
- LND UI: `docker/lnd-ui/`
|
||||
|
||||
### 4. Update backend
|
||||
|
||||
- Add port mapping in `core/archipelago/src/container/docker_packages.rs`
|
||||
- Add env vars in `get_app_config()` in `core/archipelago/src/api/rpc.rs`
|
||||
|
||||
### 5. Deploy and test
|
||||
|
||||
- Deploy: `./scripts/deploy-to-target.sh --live`
|
||||
- Install from marketplace UI at http://192.168.1.228
|
||||
- Verify it launches and auto-connects to dependencies
|
||||
- Check logs: `sudo podman logs {container-name}`
|
||||
|
||||
### 6. Security review
|
||||
|
||||
- Verify readonly root, dropped caps, non-root user
|
||||
- Check network isolation
|
||||
- No hardcoded secrets
|
||||
@@ -0,0 +1,147 @@
|
||||
---
|
||||
description: E2E encrypted records and group encryption for the Syntropy Institute portal
|
||||
match:
|
||||
- encrypt
|
||||
- decrypt
|
||||
- record
|
||||
- NIP-44
|
||||
- group key
|
||||
- channel
|
||||
- community
|
||||
---
|
||||
|
||||
# Encrypted Records Skill
|
||||
|
||||
## When to Use
|
||||
When working with E2E encrypted client records, community posts, or group key management.
|
||||
|
||||
## NIP-44 Encryption (Practitioner <-> Client)
|
||||
```typescript
|
||||
import { nip44 } from 'nostr-tools';
|
||||
|
||||
// Encrypt record for a specific client
|
||||
function encryptRecord(
|
||||
practitionerSecretKey: Uint8Array,
|
||||
clientPubKey: string,
|
||||
recordData: object
|
||||
): string {
|
||||
const plaintext = JSON.stringify(recordData);
|
||||
const conversationKey = nip44.v2.utils.getConversationKey(practitionerSecretKey, clientPubKey);
|
||||
return nip44.v2.encrypt(plaintext, conversationKey);
|
||||
}
|
||||
|
||||
// Decrypt record (client side)
|
||||
function decryptRecord(
|
||||
clientSecretKey: Uint8Array,
|
||||
practitionerPubKey: string,
|
||||
encryptedData: string
|
||||
): object {
|
||||
const conversationKey = nip44.v2.utils.getConversationKey(clientSecretKey, practitionerPubKey);
|
||||
const plaintext = nip44.v2.decrypt(encryptedData, conversationKey);
|
||||
return JSON.parse(plaintext);
|
||||
}
|
||||
```
|
||||
|
||||
## Group Encryption (Community Channels)
|
||||
|
||||
### Creating a Channel with Group Key
|
||||
```typescript
|
||||
// Generate a symmetric group key for the channel
|
||||
function generateGroupKey(): Uint8Array {
|
||||
return crypto.getRandomValues(new Uint8Array(32));
|
||||
}
|
||||
|
||||
// Encrypt group key for a specific member using NIP-44
|
||||
function wrapGroupKeyForMember(
|
||||
adminSecretKey: Uint8Array,
|
||||
memberPubKey: string,
|
||||
groupKey: Uint8Array
|
||||
): string {
|
||||
const conversationKey = nip44.v2.utils.getConversationKey(adminSecretKey, memberPubKey);
|
||||
return nip44.v2.encrypt(
|
||||
btoa(String.fromCharCode(...groupKey)),
|
||||
conversationKey
|
||||
);
|
||||
}
|
||||
|
||||
// Member unwraps their group key
|
||||
function unwrapGroupKey(
|
||||
memberSecretKey: Uint8Array,
|
||||
adminPubKey: string,
|
||||
wrappedKey: string
|
||||
): Uint8Array {
|
||||
const conversationKey = nip44.v2.utils.getConversationKey(memberSecretKey, adminPubKey);
|
||||
const decoded = nip44.v2.decrypt(wrappedKey, conversationKey);
|
||||
return Uint8Array.from(atob(decoded), c => c.charCodeAt(0));
|
||||
}
|
||||
```
|
||||
|
||||
### Encrypting Community Posts with Group Key
|
||||
```typescript
|
||||
// Encrypt post content with the channel's symmetric group key
|
||||
async function encryptPostWithGroupKey(
|
||||
groupKey: Uint8Array,
|
||||
content: string
|
||||
): Promise<string> {
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw', groupKey, 'AES-GCM', false, ['encrypt']
|
||||
);
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const encoded = new TextEncoder().encode(content);
|
||||
const ciphertext = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv }, key, encoded
|
||||
);
|
||||
const combined = new Uint8Array([...iv, ...new Uint8Array(ciphertext)]);
|
||||
return btoa(String.fromCharCode(...combined));
|
||||
}
|
||||
|
||||
// Decrypt post content
|
||||
async function decryptPostWithGroupKey(
|
||||
groupKey: Uint8Array,
|
||||
encrypted: string
|
||||
): Promise<string> {
|
||||
const combined = Uint8Array.from(atob(encrypted), c => c.charCodeAt(0));
|
||||
const iv = combined.slice(0, 12);
|
||||
const ciphertext = combined.slice(12);
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw', groupKey, 'AES-GCM', false, ['decrypt']
|
||||
);
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv }, key, ciphertext
|
||||
);
|
||||
return new TextDecoder().decode(decrypted);
|
||||
}
|
||||
```
|
||||
|
||||
### Key Rotation (when member removed)
|
||||
```typescript
|
||||
// When a member is removed from a channel:
|
||||
// 1. Generate new group key
|
||||
// 2. Re-wrap for all remaining members
|
||||
// 3. Update ChannelMember.encryptedGroupKey for each
|
||||
// 4. New posts use new key; old posts remain readable with old key
|
||||
// (store key version/epoch on each post)
|
||||
```
|
||||
|
||||
## Record Types
|
||||
```typescript
|
||||
interface ClientRecord {
|
||||
type: 'session_notes' | 'assessment' | 'treatment_plan';
|
||||
title: string;
|
||||
content: string; // Rich text or structured data
|
||||
attachments?: {
|
||||
name: string;
|
||||
mimeType: string;
|
||||
encryptedData: string; // Each file encrypted separately
|
||||
}[];
|
||||
createdAt: string; // ISO timestamp
|
||||
}
|
||||
```
|
||||
|
||||
## Rules
|
||||
- ALL encryption happens client-side — server stores only encrypted blobs
|
||||
- Use NIP-44 v2 (NOT NIP-04 — it's deprecated and has known weaknesses)
|
||||
- Group keys: AES-256-GCM with random IVs
|
||||
- Include key version/epoch on group-encrypted content for rotation support
|
||||
- Never log or expose plaintext content on the server
|
||||
- Media files are encrypted individually before upload
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
name: harden
|
||||
description: Security hardening review and fixes for Archipelago code and infrastructure
|
||||
disable-model-invocation: true
|
||||
allowed-tools: Read, Edit, Write, Glob, Grep, Bash
|
||||
argument-hint: "[area: backend|frontend|containers|scripts|all]"
|
||||
---
|
||||
|
||||
Perform a security hardening pass on $ARGUMENTS (default: all).
|
||||
|
||||
## Backend Hardening (Rust)
|
||||
|
||||
- [ ] No hardcoded credentials — check for Base64-encoded auth strings, passwords in source
|
||||
- [ ] Secrets use `core/security/secrets_manager.rs` — verify encryption is implemented (not plaintext)
|
||||
- [ ] All RPC endpoints validate inputs before processing
|
||||
- [ ] No `unwrap()` on user-supplied data — handle errors gracefully
|
||||
- [ ] Rate limiting on auth endpoints (login, password change)
|
||||
- [ ] Session tokens have proper expiry and rotation
|
||||
- [ ] File permissions: keys at 0o600, dirs at 0o700
|
||||
- [ ] Tracing never logs secrets, passwords, keys, or tokens
|
||||
|
||||
## Frontend Hardening (Vue/TypeScript)
|
||||
|
||||
- [ ] No secrets in source (API keys, passwords, tokens)
|
||||
- [ ] No `eval()` or `innerHTML` with untrusted content
|
||||
- [ ] XSS prevention — sanitize all user inputs
|
||||
- [ ] CSRF protection on state-changing requests
|
||||
- [ ] Credentials use `credentials: 'include'` not localStorage tokens
|
||||
- [ ] No sensitive data in console.log statements
|
||||
|
||||
## Container Hardening
|
||||
|
||||
- [ ] All manifests: `readonly_root: true` (unless documented exception)
|
||||
- [ ] All manifests: capabilities dropped, only required ones added
|
||||
- [ ] All manifests: non-root user (UID > 1000)
|
||||
- [ ] All manifests: `no-new-privileges: true`
|
||||
- [ ] All images pinned to specific versions (no `:latest`)
|
||||
- [ ] Network isolation — no `host` network unless required and documented
|
||||
- [ ] AppArmor profiles defined and enforced
|
||||
|
||||
## Script Hardening
|
||||
|
||||
- [ ] All scripts use `set -euo pipefail`
|
||||
- [ ] No hardcoded passwords (use deploy-config.sh or env vars)
|
||||
- [ ] SSH uses proper key-based auth where possible
|
||||
- [ ] No `chmod 777` or overly permissive permissions
|
||||
- [ ] Temp files use `mktemp` not predictable paths
|
||||
|
||||
Report all findings with file paths and line numbers. Fix issues directly where safe to do so. Flag anything that needs discussion.
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
name: lint
|
||||
description: Run all linters and type checks for the Archipelago project
|
||||
allowed-tools: Bash, Read, Grep
|
||||
argument-hint: "[backend|frontend|all]"
|
||||
---
|
||||
|
||||
Run linters and type-checks for $ARGUMENTS (default: all).
|
||||
|
||||
## Frontend Linting
|
||||
|
||||
```bash
|
||||
cd neode-ui
|
||||
|
||||
# Type check
|
||||
npm run type-check 2>&1
|
||||
|
||||
# Check for any `any` types (should be zero)
|
||||
grep -rn ': any' src/ --include='*.ts' --include='*.vue' | grep -v node_modules | grep -v '.d.ts'
|
||||
|
||||
# Check for inline Tailwind violations (long class strings)
|
||||
grep -rn 'class="[^"]\{100,\}"' src/ --include='*.vue'
|
||||
|
||||
# Check for TODO/FIXME
|
||||
grep -rn 'TODO\|FIXME' src/ --include='*.ts' --include='*.vue'
|
||||
|
||||
# Check for console.log (should be cleaned before production)
|
||||
grep -rn 'console\.\(log\|warn\|error\)' src/ --include='*.ts' --include='*.vue' | wc -l
|
||||
```
|
||||
|
||||
## Backend Linting (on dev server)
|
||||
|
||||
```bash
|
||||
sshpass -p 'EwPDR8q45l0Upx@' ssh -o StrictHostKeyChecking=no archipelago@192.168.1.228 \
|
||||
'source ~/.cargo/env && cd ~/archy/core && cargo clippy --all-targets --all-features 2>&1 && cargo fmt --all -- --check 2>&1'
|
||||
```
|
||||
|
||||
## Script Linting
|
||||
|
||||
```bash
|
||||
# Check for scripts missing set -e
|
||||
for f in scripts/*.sh; do
|
||||
if ! head -5 "$f" | grep -q 'set -e'; then
|
||||
echo "MISSING set -e: $f"
|
||||
fi
|
||||
done
|
||||
|
||||
# Check for hardcoded IPs (should use variables)
|
||||
grep -rn '192\.168\.1\.' scripts/ --include='*.sh' | grep -v deploy-config
|
||||
```
|
||||
|
||||
Report all issues found with severity (critical/warning/info).
|
||||
@@ -0,0 +1,116 @@
|
||||
---
|
||||
description: Nostr keypair authentication for the Syntropy Institute portal (auth only, no relays)
|
||||
match:
|
||||
- nostr
|
||||
- auth
|
||||
- login
|
||||
- keypair
|
||||
- sign
|
||||
- challenge
|
||||
---
|
||||
|
||||
# Nostr Auth Skill
|
||||
|
||||
## When to Use
|
||||
When working with authentication flows, keypair management, or NIP-98 API auth in the portal.
|
||||
|
||||
## Key Principle
|
||||
Nostr is used ONLY for cryptographic authentication. No relay connections. No event publishing. Just keypairs and signatures.
|
||||
|
||||
## Keypair Generation (Easy Mode)
|
||||
```typescript
|
||||
import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools';
|
||||
|
||||
// Generate new identity
|
||||
const secretKey = generateSecretKey(); // Uint8Array
|
||||
const publicKey = getPublicKey(secretKey); // hex string
|
||||
|
||||
// Encode for display (only when user requests it)
|
||||
const npub = nip19.npubEncode(publicKey);
|
||||
const nsec = nip19.nsecEncode(secretKey);
|
||||
```
|
||||
|
||||
## Private Key Encryption (for localStorage)
|
||||
```typescript
|
||||
// Encrypt private key with user's passphrase before storing
|
||||
async function encryptPrivateKey(secretKey: Uint8Array, passphrase: string): Promise<string> {
|
||||
const encoder = new TextEncoder();
|
||||
const keyMaterial = await crypto.subtle.importKey(
|
||||
'raw', encoder.encode(passphrase), 'PBKDF2', false, ['deriveKey']
|
||||
);
|
||||
const salt = crypto.getRandomValues(new Uint8Array(16));
|
||||
const derivedKey = await crypto.subtle.deriveKey(
|
||||
{ name: 'PBKDF2', salt, iterations: 600000, hash: 'SHA-256' },
|
||||
keyMaterial, { name: 'AES-GCM', length: 256 }, false, ['encrypt']
|
||||
);
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv }, derivedKey, secretKey
|
||||
);
|
||||
// Return salt + iv + ciphertext as base64
|
||||
const combined = new Uint8Array([...salt, ...iv, ...new Uint8Array(encrypted)]);
|
||||
return btoa(String.fromCharCode(...combined));
|
||||
}
|
||||
```
|
||||
|
||||
## NIP-07 Detection (Nostr Native)
|
||||
```typescript
|
||||
// Check for browser extension
|
||||
function hasNostrExtension(): boolean {
|
||||
return typeof window !== 'undefined' && 'nostr' in window;
|
||||
}
|
||||
|
||||
// Sign with extension
|
||||
async function signWithExtension(event: object): Promise<object> {
|
||||
return await (window as any).nostr.signEvent(event);
|
||||
}
|
||||
|
||||
// Get public key from extension
|
||||
async function getExtensionPubkey(): Promise<string> {
|
||||
return await (window as any).nostr.getPublicKey();
|
||||
}
|
||||
```
|
||||
|
||||
## Challenge-Response Auth (NIP-98 style)
|
||||
```typescript
|
||||
import { finalizeEvent, verifyEvent } from 'nostr-tools';
|
||||
|
||||
// Client: Sign auth challenge
|
||||
function createAuthEvent(secretKey: Uint8Array, url: string, method: string) {
|
||||
const event = finalizeEvent({
|
||||
kind: 27235, // NIP-98 HTTP Auth
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
['u', url],
|
||||
['method', method],
|
||||
],
|
||||
content: '',
|
||||
}, secretKey);
|
||||
return event;
|
||||
}
|
||||
|
||||
// Send as Authorization header:
|
||||
// Authorization: Nostr <base64-encoded-event-json>
|
||||
|
||||
// Server: Verify auth event
|
||||
function verifyAuthEvent(event: any, expectedUrl: string, expectedMethod: string): boolean {
|
||||
if (!verifyEvent(event)) return false;
|
||||
if (event.kind !== 27235) return false;
|
||||
const urlTag = event.tags.find((t: string[]) => t[0] === 'u');
|
||||
const methodTag = event.tags.find((t: string[]) => t[0] === 'method');
|
||||
if (urlTag?.[1] !== expectedUrl) return false;
|
||||
if (methodTag?.[1] !== expectedMethod) return false;
|
||||
// Check timestamp is within 60 seconds
|
||||
if (Math.abs(Date.now() / 1000 - event.created_at) > 60) return false;
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
## Rules
|
||||
- NEVER store private keys on the server
|
||||
- NEVER connect to any Nostr relay
|
||||
- NEVER transmit private keys over the network
|
||||
- ALWAYS encrypt private keys before storing in localStorage
|
||||
- Use PBKDF2 with at least 600,000 iterations for key derivation
|
||||
- Auth events expire after 60 seconds
|
||||
- Server only stores npub (public key)
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
name: overnight
|
||||
description: Commit, branch, and start the overnight automation loop
|
||||
disable-model-invocation: true
|
||||
allowed-tools: Bash(*), Read, Write, Edit, Glob, Grep
|
||||
---
|
||||
|
||||
Prepare and launch the overnight automation loop. Do ALL steps in order, stopping on any failure:
|
||||
|
||||
1. Stage and commit all uncommitted changes: `git add -A && git commit -m "chore: pre-overnight snapshot"` (skip if working tree is clean)
|
||||
2. Push current branch to origin
|
||||
3. Get today's date as YYYY-MM-DD. Check if `overnight/$DATE` branch exists:
|
||||
- If yes: `git checkout overnight/$DATE`
|
||||
- If no: run `./loop/prepare.sh`
|
||||
4. Verify `loop/plan.md` has unchecked tasks (`grep -c '^\- \[ \]' loop/plan.md`)
|
||||
5. Commit plan files if modified: `git add loop/plan.md loop/prompt.md && git commit -m "chore: overnight plan $DATE"` (skip if clean)
|
||||
6. Push: `git push -u origin overnight/$DATE`
|
||||
7. Start the loop: run `caffeinate -i ./loop/loop.sh` with `run_in_background: true`
|
||||
8. Report: branch name, number of tasks, and confirm the loop is running in background
|
||||
@@ -0,0 +1,102 @@
|
||||
---
|
||||
name: pwa-icon-cache-fix
|
||||
description: Use when the user reports a PWA icon not updating, stale PWA icon, wrong icon after install, or any PWA caching issue. Also applies when changing PWA icons in a Vite + vite-plugin-pwa project.
|
||||
version: 2.0.0
|
||||
---
|
||||
|
||||
# PWA Icon Cache Fix
|
||||
|
||||
## Problem
|
||||
|
||||
PWA icons are cached at FOUR independent layers:
|
||||
1. **Service worker cache** (Workbox precache)
|
||||
2. **Browser HTTP cache**
|
||||
3. **Browser manifest resources** (Chromium stores resized icons in its profile data, keyed by a permanent extension ID tied to the origin — NEVER re-fetched even after uninstall/reinstall)
|
||||
4. **macOS .app bundle** (`.icns` file baked into the `.app` in `~/Applications/`)
|
||||
|
||||
Query string cache busting (`?v=2`) and uninstall/reinstall do NOT fix this. Chromium reuses the same extension ID for the same origin, so it keeps the old cached icons.
|
||||
|
||||
## Fix Steps
|
||||
|
||||
### 1. Verify icon files on disk and server are correct
|
||||
|
||||
```bash
|
||||
# Visual check
|
||||
Read packages/app/public/pwa-192x192.png
|
||||
Read packages/app/public/pwa-512x512.png
|
||||
|
||||
# Hash match check
|
||||
curl -s http://localhost:5173/pwa-192x192.png | md5
|
||||
md5 -q packages/app/public/pwa-192x192.png
|
||||
```
|
||||
|
||||
### 2. Find the PWA's Chromium extension ID
|
||||
|
||||
Read the installed `.app` bundle's `Info.plist` to get the `CrAppModeShortcutID`:
|
||||
|
||||
```bash
|
||||
plutil -p "~/Applications/Brave Browser Apps.localized/AIUI.app/Contents/Info.plist" | grep CrAppModeShortcutID
|
||||
```
|
||||
|
||||
This returns an ID like `idemibpphagihbobmgmaojhjfidlfpdl`.
|
||||
|
||||
### 3. Overwrite the cached icons in browser profile
|
||||
|
||||
Chromium stores resized icons at:
|
||||
`~/Library/Application Support/BraveSoftware/Brave-Browser/Default/Web Applications/Manifest Resources/{ID}/Icons/`
|
||||
|
||||
Overwrite every size using `sips`:
|
||||
|
||||
```bash
|
||||
ICON_DIR="~/Library/Application Support/BraveSoftware/Brave-Browser/Default/Web Applications/Manifest Resources/{ID}/Icons"
|
||||
SRC="packages/app/public/pwa-512x512.png"
|
||||
for size in 32 48 64 96 128 192 256 512; do
|
||||
sips -z $size $size "$SRC" --out "${ICON_DIR}/${size}.png"
|
||||
done
|
||||
```
|
||||
|
||||
### 4. Rebuild the macOS .icns in the .app bundle
|
||||
|
||||
```bash
|
||||
ICONSET="/tmp/aiui.iconset"
|
||||
mkdir -p "$ICONSET"
|
||||
SRC="packages/app/public/pwa-512x512.png"
|
||||
sips -z 16 16 "$SRC" --out "$ICONSET/icon_16x16.png"
|
||||
sips -z 32 32 "$SRC" --out "$ICONSET/icon_16x16@2x.png"
|
||||
sips -z 32 32 "$SRC" --out "$ICONSET/icon_32x32.png"
|
||||
sips -z 64 64 "$SRC" --out "$ICONSET/icon_32x32@2x.png"
|
||||
sips -z 128 128 "$SRC" --out "$ICONSET/icon_128x128.png"
|
||||
sips -z 256 256 "$SRC" --out "$ICONSET/icon_128x128@2x.png"
|
||||
sips -z 256 256 "$SRC" --out "$ICONSET/icon_256x256.png"
|
||||
sips -z 512 512 "$SRC" --out "$ICONSET/icon_256x256@2x.png"
|
||||
sips -z 512 512 "$SRC" --out "$ICONSET/icon_512x512.png"
|
||||
cp "$SRC" "$ICONSET/icon_512x512@2x.png"
|
||||
iconutil -c icns "$ICONSET" -o "~/Applications/Brave Browser Apps.localized/AIUI.app/Contents/Resources/app.icns"
|
||||
```
|
||||
|
||||
### 5. Flush macOS icon cache
|
||||
|
||||
```bash
|
||||
touch "~/Applications/Brave Browser Apps.localized/AIUI.app"
|
||||
killall Finder
|
||||
killall Dock
|
||||
```
|
||||
|
||||
### 6. Bump PWA_CACHE_VERSION in main.ts
|
||||
|
||||
Increment the `PWA_CACHE_VERSION` constant — this nukes all SW caches on next page load for web-layer caching.
|
||||
|
||||
### 7. Delete stale build artifacts
|
||||
|
||||
Remove old `dist/` and `dev-dist/` SW/manifest files.
|
||||
|
||||
## Browser-Specific Paths
|
||||
|
||||
- **Brave**: `~/Library/Application Support/BraveSoftware/Brave-Browser/Default/Web Applications/`
|
||||
- **Chrome**: `~/Library/Application Support/Google/Chrome/Default/Web Applications/`
|
||||
- **PWA apps (Brave)**: `~/Applications/Brave Browser Apps.localized/`
|
||||
- **PWA apps (Chrome)**: `~/Applications/Chrome Apps.localized/`
|
||||
|
||||
## Key Insight
|
||||
|
||||
Chromium assigns a permanent extension ID per origin (e.g., `localhost:5173`). This ID persists across uninstall/reinstall. The icon cache in `Manifest Resources/{ID}/Icons/` is populated ONCE and never refreshed from the manifest. The only fix is to overwrite the files directly on disk.
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: refactor
|
||||
description: Refactor code for quality, maintainability, and adherence to project standards
|
||||
disable-model-invocation: true
|
||||
allowed-tools: Read, Edit, Write, Glob, Grep, Bash
|
||||
argument-hint: "[file-or-area]"
|
||||
---
|
||||
|
||||
Refactor the specified code ($ARGUMENTS) following Archipelago coding standards.
|
||||
|
||||
## Checklist
|
||||
|
||||
### Rust Backend
|
||||
- [ ] No `unwrap()` or `expect()` — use `?` operator with context
|
||||
- [ ] Replace `#[allow(dead_code)]` — either use it or remove it
|
||||
- [ ] Functions under 50 lines, single responsibility
|
||||
- [ ] Custom error types per module with `thiserror`
|
||||
- [ ] `tracing` for logging — no `println!` or secrets in logs
|
||||
- [ ] Split files over 500 lines into focused modules
|
||||
- [ ] Run `cargo clippy --all-targets --all-features` mentally and fix issues
|
||||
|
||||
### Vue Frontend
|
||||
- [ ] Extract ALL inline Tailwind to global classes in `neode-ui/src/style.css`
|
||||
- [ ] Use semantic class names: `.glass-card`, `.info-card`, `.glass-button`, `.path-option-card`
|
||||
- [ ] Replace ALL `.gradient-button` with `.glass-button` (gradient buttons are BANNED)
|
||||
- [ ] Replace ALL `.gradient-card` / `.gradient-card-dark` with `.glass-card` or `.path-option-card`
|
||||
- [ ] Settings.vue is the gold standard — all screens should match its patterns
|
||||
- [ ] Replace `any` types with proper interfaces or `unknown`
|
||||
- [ ] Ensure `<script setup lang="ts">` on all components
|
||||
- [ ] Remove dead code (unused imports, components like HelloWorld.vue)
|
||||
- [ ] Remove all `TODO`/`FIXME` — fix now or create GitHub issues
|
||||
- [ ] Consolidate `console.log` calls to use a logging utility
|
||||
- [ ] Split views over 800 LOC into sub-components
|
||||
|
||||
### General
|
||||
- [ ] No hardcoded paths (`/Users/dorian/...`)
|
||||
- [ ] No hardcoded credentials — use env vars or secrets manager
|
||||
- [ ] Comment WHY not WHAT
|
||||
- [ ] Remove commented-out code entirely
|
||||
|
||||
After refactoring, verify the code still compiles/type-checks. For frontend: `cd neode-ui && npm run type-check`. Do NOT deploy — leave that to `/deploy`.
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
name: test
|
||||
description: Run tests or create test coverage for Archipelago
|
||||
disable-model-invocation: true
|
||||
allowed-tools: Read, Edit, Write, Glob, Grep, Bash
|
||||
argument-hint: "[area: backend|frontend|all] or [specific-file]"
|
||||
---
|
||||
|
||||
Run or create tests for $ARGUMENTS.
|
||||
|
||||
## Backend Testing (Rust)
|
||||
|
||||
### Run existing tests
|
||||
```bash
|
||||
# On dev server (never build Rust on macOS)
|
||||
sshpass -p 'EwPDR8q45l0Upx@' ssh -o StrictHostKeyChecking=no archipelago@192.168.1.228 \
|
||||
'source ~/.cargo/env && cd ~/archy/core && cargo test --all-features 2>&1'
|
||||
```
|
||||
|
||||
### Creating new tests
|
||||
- Place unit tests in the same file with `#[cfg(test)]` module
|
||||
- Place integration tests in `core/{crate}/tests/`
|
||||
- Use `#[tokio::test]` for async tests
|
||||
- Mock external dependencies (filesystem, network, Podman)
|
||||
- Test error cases, not just happy paths
|
||||
- Aim for >80% coverage on core logic
|
||||
|
||||
### Priority areas needing tests
|
||||
1. RPC endpoint handlers (core/archipelago/src/api/)
|
||||
2. Manifest parsing (core/container/src/manifest.rs)
|
||||
3. Dependency resolver (core/container/src/dependency_resolver.rs)
|
||||
4. Auth flows (core/archipelago/src/auth.rs)
|
||||
5. Secrets manager (core/security/src/secrets_manager.rs)
|
||||
6. Port allocation (core/container/src/port_manager.rs)
|
||||
|
||||
## Frontend Testing (Vue/TypeScript)
|
||||
|
||||
### Setup (if not already configured)
|
||||
Ensure vitest is configured in `neode-ui/`:
|
||||
```bash
|
||||
cd neode-ui && npm run test 2>&1 || echo "No test script configured"
|
||||
```
|
||||
|
||||
### Creating new tests
|
||||
- Use Vitest + @vue/test-utils
|
||||
- Place tests in `neode-ui/src/__tests__/` or co-located `*.test.ts`
|
||||
- Test stores (Pinia) with `createTestingPinia()`
|
||||
- Test API clients with mocked fetch
|
||||
- Test component rendering and interactions
|
||||
- Test routing guards
|
||||
|
||||
### Priority areas needing tests
|
||||
1. Pinia stores (app.ts, container.ts, appLauncher.ts)
|
||||
2. RPC client (api/rpc-client.ts) — error handling, retry logic
|
||||
3. WebSocket client (api/websocket.ts) — reconnection
|
||||
4. Router guards — auth flow, session timeout
|
||||
5. Key components — ContainerStatus, SpotlightSearch
|
||||
|
||||
Report test results and any new tests created.
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
name: ux-review
|
||||
description: Review UI components against Archipelago glassmorphism design standards and UX conventions
|
||||
disable-model-invocation: true
|
||||
allowed-tools: Read, Glob, Grep, Edit, Write
|
||||
argument-hint: "[component-or-view-name]"
|
||||
---
|
||||
|
||||
Review the UI of $ARGUMENTS against Archipelago's glassmorphism design system and UX standards.
|
||||
|
||||
## Design System Compliance
|
||||
|
||||
### Glass Classes (must use global classes from style.css)
|
||||
- [ ] Section containers use `.path-option-card cursor-default px-6 py-6` (Settings-style sections)
|
||||
- [ ] Content containers/modals use `.glass-card`
|
||||
- [ ] Interactive selectable cards use `.path-option-card` (with hover)
|
||||
- [ ] Status displays use `.info-card` (no hover effects)
|
||||
- [ ] ALL buttons use `.glass-button` — NEVER `.gradient-button` (BANNED)
|
||||
- [ ] Large primary actions use `.path-action-button`
|
||||
- [ ] Info sub-cards use `bg-black/20 rounded-xl border border-white/10`
|
||||
- [ ] Info rows use `bg-white/5 rounded-lg` pattern
|
||||
- [ ] Action buttons in info sections use `.info-card-button`
|
||||
|
||||
### BANNED — Flag These as Violations
|
||||
- [ ] No `.gradient-button` anywhere (replace with `.glass-button`)
|
||||
- [ ] No `.gradient-card` / `.gradient-card-dark` (replace with `.glass-card` or `.path-option-card`)
|
||||
|
||||
### NO Inline Tailwind
|
||||
- [ ] Check for long `class="..."` strings with layout/color utilities
|
||||
- [ ] Extract to semantic classes in `neode-ui/src/style.css`
|
||||
- [ ] Name classes semantically: `.app-card`, `.status-badge`, `.nav-item`
|
||||
|
||||
### Color Compliance
|
||||
- [ ] Primary text: `text-white/90` (not `text-white` or arbitrary opacity)
|
||||
- [ ] Muted text: `text-white/60` to `text-white/70`
|
||||
- [ ] Backgrounds: `rgba(0,0,0,0.60)` with `backdrop-filter: blur(24px)`
|
||||
- [ ] Borders: `rgba(255,255,255,0.18)` standard
|
||||
- [ ] Status colors: green=#4ade80, red=#ef4444, yellow=#facc15, blue=#3b82f6, orange=#fb923c
|
||||
|
||||
### Typography
|
||||
- [ ] Font: Avenir Next (body), Montserrat (headings via `font-archipelago`)
|
||||
- [ ] H1: text-3xl font-bold, H2: text-2xl font-semibold, H3: text-xl font-semibold
|
||||
- [ ] Body: text-base, Small: text-sm, Labels: text-xs
|
||||
|
||||
### Interaction States
|
||||
- [ ] Hover: `translateY(-2px)` lift + background brighten + enhanced shadow
|
||||
- [ ] Active: `translateY(1px)` press
|
||||
- [ ] Selected: brighter background + glow shadow + enhanced gradient border
|
||||
- [ ] Disabled: reduced opacity (~50%), no pointer events
|
||||
- [ ] Loading: spinner SVG + descriptive text, button disabled
|
||||
- [ ] Focus-visible: soft blue glow `rgba(120, 180, 255, 0.2)`
|
||||
|
||||
### Transitions
|
||||
- [ ] Standard: `all 0.3s ease`
|
||||
- [ ] All interactive elements have transitions (no jarring state changes)
|
||||
- [ ] Respect `prefers-reduced-motion`
|
||||
|
||||
### Spacing
|
||||
- [ ] 4px grid system (p-1=4px, p-2=8px, p-3=12px, p-4=16px)
|
||||
- [ ] 16px default padding on cards
|
||||
- [ ] Consistent gap values between grid items
|
||||
|
||||
### Responsive
|
||||
- [ ] Mobile: single column, reduced padding, touch targets >= 44x44px
|
||||
- [ ] Tablet (md:): two columns
|
||||
- [ ] Desktop (lg:): three columns, full effects
|
||||
|
||||
### Accessibility
|
||||
- [ ] Semantic HTML (`<button>`, `<nav>`, `<main>`, not div soup)
|
||||
- [ ] ARIA labels on icon-only buttons
|
||||
- [ ] Keyboard navigable (Tab order, Enter to activate, Esc to close)
|
||||
- [ ] Color contrast WCAG AA (4.5:1 normal text, 3:1 large)
|
||||
- [ ] Images have alt text (decorative: `alt=""`)
|
||||
|
||||
### Icons
|
||||
- [ ] Stroke-based SVGs, stroke-width 2.5 default
|
||||
- [ ] Color: `text-white/85` default, `text-white` on hover
|
||||
- [ ] Drop-shadow filter applied on interactive icons
|
||||
- [ ] Size: w-5 h-5 standard, w-4 h-4 small
|
||||
|
||||
## Service UI Review (if reviewing docker/*-ui/)
|
||||
- [ ] Uses `.glass-card` for main sections
|
||||
- [ ] Uses `.info-card` for status (no hover)
|
||||
- [ ] Uses `.info-card-button` for actions (with hover)
|
||||
- [ ] Uses `bg-white/5` for info rows
|
||||
- [ ] Header: logo + title + description + status
|
||||
- [ ] Background image loads correctly
|
||||
- [ ] Mobile responsive
|
||||
|
||||
Report violations with file paths and specific fixes.
|
||||
Reference in New Issue
Block a user