Merge #c1156d20: Terminal developer environment, resumable TUI, and Arc…
Terminal developer environment, resumable TUI, and Archipelago skills nostr:nevent1qqsvz9tdypdv9cmtjmsz6azv8yujk0xezwv0j4wpa57qnk60nfyj28gpz3mhxue69uhhyetvv9ujumn8d96zuer9wcffu24e PR-Author: Personal nostr:npub1w3sqdkrhn0gyuvsex32effzgnfpyde6qrrc4u467flg5e9txh4wsfn5vjg PR description: Adds the authenticated resumable terminal session backend, xterm-based terminal UI with bounded scrolling and app-style controls, tmux key translation, Codex/node bootstrap, preloaded Archipelago skills, developer app starter, and UAT runbook. Validation: npm run build:production; cargo check -p archipelago; scripts/tests/archy-developer-setup-test.sh. Framework UAT health and unauthenticated terminal endpoint checks pass.
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
---
|
||||
name: archipelago-app
|
||||
description: Build, package, test, and update a manifest-driven Archipelago app using the real app developer contract, rootless runtime, and lifecycle acceptance flow.
|
||||
metadata:
|
||||
short-description: Build a real Archipelago app
|
||||
---
|
||||
|
||||
# Archipelago app development
|
||||
|
||||
Use this skill when creating or changing an Archipelago app, manifest, container
|
||||
build, app integration, credentials, signer flow, or app preview. Always load
|
||||
`archipelago-design` for newly authored UI.
|
||||
|
||||
Read [app-contract.md](references/app-contract.md),
|
||||
`docs/app-developer-guide.md`, and `docs/app-manifest-spec.md` before coding.
|
||||
|
||||
## Required loop
|
||||
|
||||
1. Create a dedicated worktree or project directory and choose the smallest
|
||||
useful app scope. Prefer the maintained starter and its pinned dependencies.
|
||||
2. Implement the app as a manifest, rootless container, persistent data path,
|
||||
truthful health/readiness check, declared interface, and tests. Never add a
|
||||
per-app Rust installer or rootful/Docker-socket shortcut.
|
||||
3. Use generated secrets or declared secret files; never put credentials in a
|
||||
manifest, image, logs, URL, or frontend bundle. Keep production wallets and
|
||||
app databases outside development fixtures.
|
||||
4. Validate the manifest and build context, then run the app through install,
|
||||
start, stop, restart, manager restart, uninstall with data preservation, and
|
||||
reinstall. Verify the real My Apps/Services launch path, not only a direct
|
||||
port.
|
||||
5. For UI, exercise standalone and embedded modes at 320, 390, 768, and 1440px
|
||||
plus phone landscape, keyboard navigation, loading/empty/error/retry/success,
|
||||
safe areas, and reduced motion.
|
||||
6. Report source, disposable-node, actual-node, and release-artifact evidence
|
||||
separately. Catalog publication is a separate request.
|
||||
|
||||
Important runtime facts:
|
||||
|
||||
- `/opt/archipelago/apps` is rebuilt from the runtime payload at backend start;
|
||||
staging only there will be lost. Follow the developer guide's payload path.
|
||||
- Signed catalog entries take precedence over disk manifests for catalog apps.
|
||||
- Installed does not mean ready or launchable; use health and readiness evidence.
|
||||
- App interfaces describe the service behind the gate. Do not hard-code a node
|
||||
IP, scheme, app path, or host frame URL into app code.
|
||||
|
||||
## References
|
||||
|
||||
- [app contract and acceptance](references/app-contract.md)
|
||||
- [design routing](../archipelago-design/SKILL.md)
|
||||
@@ -0,0 +1,7 @@
|
||||
interface:
|
||||
display_name: "Archipelago app"
|
||||
short_description: "Build a real Archipelago app"
|
||||
brand_color: "#F7931A"
|
||||
default_prompt: "Use $archipelago-app to build and validate this Archipelago app."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,17 @@
|
||||
# App contract
|
||||
|
||||
Start with `docs/app-developer-guide.md` and `docs/app-manifest-spec.md`; those
|
||||
files are authoritative for fields and launch behavior. Validate with:
|
||||
|
||||
```bash
|
||||
./scripts/validate-app-manifest.sh apps/<id>/manifest.yml
|
||||
python3 scripts/generate-app-catalog.py
|
||||
python3 scripts/check-app-catalog-drift.py --release --strict
|
||||
```
|
||||
|
||||
Use pinned image versions, read-only root, no-new-privileges, minimal
|
||||
capabilities, rootless Podman, declared persistent data under
|
||||
`/var/lib/archipelago/<id>`, health checks, generated/declared secrets, and
|
||||
truthful interfaces. Test install/start/stop/restart/manager-restart/uninstall
|
||||
with data preservation/reinstall on a disposable node. Do not replace the
|
||||
signed catalog to make a local test app appear.
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: archipelago-design
|
||||
description: Create or change Archipelago UI using the shared semantic tokens, components, responsive patterns, accessibility states, and embedded/standalone host contract.
|
||||
metadata:
|
||||
short-description: Keep Archipelago UI consistent
|
||||
---
|
||||
|
||||
# Archipelago design system
|
||||
|
||||
Use this skill for any new or changed Archipelago UI, including app screens,
|
||||
Terminal, setup flows, dialogs, dashboards, and app wrappers. Read
|
||||
[design-contract.md](references/design-contract.md) before implementation.
|
||||
|
||||
## Rules
|
||||
|
||||
- Find and reuse the closest shared component and pattern before creating one.
|
||||
- Use semantic `--archy-*` tokens and the pinned kit version. A bespoke color,
|
||||
font, radius, shadow, or z-index needs a documented reason.
|
||||
- Preserve the dark baseline, readable surfaces, 4px spacing rhythm, bottom
|
||||
action placement, 44px touch targets, visible focus, and safe-area behavior.
|
||||
- Keep terminal/editor/tmux keys inside the terminal focus boundary; generic
|
||||
modal Escape/arrow handlers must not consume them.
|
||||
- Implement loading, empty, offline, denied, validation-error, retry, disabled,
|
||||
and confirmed-success states. Status color must have text or icon support.
|
||||
- Test standalone and embedded modes. Embedded apps do not duplicate the host
|
||||
wallpaper or navigation and must work without a host handshake.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Read the reference screen and source; choose the matching component/pattern.
|
||||
2. Build with the shared kit and local assets, not runtime dashboard CSS or CDN
|
||||
fonts. Keep host RPC, signer, and credential bridges behind typed adapters.
|
||||
3. Render the gallery/preview at required viewports and inspect screenshots and
|
||||
keyboard behavior. Check contrast on the real composited surface.
|
||||
4. Record deliberate exceptions and update the kit only when a reusable need is
|
||||
demonstrated. Do not silently accept visual-diff changes.
|
||||
|
||||
## References
|
||||
|
||||
- [design contract](references/design-contract.md)
|
||||
- [existing component map](references/component-map.md)
|
||||
@@ -0,0 +1,7 @@
|
||||
interface:
|
||||
display_name: "Archipelago design"
|
||||
short_description: "Keep Archipelago UI consistent"
|
||||
brand_color: "#F7931A"
|
||||
default_prompt: "Use $archipelago-design to implement this UI in Archipelago's design system."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,15 @@
|
||||
# Existing component map
|
||||
|
||||
Reuse these first:
|
||||
|
||||
- Structure: `BaseModal.vue`, `BackButton.vue`, `EmptyState.vue`,
|
||||
`SkeletonCard.vue`.
|
||||
- Controls: `ToggleSwitch.vue`, `PasswordRevealInput.vue`,
|
||||
`AppSearchField.vue`, `CopyButton.vue`.
|
||||
- Feedback: `ToastStack.vue`, `ContainerStatus.vue`, existing upload/progress
|
||||
components.
|
||||
- Completion: `PaymentSuccessPane.vue`, `IdentitySuccessPane.vue`.
|
||||
- App flows: `AppLauncherOverlay.vue`, `AppCredentialInterstitial.vue`.
|
||||
|
||||
The terminal must have an explicit keyboard ownership boundary and must not be
|
||||
wrapped in the generic arrow-key modal behavior without adapting it.
|
||||
@@ -0,0 +1,18 @@
|
||||
# Design contract
|
||||
|
||||
The current baseline is defined by `neode-ui/src/style.css`,
|
||||
`neode-ui/tailwind.config.js`, `BaseModal.vue`, `AppSearchField.vue`,
|
||||
`EmptyState.vue`, `SkeletonCard.vue`, and `PaymentSuccessPane.vue`. Preserve the
|
||||
dark canvas, glass-card surface, 4px spacing scale, semantic orange accent,
|
||||
local licensed fonts, bottom card actions, 40px desktop/52px mobile search,
|
||||
44px touch targets, visible focus, dynamic viewport, safe-area and embedded
|
||||
canvas rules while the shared kit is extracted.
|
||||
|
||||
Use semantic tokens, not raw values. New controls must document keyboard,
|
||||
focus, disabled, loading, validation, error, retry, success, responsive, and
|
||||
reduced-motion behavior. No essential action may be hover-only. Use the existing
|
||||
dialog footer/content split and restore focus after close.
|
||||
|
||||
Visual acceptance covers 320/390/768/1440px, phone landscape, enlarged text,
|
||||
standalone/embedded modes, dark native controls, no-blur fallback, and actual
|
||||
Companion WebView behavior where applicable.
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: archipelago
|
||||
description: Configure, troubleshoot, and safely modify an Archipelago node or its developer environment using the repository's typed operations, ownership rules, and recovery workflow.
|
||||
metadata:
|
||||
short-description: Work safely on Archipelago systems
|
||||
---
|
||||
|
||||
# Archipelago system work
|
||||
|
||||
Use this skill for node configuration, terminal/developer setup, service
|
||||
diagnosis, network and SSH work, supported app operations, and system changes.
|
||||
For app implementation load `archipelago-app`; for any new or changed UI also
|
||||
load `archipelago-design`.
|
||||
|
||||
Read [system-map.md](references/system-map.md) before acting. Read `AGENTS.md`
|
||||
and the relevant project documentation in the current checkout.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Identify the node, repository/worktree, owner, and whether the request is
|
||||
planning, source work, a disposable test, or a live-node operation.
|
||||
2. Inspect current state before changing it. Prefer `archy`/Archipelago RPC
|
||||
operations and existing scripts over direct edits or ad-hoc service commands.
|
||||
3. Preserve wallets, app data, credentials, user uninstall decisions, and
|
||||
unrelated services. Back up only the scoped state before a mutation.
|
||||
4. Make the smallest reversible change. Keep generated state separate from
|
||||
user overrides; never edit generated or packaged files when an owned source
|
||||
or managed override exists.
|
||||
5. Verify the actual result and report source tests, disposable integration,
|
||||
live-node acceptance, and packaged-artifact checks separately.
|
||||
|
||||
For repository work, use a dedicated worktree and focused branch. For backend
|
||||
unit tests use `scripts/test-backend-isolated.sh`; do not run unrestricted
|
||||
`cargo test` on a node with installed apps. Do not publish OTA, ISO, catalog,
|
||||
or Git mirrors from this skill unless that publication is explicitly requested
|
||||
and every release gate is satisfied.
|
||||
|
||||
## Terminal and sessions
|
||||
|
||||
Treat terminal close as detach. Resume the existing named session; never create
|
||||
a duplicate shell or replay a lost command. A reboot can restore workspace and
|
||||
Codex conversation metadata but cannot restore the old process. Distinguish
|
||||
running, detached, ended, and interrupted states in user-facing output.
|
||||
|
||||
Keep credentials, wallet material, terminal output, and command contents out of
|
||||
diagnostics and support bundles. A terminal is a privileged capability: verify
|
||||
the authenticated owner, origin, attachment grant, and account boundary before
|
||||
any PTY bytes flow.
|
||||
|
||||
## References
|
||||
|
||||
- [system map and safe recipes](references/system-map.md)
|
||||
- [app and design routing](references/routing.md)
|
||||
@@ -0,0 +1,7 @@
|
||||
interface:
|
||||
display_name: "Archipelago system"
|
||||
short_description: "Work safely on Archipelago systems"
|
||||
brand_color: "#F7931A"
|
||||
default_prompt: "Use $archipelago to inspect and safely change this Archipelago node."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,10 @@
|
||||
# Skill routing
|
||||
|
||||
Load `archipelago-app` for manifests, containers, app previews, lifecycle,
|
||||
credentials, signer integration, or app packaging. Load `archipelago-design` for
|
||||
any newly authored or changed UI. For backend-only work, use only the system
|
||||
skill and the relevant project docs.
|
||||
|
||||
Planning stays planning. A source change, node mutation, deployment, or
|
||||
publication requires that explicit scope from the user. A skill supplies
|
||||
workflow knowledge; it does not grant additional privileges.
|
||||
@@ -0,0 +1,19 @@
|
||||
# System map and safe recipes
|
||||
|
||||
The native backend is `core/archipelago`; the Vue dashboard is `neode-ui`; ISO
|
||||
and first-boot material lives under `image-recipe` and `scripts`; app manifests
|
||||
are under `apps/<id>/manifest.yml`. Read `CLAUDE.md`, `AGENTS.md`, and the current
|
||||
release checklist before release work.
|
||||
|
||||
Use the existing typed RPC and orchestration layers for app lifecycle, network,
|
||||
wallet, identity, and service operations. Inspect a matching handler before
|
||||
adding an endpoint. Preserve the existing session cookie, CSRF, Origin, and
|
||||
app-gate protections.
|
||||
|
||||
For source tests, run frontend checks from `neode-ui`. Backend unit tests run
|
||||
only through `scripts/test-backend-isolated.sh`; live host checks must be named
|
||||
and explicit. Do not touch real wallet/payment/channel state for a test fixture.
|
||||
|
||||
Developer changes belong in a dedicated worktree. Keep generated catalog,
|
||||
runtime payload, release artifacts, and local node state separate until the
|
||||
request explicitly includes packaging or deployment.
|
||||
@@ -14,6 +14,7 @@ mod remote_input;
|
||||
mod remote_relay;
|
||||
mod rental_playback;
|
||||
mod routstr_proxy;
|
||||
mod terminal;
|
||||
mod websocket;
|
||||
|
||||
use crate::api::rpc::RpcHandler;
|
||||
@@ -426,6 +427,16 @@ impl ApiHandler {
|
||||
.await;
|
||||
}
|
||||
|
||||
// Owner terminal attachment — the browser socket is disposable; the
|
||||
// authenticated tmux session survives reconnects and browser closes.
|
||||
if method == Method::GET && path == "/ws/terminal" {
|
||||
if !self.is_authenticated(req.headers()).await {
|
||||
tracing::warn!("401 WebSocket /ws/terminal — session invalid or missing");
|
||||
return Ok(Self::unauthorized());
|
||||
}
|
||||
return Self::handle_terminal_websocket(req).await;
|
||||
}
|
||||
|
||||
// Remote input WebSocket — companion app sends keyboard/mouse events
|
||||
if method == Method::GET && path == "/ws/remote-input" {
|
||||
if !self.is_authenticated(req.headers()).await {
|
||||
@@ -544,6 +555,15 @@ impl ApiHandler {
|
||||
.unwrap())
|
||||
}
|
||||
|
||||
(Method::GET, "/api/terminal/sessions") => {
|
||||
if !self.is_authenticated(&headers).await { return Ok(Self::unauthorized()); }
|
||||
terminal::list_response().await
|
||||
}
|
||||
(Method::POST, "/api/terminal/sessions") => {
|
||||
if !self.is_authenticated(&headers).await { return Ok(Self::unauthorized()); }
|
||||
terminal::create(&body_bytes).await
|
||||
}
|
||||
|
||||
// Node message — P2P endpoint (authenticated by source validation, not cookie)
|
||||
(Method::POST, "/archipelago/node-message") => {
|
||||
Self::handle_node_message(body_bytes).await
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
//! Owner-authenticated terminal sessions backed by private tmux processes.
|
||||
//!
|
||||
//! Browser connections are disposable attachments. The tmux process and its
|
||||
//! metadata remain on the node so a reconnect resumes the same workspace.
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use hyper::{Request, Response, StatusCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::process::Command;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
|
||||
fn tmux_key_for_input(data: &str) -> Option<&'static str> {
|
||||
match data {
|
||||
"\u{3}" => Some("C-c"),
|
||||
"\u{4}" => Some("C-d"),
|
||||
"\r" | "\n" => Some("Enter"),
|
||||
"\u{7f}" => Some("BSpace"),
|
||||
"\t" => Some("Tab"),
|
||||
"\u{1b}[A" => Some("Up"),
|
||||
"\u{1b}[B" => Some("Down"),
|
||||
"\u{1b}[C" => Some("Right"),
|
||||
"\u{1b}[D" => Some("Left"),
|
||||
"\u{1b}[H" => Some("Home"),
|
||||
"\u{1b}[F" => Some("End"),
|
||||
"\u{1b}[3~" => Some("DC"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::{build_response, ApiHandler};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct SessionRecord {
|
||||
pub schema: u8,
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub workspace: String,
|
||||
pub state: String,
|
||||
pub tmux: String,
|
||||
pub updated_at: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CreateRequest {
|
||||
name: Option<String>,
|
||||
workspace: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ClientMessage {
|
||||
#[serde(rename = "type")]
|
||||
kind: String,
|
||||
data: Option<String>,
|
||||
cols: Option<u16>,
|
||||
rows: Option<u16>,
|
||||
}
|
||||
|
||||
pub(crate) fn state_dir() -> PathBuf {
|
||||
std::env::var_os("ARCHY_SESSION_STATE_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| {
|
||||
if let Some(xdg) = std::env::var_os("XDG_STATE_HOME") {
|
||||
PathBuf::from(xdg).join("archipelago/sessions")
|
||||
} else if let Some(home) = std::env::var_os("HOME") {
|
||||
let user_dir = PathBuf::from(home).join(".local/state/archipelago/sessions");
|
||||
if user_dir.exists() {
|
||||
user_dir
|
||||
} else {
|
||||
PathBuf::from("/var/lib/archipelago/sessions")
|
||||
}
|
||||
} else {
|
||||
PathBuf::from("/var/lib/archipelago/sessions")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn valid_id(id: &str) -> bool {
|
||||
!id.is_empty()
|
||||
&& id.len() <= 64
|
||||
&& id
|
||||
.bytes()
|
||||
.all(|b| b.is_ascii_alphanumeric() || b == b'.' || b == b'_' || b == b'-')
|
||||
}
|
||||
|
||||
async fn read_record(dir: &Path, id: &str) -> Result<SessionRecord> {
|
||||
if !valid_id(id) {
|
||||
return Err(anyhow!("invalid session id"));
|
||||
}
|
||||
let bytes = tokio::fs::read(dir.join(format!("{id}.json"))).await?;
|
||||
Ok(serde_json::from_slice(&bytes)?)
|
||||
}
|
||||
|
||||
async fn tmux_alive(name: &str) -> bool {
|
||||
Command::new("tmux")
|
||||
.args(["has-session", "-t", name])
|
||||
.output()
|
||||
.await
|
||||
.map(|out| out.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
async fn write_record(dir: &Path, record: &SessionRecord) -> Result<()> {
|
||||
tokio::fs::create_dir_all(dir).await?;
|
||||
let tmp = dir.join(format!(".{}.tmp-{}", record.id, Uuid::new_v4()));
|
||||
let final_path = dir.join(format!("{}.json", record.id));
|
||||
tokio::fs::write(&tmp, serde_json::to_vec_pretty(record)?).await?;
|
||||
tokio::fs::rename(tmp, final_path).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn list(dir: &Path) -> Result<Vec<SessionRecord>> {
|
||||
let mut out = Vec::new();
|
||||
let mut entries = match tokio::fs::read_dir(dir).await {
|
||||
Ok(entries) => entries,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(out),
|
||||
Err(error) => return Err(error.into()),
|
||||
};
|
||||
while let Some(entry) = entries.next_entry().await? {
|
||||
if entry.path().extension().and_then(|s| s.to_str()) != Some("json") {
|
||||
continue;
|
||||
}
|
||||
let Ok(bytes) = tokio::fs::read(entry.path()).await else {
|
||||
continue;
|
||||
};
|
||||
let Ok(mut record) = serde_json::from_slice::<SessionRecord>(&bytes) else {
|
||||
continue;
|
||||
};
|
||||
record.state = if tmux_alive(&record.tmux).await {
|
||||
"detached".into()
|
||||
} else if record.state == "running" {
|
||||
"interrupted".into()
|
||||
} else {
|
||||
record.state.clone()
|
||||
};
|
||||
out.push(record);
|
||||
}
|
||||
out.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub(crate) async fn list_response() -> Result<Response<hyper::Body>> {
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(hyper::Body::from(serde_json::to_vec(
|
||||
&list(&state_dir()).await?,
|
||||
)?))?)
|
||||
}
|
||||
|
||||
pub(crate) async fn create(body: &[u8]) -> Result<Response<hyper::Body>> {
|
||||
let request: CreateRequest = serde_json::from_slice(body).unwrap_or(CreateRequest {
|
||||
name: None,
|
||||
workspace: None,
|
||||
});
|
||||
let workspace_was_requested = request.workspace.is_some();
|
||||
let workspace = request.workspace.unwrap_or_else(|| {
|
||||
std::env::var_os("HOME")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| PathBuf::from("/tmp"))
|
||||
.join("Work")
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
});
|
||||
let workspace_path = PathBuf::from(&workspace);
|
||||
if !workspace_was_requested {
|
||||
tokio::fs::create_dir_all(&workspace_path).await?;
|
||||
}
|
||||
if !workspace_path.is_absolute() || !workspace_path.is_dir() || workspace_path == Path::new("/")
|
||||
{
|
||||
return Ok(build_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"application/json",
|
||||
hyper::Body::from(r#"{"error":"workspace must be an existing non-root directory"}"#),
|
||||
));
|
||||
}
|
||||
let id = format!("s-{}", Uuid::new_v4().simple());
|
||||
let tmux_name = format!("archy-{id}");
|
||||
let output = Command::new("tmux")
|
||||
.args(["new-session", "-d", "-s", &tmux_name, "-c", &workspace])
|
||||
.output()
|
||||
.await?;
|
||||
if !output.status.success() {
|
||||
return Ok(build_response(
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
"application/json",
|
||||
hyper::Body::from(r#"{"error":"tmux could not start the session"}"#),
|
||||
));
|
||||
}
|
||||
let name = request
|
||||
.name
|
||||
.filter(|n| !n.trim().is_empty())
|
||||
.unwrap_or_else(|| "Work".into());
|
||||
let record = SessionRecord {
|
||||
schema: 1,
|
||||
id,
|
||||
name,
|
||||
workspace,
|
||||
state: "running".into(),
|
||||
tmux: tmux_name,
|
||||
updated_at: chrono::Utc::now().timestamp(),
|
||||
};
|
||||
write_record(&state_dir(), &record).await?;
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::CREATED)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(hyper::Body::from(serde_json::to_vec(&record)?))?)
|
||||
}
|
||||
|
||||
pub(crate) async fn websocket(req: Request<hyper::Body>) -> Result<Response<hyper::Body>> {
|
||||
let id = req
|
||||
.uri()
|
||||
.query()
|
||||
.and_then(|query| {
|
||||
query
|
||||
.split('&')
|
||||
.find_map(|part| part.strip_prefix("session="))
|
||||
})
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let record = read_record(&state_dir(), &id).await?;
|
||||
if !tmux_alive(&record.tmux).await {
|
||||
return Ok(build_response(
|
||||
StatusCode::CONFLICT,
|
||||
"application/json",
|
||||
hyper::Body::from(r#"{"error":"session is not running"}"#),
|
||||
));
|
||||
}
|
||||
let (response, ws_fut) =
|
||||
hyper_ws_listener::create_ws(req).map_err(|e| anyhow!("WebSocket upgrade failed: {e}"))?;
|
||||
if let Some(ws_fut) = ws_fut {
|
||||
tokio::spawn(async move {
|
||||
let Ok(Ok(stream)) = ws_fut.await else { return };
|
||||
let (mut tx, mut rx) = stream.split();
|
||||
let mut interval = tokio::time::interval(std::time::Duration::from_millis(150));
|
||||
let mut last = String::new();
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = interval.tick() => {
|
||||
// Capture the visible pane only. Asking tmux for a large
|
||||
// historical range injects hundreds of blank rows into
|
||||
// xterm on every reconnect, producing a misleading
|
||||
// giant initial scrollbar. xterm owns live scrollback.
|
||||
let output = Command::new("tmux").args(["capture-pane", "-p", "-e", "-t", &record.tmux]).output().await;
|
||||
if let Ok(output) = output {
|
||||
let text = String::from_utf8_lossy(&output.stdout).into_owned();
|
||||
if text != last { last = text.clone(); if tx.send(Message::Text(serde_json::json!({"type":"output", "data":text}).to_string())).await.is_err() { break; } }
|
||||
} else { break; }
|
||||
}
|
||||
message = rx.next() => match message {
|
||||
Some(Ok(Message::Text(text))) => {
|
||||
let Ok(message) = serde_json::from_str::<ClientMessage>(&text) else { continue };
|
||||
match message.kind.as_str() {
|
||||
"input" => if let Some(data) = message.data { let mut command = Command::new("tmux"); command.args(["send-keys", "-t", &record.tmux]); if let Some(key) = tmux_key_for_input(&data) { command.arg(key); } else { command.args(["-l", "--", &data]); } let _ = command.output().await; },
|
||||
"resize" => if let (Some(cols), Some(rows)) = (message.cols, message.rows) { let _ = Command::new("tmux").args(["resize-window", "-t", &record.tmux, "-x", &cols.to_string(), "-y", &rows.to_string()]).output().await; },
|
||||
"ping" => { let _ = tx.send(Message::Text(r#"{"type":"pong"}"#.into())).await; },
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Some(Ok(Message::Close(_))) | None => break,
|
||||
Some(Err(_)) => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
impl ApiHandler {
|
||||
pub(super) async fn handle_terminal_websocket(
|
||||
req: Request<hyper::Body>,
|
||||
) -> Result<Response<hyper::Body>> {
|
||||
websocket(req).await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
# Archipelago design system for agent-built apps
|
||||
|
||||
Status: planning draft, 2026-10-09. Companion to the
|
||||
[terminal and developer environment plan](terminal-developer-environment-spec.md).
|
||||
No tokens, components, templates or installed skills are changed by this draft.
|
||||
|
||||
An agent asked to build an Archipelago app should produce an app that belongs in
|
||||
Archipelago by construction. Give it a maintained component kit, runnable
|
||||
starters, documented screen patterns and visual references. A skill telling it
|
||||
to “use glassmorphism and orange” is insufficient.
|
||||
|
||||
The same foundation should drive Terminal, setup screens, first-party apps and
|
||||
newly generated apps. Existing third-party applications keep their own product UI;
|
||||
their Archipelago entry, launch and integration surfaces follow this contract.
|
||||
|
||||
## Evidence and design authority
|
||||
|
||||
At repository baseline `2cb1bae5`, the dashboard's design is distributed across
|
||||
`neode-ui/src/style.css`, `neode-ui/tailwind.config.js`, Vue components, and the
|
||||
short standards section of `docs/developer-guide.md`. AIUI maintains a separate
|
||||
token/style definition in `aiui/packages/app/src/styles/main.css`.
|
||||
|
||||
| Existing rule | Source observation | Contract to preserve or resolve |
|
||||
| --- | --- | --- |
|
||||
| Dark controls | Dashboard sets `color-scheme: dark` and explicit select colors | Dark baseline, including native controls |
|
||||
| Glass cards | `.glass-card`: black at 0.65 alpha, white border at 0.18, 16px radius | Preserve the established surface, not a generic frosted card |
|
||||
| Blur | Default card blur 18px; dashboard contexts deliberately suppress backdrop blur to avoid rendering corruption | Context-aware surface variants; never reintroduce blanket blur |
|
||||
| Buttons | `.glass-button`: 44px minimum, 12px radius; established focus/hover/pressed variants | Reusable buttons with full interaction states |
|
||||
| Accent | Dashboard focus/action uses `#fb923c`; AIUI accent is `#F7931A` | Named semantic tokens with explicit mapping; do not silently choose one for everything |
|
||||
| Typography | Dashboard body uses Avenir Next/system fallback, headings have bundled Montserrat; AIUI uses Inter/system fallback | Approved roles, metrics, fallback behavior and licensed assets |
|
||||
| Spacing | Tailwind extends a 4px grid | Shared spacing scale and layout examples |
|
||||
| Search | Existing search is 40px on desktop, 52px below the 920px breakpoint; right clear control retains focus | Shared search component and documented responsive behavior |
|
||||
| Card actions | Existing CSS places full-width actions at the bottom on desktop and mobile | Preserve this layout in generated card screens |
|
||||
| Modal layout | `BaseModal.vue`: pinned title/footer with independently scrolling content | Shared dialog shell with tested focus and Back behavior |
|
||||
| Mobile layout | Dynamic viewport, safe-area and audio-player offsets already exist | One documented inset contract; prevent double padding in embedded apps |
|
||||
| Success | Shared `PaymentSuccessPane.vue` and `IdentitySuccessPane.vue` express branded completion | Reuse the relevant pattern only when its completion condition is actually met |
|
||||
| Embedded canvas | AIUI embedded mode is transparent; standalone has its own background | Host owns wallpaper; apps choose explicit embedded/standalone mode |
|
||||
|
||||
These observations seed a visual audit; they do not make every legacy style a
|
||||
rule. Capture representative current screens in the running supported UI before
|
||||
extracting components. Approve Home, Apps, app detail, Settings, forms/dialogs,
|
||||
list/detail screens, Terminal and mobile examples as reference baselines.
|
||||
|
||||
The source of truth becomes versioned tokens plus components and pattern docs.
|
||||
Screenshots illustrate that contract and detect drift; they do not replace it.
|
||||
Existing dashboard and AIUI differences need explicit migration decisions.
|
||||
Preserve the current look first and make intentional design improvements visible
|
||||
in review rather than introducing them incidentally through an app template.
|
||||
|
||||
## Deliverable shape
|
||||
|
||||
Proposed layout, to be finalized against repository packaging conventions:
|
||||
|
||||
```text
|
||||
packages/archipelago-design/
|
||||
tokens/ semantic definitions and generated CSS/JSON
|
||||
styles/ scoped foundations and non-Vue component classes
|
||||
assets/ approved fonts, icons and licenses
|
||||
packages/archipelago-ui/
|
||||
components/ Vue components with documented states
|
||||
patterns/ app shells and composed screen patterns
|
||||
docs/design-system/
|
||||
index.md decision guide and version compatibility
|
||||
foundations.md typography, color, surfaces, spacing, motion
|
||||
components.md usage, states, accessibility, stable imports
|
||||
patterns.md complete screen composition and behavior
|
||||
examples/ reference captures and their matching source
|
||||
examples/archipelago-app/
|
||||
... working starter with manifest and tests
|
||||
skills/archipelago-design/
|
||||
SKILL.md concise trigger, routing and workflow
|
||||
references/ versioned design index and evaluation rubric
|
||||
```
|
||||
|
||||
Build an offline component gallery with the UI kit. Every example links to its
|
||||
source, token usage and applicable app pattern. A developer can run the gallery
|
||||
without a node session or provider API key. It must show loading, empty, error,
|
||||
success, disabled, focused, hovered and pressed states, not only the ideal screen.
|
||||
|
||||
Publish packages only as part of an explicitly authorized later implementation
|
||||
workflow. Until then these names and paths describe proposed deliverables.
|
||||
|
||||
## Token contract
|
||||
|
||||
Use framework-neutral CSS custom properties generated from a single definition.
|
||||
Vue components and any Tailwind adapters consume the same values. Prefix tokens
|
||||
with `--archy-`; do not expose arbitrary dashboard-global selectors to apps.
|
||||
|
||||
| Token family | Required coverage |
|
||||
| --- | --- |
|
||||
| Color | Canvas, card, inset, overlay, border, text primary/secondary/muted, interactive accent, focus, selected, success, warning, danger and info |
|
||||
| Typography | Body, heading and monospace families; type scale, weights, line heights and numeric alignment |
|
||||
| Space and size | 4px-based scale, page gutters, content widths, control heights, touch targets and icon sizes |
|
||||
| Shape | Card, dialog, input, button and pill radii |
|
||||
| Elevation | Card/overlay shadows, border strengths, blur-permitted and no-blur surfaces |
|
||||
| Motion | Durations/easing, permitted hover/press transitions and reduced-motion equivalents |
|
||||
| Layering | Header, navigation, menu, popover, modal and toast layers with ownership rules |
|
||||
| Viewport | Host-provided safe areas, keyboard viewport, navigation/audio offsets and embedded mode |
|
||||
| Terminal | Canvas, foreground, cursor, selection and a readable 16-color ANSI palette |
|
||||
|
||||
Make brand orange and interactive/focus orange distinct roles if preserving both
|
||||
existing values. Status colors must communicate meaning alongside text/icons.
|
||||
Name action variants by purpose: the existing orange “warning” class also serves
|
||||
primary actions, so exporting that name unchanged would teach the wrong semantics.
|
||||
|
||||
Typography must have predictable dimensions on Debian, Android and desktop
|
||||
browsers. Use only redistributable local font assets. Referencing Avenir or Courier
|
||||
New in a fallback stack does not license bundling those fonts. Freeze a tested
|
||||
body/heading/mono choice and line-height matrix in the visual review; do not let
|
||||
each generated app choose its own fonts or fetch a font CDN.
|
||||
|
||||
Check contrast on actual composited surfaces and the brightest/darkest supported
|
||||
backgrounds. Opacity values or CSS comments alone do not establish accessibility.
|
||||
Small text must remain readable, and focus must remain visible where current
|
||||
global styles suppress outlines. Low-power/no-backdrop-filter fallbacks retain
|
||||
the same hierarchy and usable contrast.
|
||||
|
||||
## Components and patterns agents can reuse
|
||||
|
||||
Extract reusable behavior from existing code; avoid giving agents a second
|
||||
lookalike library with slightly different interactions. Package components
|
||||
without dashboard stores, router assumptions, privileged RPC clients or secrets.
|
||||
Use adapters for integrations owned by the host.
|
||||
|
||||
| Foundation | First required components | Existing reference |
|
||||
| --- | --- | --- |
|
||||
| Structure | AppShell, PageHeader, Section, Card, InsetPanel, ActionRow | Dashboard CSS, app headers, card action rules |
|
||||
| Controls | Button, IconButton, TextField, PasswordField, Select, Toggle, SearchField, SegmentedControl | `ToggleSwitch.vue`, `PasswordRevealInput.vue`, `AppSearchField.vue` |
|
||||
| Navigation | Tabs, Breadcrumbs, Back action, contextual menu | Dashboard navigation, `BackButton.vue`, Cloud menus |
|
||||
| Feedback | StatusBadge, EmptyState, Skeleton, Progress, InlineError, Toast | `EmptyState.vue`, `SkeletonCard.vue`, `ToastStack.vue`, existing progress patterns |
|
||||
| Dialogs | Dialog, ConfirmDialog, credential handoff | `BaseModal.vue`, `AppConfirmModal.vue`, `AppCredentialInterstitial.vue` |
|
||||
| Data | ListRow, responsive table/list, detail key/value row, copy action | Existing app lists, `CopyButton.vue` |
|
||||
| Terminal | TerminalShell, SessionList, SessionRow, ConnectionStatus, mobile key strip | New consumers of the same foundation |
|
||||
|
||||
Each public component documents supported props/events/slots, sizing, keyboard
|
||||
behavior, accessibility, states and examples. Shared helpers must respect context:
|
||||
the current modal helper captures Escape and arrow keys, which is unsuitable
|
||||
for a terminal, editor or other widget that owns those keys. Fix/reuse that
|
||||
behavior through explicit contracts rather than blindly wrapping Terminal in it.
|
||||
|
||||
Provide composed patterns for:
|
||||
|
||||
- A searchable collection with filters, list/grid view, empty state and bottom
|
||||
card actions.
|
||||
- A detail screen with back navigation, metadata, status and primary action.
|
||||
- Settings grouped by task, with visible validation, save progress and retry.
|
||||
- A multi-step setup flow with skip/resume, honest progress and recovery.
|
||||
- First launch and credential handoff, including apps with their own login.
|
||||
- A destructive confirmation that names its target and data-preservation effect.
|
||||
- A session picker and terminal that clearly distinguish Close from End session.
|
||||
|
||||
Copywriting is part of the contract: concise task labels, useful error recovery,
|
||||
explicit pending versus complete states, no raw internal exception as the sole
|
||||
user message, and appropriate units/time formatting. Technical detail is
|
||||
available on demand. App-generated progress must reflect actual work.
|
||||
|
||||
## App shells and host integration
|
||||
|
||||
Supply a preferred Vue/TypeScript starter and a small portable HTML/CSS example.
|
||||
Existing React or other framework applications consume the tokens/styles and
|
||||
behavior contracts; do not require a framework rewrite to package an app.
|
||||
Full additional framework bindings follow actual demand and have their own
|
||||
acceptance, rather than presenting CSS classes as equivalent accessible widgets.
|
||||
|
||||
New starters include local assets, locked dependencies, scripts, manifest,
|
||||
container definition, health check, persistent-data example, tests and a complete
|
||||
reference screen. A starter must build and run from its copied location without
|
||||
monorepo-only aliases. It cannot require fetching current dashboard CSS at runtime.
|
||||
|
||||
The host integration contract defines:
|
||||
|
||||
- Explicit standalone versus embedded appearance. Embedded apps use a transparent
|
||||
canvas where appropriate; they do not add a duplicate wallpaper or navigation
|
||||
shell. Standalone mode still has a complete, readable canvas.
|
||||
- Versioned, non-secret theme/inset messages if runtime synchronization is needed.
|
||||
Validate sender origin, source window and payload; no wildcard privileged
|
||||
message bridge. An app must remain usable with no host handshake.
|
||||
- Exactly one owner for top/bottom safe-area padding and fixed player/navigation
|
||||
clearance. Test the actual Companion WebView, not just a mobile screenshot.
|
||||
- Existing launch interfaces and app-gate behavior from the developer docs.
|
||||
Asset URLs work under their supported base path and HTTP/HTTPS entry points.
|
||||
- Nostr signer, media controls and credential handoff only through their supported
|
||||
APIs. Visual kit installation does not give an app host management access.
|
||||
|
||||
Package tokens/components with the app's pinned kit version. Evolve them with
|
||||
documented compatibility and migrations. Runtime theme values may be forwarded
|
||||
through the agreed contract; runtime JavaScript/CSS replacement is not the update
|
||||
mechanism. A platform update must not silently break every installed app.
|
||||
|
||||
## Agent workflow
|
||||
|
||||
The app skill always routes new UI work through `archipelago-design`. The system
|
||||
skill does the same when a system modification affects a visible screen. A
|
||||
backend-only request loads no unnecessary design material.
|
||||
|
||||
For relevant tasks the agent:
|
||||
|
||||
1. Reads the design index, supported kit version and matching pattern; identifies
|
||||
existing components before creating new ones.
|
||||
2. Inspects the reference screen and its source, then selects the closest starter.
|
||||
3. Implements the requested behavior using shared components and semantic tokens.
|
||||
4. Exercises normal, loading, empty, invalid, failed and successful states.
|
||||
5. Renders standalone and embedded previews at the required viewports; inspects
|
||||
captures, keyboard behavior and actual component geometry.
|
||||
6. Runs app checks and supplies the working preview, component/token provenance,
|
||||
results and any deliberate design exceptions.
|
||||
|
||||
Make that loop easy: `archy app new`, `archy app preview` and `archy app check`
|
||||
should supply the starter, preview fixtures and relevant checks. Those are
|
||||
proposed tools in the terminal plan, not currently implemented commands.
|
||||
Ordinary changes within the requested task proceed through this loop without
|
||||
requiring approval for every edit. A new shared design primitive or intentional
|
||||
departure is identified in the review, with its use case and visual evidence.
|
||||
|
||||
The skill should tell agents how to find and use the design system, not make
|
||||
them recreate it from a long list of adjectives. Keep the entrypoint concise;
|
||||
conditional references cover forms, layout, terminal interaction, media and
|
||||
signer flows. Templates and screenshots belong in reusable assets, not prose
|
||||
that the agent has to transcribe.
|
||||
|
||||
## Design conformance and definition of done
|
||||
|
||||
Combine targeted static checks, interaction tests and visual review. Static
|
||||
checks cannot prove a coherent design; screenshots cannot prove an accessible
|
||||
or correct interaction.
|
||||
|
||||
| Gate | Acceptance |
|
||||
| --- | --- |
|
||||
| Shared foundation | New UI imports the approved kit/version; bespoke color/font/radius/z-index values need a named exception, with allowances for content such as charts and user imagery |
|
||||
| Visual hierarchy | Reference-matched page width/gutters, type roles, card surfaces, action placement and density |
|
||||
| Responsive layout | 320, 390, 768 and 1440px widths plus phone landscape; 200% text zoom; no accidental horizontal page overflow |
|
||||
| Input | Keyboard-only use, visible focus, sensible tab order, labels, dialog focus containment/restoration and Back behavior; terminal keys remain intact |
|
||||
| Touch | 44px usable targets for new mobile controls, reachable persistent actions, no essential hover-only interaction |
|
||||
| States | Loading, empty, offline, denied, validation error, retry and success demonstrated with deterministic fixtures |
|
||||
| Appearance | Dark native controls, foreground/background contrast, reduced motion, no-blur fallback and locally available fonts |
|
||||
| Host modes | Embedded/standalone, HTTP/HTTPS where supported, software keyboard, safe areas, audio-player offsets and Companion |
|
||||
| Runtime truth | Success and readiness only follow actual confirmed results; theme/state changes do not erase form or session work |
|
||||
| Regression | Shared component changes compared against representative dashboard and app reference captures before release |
|
||||
|
||||
Choose per-component visual-diff tolerances after baseline capture; avoid a
|
||||
single permissive threshold that conceals layout regressions. Stabilize fonts,
|
||||
viewport, fixture data and animations. Review intended baseline changes rather
|
||||
than automatically accepting new screenshots when CI fails.
|
||||
|
||||
Behavioral skill evaluation should use realistic tasks with no hidden design
|
||||
brief: build a bookmarks app, add a settings form, create an app with first-run
|
||||
credentials, adapt a non-Vue app, and improve Terminal's session picker. Inspect
|
||||
the generated artifacts and previews for component reuse, consistent appearance,
|
||||
correct manifest integration and usable failure states. Repeat on a fresh
|
||||
environment to ensure success does not rely on this checkout or an agent's
|
||||
conversation history.
|
||||
|
||||
## Implementation order
|
||||
|
||||
1. Audit representative existing screens; resolve accent/typography/blur and
|
||||
embedded-mode decisions; record the approved reference set.
|
||||
2. Extract semantic tokens and the first components while preserving current
|
||||
dashboard rendering. Migrate a small representative slice to prove parity.
|
||||
3. Build the offline gallery, app shell and working starter. Use Terminal/setup
|
||||
as real consumers so the foundation is exercised immediately.
|
||||
4. Package the app/design skills with matching docs and assets; run realistic
|
||||
generation tasks against the starter and gallery.
|
||||
5. Add scoped conformance checks to app validation and qualification; migrate
|
||||
additional first-party surfaces incrementally.
|
||||
|
||||
App generation cannot be called complete while agents still invent the visual
|
||||
foundation. Completion means a new agent, a new workspace and an ordinary app
|
||||
request reliably produce a working, recognizably Archipelago result.
|
||||
@@ -0,0 +1,539 @@
|
||||
# Archipelago terminal and developer environment plan
|
||||
|
||||
Status: execution draft, 2026-10-09. The local persistent-session primitive is
|
||||
implemented in `scripts/archy-session`; dashboard/WebSocket attachment,
|
||||
developer-account provisioning, and Codex installation remain follow-on slices.
|
||||
Repository baseline: `2cb1bae5ce244d387679f90951d62fd030ebf228`.
|
||||
|
||||
Archipelago should let an owner open a real terminal, resume previous work,
|
||||
configure their system, and ask a preinstalled coding agent to build an app that
|
||||
looks and behaves like Archipelago. The experience must work from the dashboard,
|
||||
local console, and SSH, with the same tools and discoverable commands.
|
||||
|
||||
The user requirements are:
|
||||
|
||||
- Bring Omarchy's initial setup and development capabilities to Archipelago.
|
||||
- Ship Codex ready to launch, with guided personal authentication.
|
||||
- Make closing and resuming terminal sessions easy and reliable.
|
||||
- Ship local skills that help agents modify the system and build apps using the
|
||||
actual developer documentation.
|
||||
- Make visual and interaction consistency an enforced part of app creation.
|
||||
- Plan in a separate worktree, without conflicting with current release work.
|
||||
|
||||
The companion [agent design system plan](archipelago-agent-design-system-spec.md)
|
||||
defines the shared UI kit, agent workflow, app templates, and visual acceptance.
|
||||
That work is a dependency of the app-building experience, not finishing polish.
|
||||
|
||||
## Product outcome
|
||||
|
||||
On a newly installed node, Terminal opens to a usable shell with a small welcome
|
||||
panel offering Setup, Resume, Build an app, Work on Archipelago, and Help. The
|
||||
shell is immediately usable; onboarding is dismissible and resumable. Existing
|
||||
owners get the same environment through an upgrade that preserves their files,
|
||||
configuration, credentials, and uninstall decisions.
|
||||
|
||||
A representative first session:
|
||||
|
||||
1. Open Terminal from the dashboard or launch it locally.
|
||||
2. See the node, workspace, account, and any existing sessions clearly identified.
|
||||
3. Run the setup guide, select editor/toolchains, and sign in to Codex.
|
||||
4. Choose Build an app and describe the app. The agent loads Archipelago's app
|
||||
and design guidance and starts from the maintained starter.
|
||||
5. Review a working local preview, including mobile and failure states.
|
||||
6. Close Terminal while a build or agent task runs.
|
||||
7. Reopen it, select the named session, and continue where it was left.
|
||||
8. Install the candidate through the existing app lifecycle on a chosen test
|
||||
node, then contribute through ngit when publication is requested.
|
||||
|
||||
## Current implementation and gaps
|
||||
|
||||
These are source findings, not live-node acceptance results.
|
||||
|
||||
| Surface | Current source | Required change |
|
||||
| --- | --- | --- |
|
||||
| Dashboard terminal | `neode-ui/src/components/CLIPopup.vue`: development-only simulated commands; production SSH instructions and a literal default password | Real terminal and session picker; connection information derived from actual setup state |
|
||||
| Terminal state | `neode-ui/src/stores/cli.ts`: open/close boolean | Server-owned session inventory; client UI state independent of process lifetime |
|
||||
| Keyboard | `neode-ui/src/App.vue`: global F and other shortcuts; `useModalKeyboard.ts` captures arrows and Escape | A terminal focus boundary so shell, editor and agent keys reach the PTY |
|
||||
| Console setup | `image-recipe/archipelago-scripts/archipelago-menu.sh`: legacy menu, eager installer-tool installation, direct container setup | Shared command catalog and current orchestration; opening a menu does not install software |
|
||||
| Shell onboarding | `scripts/welcome-banner.sh`: banner and SSH information, including literal default password; may wait for networking | Short local welcome, accurate access state, fast offline shell startup |
|
||||
| ISO | `image-recipe/build-debian-iso.sh` invokes a relocated copy of `_archived/build-auto-installer-iso.sh` | Integrate the actual active build path, despite its archived filename |
|
||||
| Upgrade | `scripts/self-update.sh`, `core/archipelago/src/bootstrap.rs`, runtime assets | One versioned environment payload and repeatable migration shared with ISO |
|
||||
| Existing development access | Pinned ngit installer and GitWorkshop app already exist | Reuse these; keep ngit the contribution platform |
|
||||
| App packaging | `docs/app-developer-guide.md`, manifest spec and Rust parser | Turn existing contracts into agent workflows, validated starters and development commands |
|
||||
| Design | Dashboard CSS/Tailwind, reusable Vue components, separate AIUI CSS | Extract and document a coherent shared contract; see companion plan |
|
||||
|
||||
The backend uses Hyper/Tokio and existing WebSocket handlers; the developer guide
|
||||
currently calls it Axum. Several contributor documents also show unrestricted
|
||||
`cargo test`, conflicting with `AGENTS.md`. Correct those documentation examples
|
||||
before packaging them as agent instructions. On a live node, backend execution
|
||||
must use `scripts/test-backend-isolated.sh`.
|
||||
|
||||
## Omarchy reference and capability mapping
|
||||
|
||||
Research baseline: the official [Omarchy repository](https://github.com/omacom/omarchy),
|
||||
formerly reached through `basecamp/omarchy`. Its default branch was `quattro` at
|
||||
[`fcf9eeb5c454739f3f23cdfd7d57d77b12961025`](https://github.com/omacom/omarchy/tree/fcf9eeb5c454739f3f23cdfd7d57d77b12961025).
|
||||
The latest published release returned during research was
|
||||
[v4.0.4](https://github.com/omacom/omarchy/releases/tag/v4.0.4), published September 15,
|
||||
2026, resolving to `c668141e9c42b13c80c9ca4ea108e11708c5e8a5`.
|
||||
The inventory below describes the pinned default-branch source; it does not
|
||||
assert that every item is present in that published release.
|
||||
|
||||
“All setup capabilities” means explicit disposition of the setup surface, not
|
||||
silently omitting desktop-specific features. The proposed core release covers
|
||||
the terminal, system setup and development rows below. Hardware/desktop options
|
||||
remain named follow-on work; completion of the core is not full Omarchy parity.
|
||||
|
||||
| Omarchy capability | Proposed Archipelago equivalent | Delivery |
|
||||
| --- | --- | --- |
|
||||
| Owner, keyboard, hostname, timezone, optional Git name/email | Resume existing node onboarding; configure developer identity separately from appliance identity | Core |
|
||||
| Deferred provisioning for another owner | Leave personal developer credentials unset; first owner completes setup | Core |
|
||||
| Separate packaged defaults, user finalization and migrations | Versioned system payload, per-user setup receipts, explicit reset with backup | Core |
|
||||
| First-login welcome, shortcuts, network/update guidance | Welcome and searchable help shared by terminal and dashboard | Core |
|
||||
| Bash completion, history, prompt, fuzzy finding and directory navigation | Bash, completion, Starship, fzf and zoxide with readable console fallback | Core |
|
||||
| File/search/system tools | ripgrep, fd, bat, eza, jq, less, man, tldr, btop and fastfetch | Core |
|
||||
| Terminal selection: Foot, Alacritty, Ghostty, Kitty | Browser terminal plus console/SSH; local emulator adapters compatible with the actual kiosk display stack | Browser/console core; native choices follow-on |
|
||||
| tmux sessions, panes, developer layouts | Named persistent sessions and editor/agent/shell layouts; same sessions accessible by SSH | Core |
|
||||
| Neovim and selectable editors | nano available immediately; maintained Neovim profile and editor preference; GUI editors when desktop support exists | Core terminal editors; GUI follow-on |
|
||||
| Mise development environments | Versioned Node/npm, Rust, Python/uv profiles first; project-local versions | Core |
|
||||
| Ruby/Rails, Bun, Deno, Go, PHP/Laravel/Symfony, Elixir/Phoenix, Java, Zig, OCaml, .NET, Clojure, Scala | Optional profiles in the same installer catalog, each with architecture and verification metadata | Parity follow-on |
|
||||
| Git, lazygit, GitHub CLI | Git/lazygit and ngit/GitWorkshop first-class; gh optional for other upstream projects | Core |
|
||||
| Lazy agent launchers | Codex actually bundled; optional adapters for other agent CLIs | Core Codex; provider expansion follow-on |
|
||||
| Default-agent selector and starter prompts | Codex selected initially for a fresh setup; preserve existing preference; system/app/design entry prompts | Core |
|
||||
| Agent account selection and usage panel | Explicit account context and supported login status; manual account profiles before considering usage automation | Follow-on |
|
||||
| Local model tools | Integrate the existing Ollama app and compatible provider setup; downloads and model resource needs visible | Follow-on |
|
||||
| Bundled system and app-building skills | Archipelago system, app and design skills with offline references and templates | Core |
|
||||
| Development databases: MySQL, PostgreSQL, Redis, MongoDB, MariaDB, MSSQL | Scoped rootless Podman development recipes, isolated ports/data and generated credentials | Core PostgreSQL/Redis-compatible recipe; remaining recipes follow-on |
|
||||
| Container tooling | Existing rootless Podman; documented Compose compatibility where tested | Core |
|
||||
| DNS, Wi-Fi, network QR, SSH daemon and SSH agent setup | Existing network/SSH controls exposed through shared setup operations, with accurate connection details | Core |
|
||||
| Fingerprint, FIDO2 and privilege preferences | Hardware-aware account-security setup; no copied Arch PAM configuration | Follow-on |
|
||||
| Monitors, keyboard bindings, input, XCompose | Kiosk/console equivalents through existing system configuration; desktop-specific adapters separately | Core console basics; desktop follow-on |
|
||||
| Browser, terminal, editor and dictation defaults | Editor/agent/terminal choices first; browser/dictation surfaced when relevant to the device | Core subset; follow-on adapters |
|
||||
| Themes, fonts, background and prompt | Shared Archipelago tokens and terminal palette; preserve owner customization | Core |
|
||||
| Shell plugins and customization hooks | Versioned extension points and documented user overrides; dashboard extensions require a separate supported contract | User overrides core; plugins follow-on |
|
||||
| Package, TUI, web-app and development installation menus | Curated tool catalog plus existing Archipelago app catalog; distinguish developer tools from managed apps | Core |
|
||||
| Commercial services, GUI apps, gaming, Windows VM | Individual optional app/desktop integrations, inventoried as a separate parity backlog | Follow-on, not preinstalled on nodes |
|
||||
| Update, reset, snapshots, direct boot | Integrate Archipelago update/recovery choices; explicit reset scope and backup | Core existing operations; new boot/snapshot features follow-on |
|
||||
| Hardware detection and vendor fixes | Existing Archipelago hardware configuration, capability detection and separately qualified device fixes | Core detection; device adapters follow-on |
|
||||
| Crash diagnosis skill | Sanitized diagnostics with an explicit handoff to the chosen agent; preserve source versus live evidence | Follow-on |
|
||||
|
||||
Primary source routes for the inventory:
|
||||
|
||||
- [Provisioning and file layout](https://github.com/omacom/omarchy/blob/fcf9eeb5c454739f3f23cdfd7d57d77b12961025/docs/file-layout.md),
|
||||
[setup form](https://github.com/omacom/omarchy/blob/fcf9eeb5c454739f3f23cdfd7d57d77b12961025/install/provisioning/setup-form.sh),
|
||||
[setup and installation menu](https://github.com/omacom/omarchy/blob/fcf9eeb5c454739f3f23cdfd7d57d77b12961025/default/omarchy/omarchy-menu.jsonc).
|
||||
- [Base packages](https://github.com/omacom/omarchy/blob/fcf9eeb5c454739f3f23cdfd7d57d77b12961025/install/omarchy-base.packages),
|
||||
[shell initialization](https://github.com/omacom/omarchy/blob/fcf9eeb5c454739f3f23cdfd7d57d77b12961025/default/bash/init),
|
||||
[terminal and tmux](https://github.com/omacom/omarchy/blob/fcf9eeb5c454739f3f23cdfd7d57d77b12961025/manual/15-terminal.md).
|
||||
- [Development profiles](https://github.com/omacom/omarchy/blob/fcf9eeb5c454739f3f23cdfd7d57d77b12961025/bin/omarchy-install-dev-env),
|
||||
[agent setup](https://github.com/omacom/omarchy/blob/fcf9eeb5c454739f3f23cdfd7d57d77b12961025/install/user/mise.sh),
|
||||
[agent launcher implementation](https://github.com/omacom/omarchy/blob/fcf9eeb5c454739f3f23cdfd7d57d77b12961025/bin/omarchy-mise-install),
|
||||
[development databases](https://github.com/omacom/omarchy/blob/fcf9eeb5c454739f3f23cdfd7d57d77b12961025/bin/omarchy-install-docker-dbs).
|
||||
- [System skill](https://github.com/omacom/omarchy/blob/fcf9eeb5c454739f3f23cdfd7d57d77b12961025/default/agents/skills/omarchy/SKILL.md)
|
||||
and [app skill](https://github.com/omacom/omarchy/blob/fcf9eeb5c454739f3f23cdfd7d57d77b12961025/default/agents/skills/omarchy-app/SKILL.md).
|
||||
|
||||
Adapt the workflows to Debian and Archipelago's manifest-driven runtime. In
|
||||
particular, Omarchy's app skill produces Qt desktop apps; Archipelago's starter
|
||||
must produce an Archipelago app. Omarchy's agent launchers download tools on use;
|
||||
the user's requirement here is stronger: Codex must already be installed. Its
|
||||
database recipes and permission-changing aliases are not defaults to transplant.
|
||||
Any reused upstream files need their original license notices and provenance.
|
||||
|
||||
## Terminal experience
|
||||
|
||||
Use a real PTY with a bundled terminal renderer, proposed `@xterm/xterm` with
|
||||
fit/search support. It must run Bash, nano/Neovim, lazygit, tmux and Codex without
|
||||
special simulated command handling. Exact dependency versions are selected and
|
||||
locked during implementation qualification.
|
||||
|
||||
The dashboard entry opens a large resizable terminal on desktop and a full-height
|
||||
surface on mobile. It provides session name, workspace/node context, connection
|
||||
status, session switcher, New, Resume, Search, copy/paste, font size, fullscreen,
|
||||
and Close. End session belongs in a separate menu with a clear running-work warning.
|
||||
Mobile adds an Esc/Ctrl/Tab/arrows key strip and works with the software keyboard.
|
||||
|
||||
Use the shared Archipelago shell/components around an opaque, readable terminal
|
||||
canvas. Terminal content uses a packaged monospace font with Unicode support;
|
||||
the console gets an ASCII-compatible fallback. Apply the design plan's focus,
|
||||
contrast, reduced-motion and safe-area rules.
|
||||
|
||||
While terminal input has focus, F, Escape, arrows, Ctrl+C, Ctrl+D, Ctrl+R, Tab,
|
||||
editor keys and tmux prefixes belong to the terminal. Closing uses a visible
|
||||
control or a documented terminal-specific shortcut. Do not reuse the current
|
||||
modal keyboard handler unchanged. Ctrl+C interrupts the foreground job; Ctrl+D
|
||||
has normal shell semantics. Focus can move to toolbar controls and back.
|
||||
|
||||
## Persistent sessions and one-click resume
|
||||
|
||||
### Implemented local primitive
|
||||
|
||||
`scripts/archy-session` provides the first execution slice for named sessions.
|
||||
It stores atomic JSON metadata under the user state directory and uses one
|
||||
isolated tmux session per workspace. `list` reconciles metadata with the live
|
||||
tmux process, `attach` resumes an existing process without creating a duplicate,
|
||||
`rename` preserves the workspace, and `end` is the only operation that stops a
|
||||
session. Its lifecycle test is `scripts/tests/archy-session-test.sh`.
|
||||
|
||||
This helper is intentionally not the browser security boundary: the future
|
||||
session service must enforce owner/node/boot identity, attachment grants, CSRF,
|
||||
and revocation before exposing it through the dashboard.
|
||||
|
||||
This is a core release criterion, including the first usable terminal milestone.
|
||||
|
||||
Session execution lives on the node in a dedicated, supervised user environment.
|
||||
Use tmux as the initial persistence engine; a browser WebSocket is an attachment,
|
||||
not the owner of the shell process. Keep the tmux server and worker scope outside
|
||||
the dashboard/backend service's kill group. Ordinary manager deployment must not
|
||||
tear down developer sessions.
|
||||
|
||||
Prefer one tmux server/socket and supervised process scope per managed session,
|
||||
so ending one session cannot kill a shared server containing other work. Keep
|
||||
those sockets private to the developer account. SSH and local-console resume use
|
||||
the same session registry and attachment helper rather than guessing tmux names.
|
||||
Cross-device resume refers to browsers connected to the same node; moving live
|
||||
processes between different nodes is outside this contract.
|
||||
|
||||
| Event | Required behavior |
|
||||
| --- | --- |
|
||||
| Close terminal panel, navigate away, close tab, kill browser | Detach; shell, build and agent continue |
|
||||
| Reopen Terminal | Show running and recent sessions; Resume most recent available in one action; do not auto-create duplicates |
|
||||
| Switch session | Detach the old view and attach the selected session |
|
||||
| Network drop or mobile sleep | Show Reconnecting; reauthenticate if needed, then attach to the existing session |
|
||||
| Dashboard reload or backend restart | Session survives; rediscover it from node state |
|
||||
| Second browser/device | Same authenticated owner sees sessions; explicit transfer of input control |
|
||||
| Logout/session expiry | Revoke browser attachments immediately; background work remains; a fresh owner login is required to reattach |
|
||||
| Explicit End session | Confirm when work is active, terminate that session's process scope, record ended state; do not remove its files |
|
||||
| Shell exits | Show ended status and exit information where available; offer a new shell in that workspace |
|
||||
| Broker crash | tmux/process scope survives if its supervisor survives; reconcile inventory, without replaying input |
|
||||
| Node reboot/power failure | Processes stop. Keep workspace/session metadata and offer Reopen workspace and Resume Codex conversation; never label this a live process resume |
|
||||
|
||||
The picker shows a user-editable name, workspace, creation/last-attachment time,
|
||||
Running/Detached/Ended/Interrupted state, and optional pinned status. Scope every
|
||||
entry to a node and stable owner identity. LocalStorage may remember selection;
|
||||
it is never the authoritative session registry.
|
||||
|
||||
Persist metadata atomically under the workspace account's private state directory.
|
||||
Record schema version, opaque session ID, owner ID, node ID, boot ID, tmux target,
|
||||
workspace ID/path, created/attached timestamps and lifecycle state. Treat tmux and
|
||||
supervised process state as authority for whether a session is still alive.
|
||||
After a crash, reconcile orphan sessions and interrupted creates before accepting
|
||||
another create with the same request ID.
|
||||
|
||||
Place workspaces and agent state on durable storage with an explicit ownership,
|
||||
quota and backup policy; do not consume a small system partition accidentally.
|
||||
Expose a familiar `~/Work` entrypoint while recording the actual workspace root.
|
||||
Reconnecting never requires a fresh Git checkout. A missing or unmounted workspace
|
||||
is a visible recovery state, not permission to create an empty replacement at the
|
||||
same path. Deleting session metadata and deleting project files are distinct actions.
|
||||
|
||||
Keep bounded in-memory scrollback and restore the current terminal screen from
|
||||
the surviving tmux attachment. Do not assume client-side scrollback survived.
|
||||
Reboot persistence covers metadata and saved files; transcript recording is a
|
||||
separate opt-in setting because terminal output can include credentials.
|
||||
|
||||
Initial proposed limits: eight sessions per owner, one active input controller
|
||||
per session, a 10,000-line scrollback ceiling plus a byte ceiling, and bounded
|
||||
per-connection queues. Do not kill detached work because a browser idle timer
|
||||
expired. Make resource use visible and allow explicit stop/cleanup. Qualify CPU,
|
||||
memory and disk limits on the smallest supported node before choosing defaults.
|
||||
|
||||
Input is never automatically replayed after reconnect: a lost acknowledgement
|
||||
does not establish whether Enter or a command reached the shell. Resize events
|
||||
are idempotent. A newly attached terminal redraws from the current PTY state.
|
||||
Revoke the previous writer before granting a new writer lease; read-only views
|
||||
can be a later addition. User labels and working directories must never be
|
||||
interpolated into shell command strings.
|
||||
|
||||
## Execution and connection architecture
|
||||
|
||||
Proposed components and responsibility boundaries:
|
||||
|
||||
```text
|
||||
Dashboard terminal entry / SSH / local console
|
||||
|
|
||||
authenticated attachment
|
||||
|
|
||||
Terminal session service -- session metadata and ownership
|
||||
|
|
||||
supervised developer user + tmux + PTY
|
||||
|
|
||||
Bash / editor / Codex / project toolchains
|
||||
|
|
||||
archy CLI -- existing typed management operations
|
||||
```
|
||||
|
||||
Add a small terminal session service with a local Unix-socket control interface.
|
||||
The existing management backend validates owner authorization and brokers
|
||||
short-lived attachments; it does not execute arbitrary shell strings in an RPC
|
||||
handler. Use typed arguments and fixed executable paths for session creation.
|
||||
An implementation spike must verify PTY allocation, systemd ownership, tmux
|
||||
reattachment and Codex rendering before committing to the broker library.
|
||||
|
||||
Prefer a dedicated developer Unix account, separate from the existing
|
||||
`archipelago` service account, with its own home, rootless container storage and
|
||||
no mounts of production wallets/secrets. Keep the current service account and
|
||||
data ownership intact. The inspected ISO builder grants the service account broad
|
||||
passwordless sudo; using that account for a browser shell would grant equivalent
|
||||
host authority. A new developer account alone is not proof of isolation: test
|
||||
actual filesystem permissions, sockets, groups and sudo policy.
|
||||
|
||||
Provide an explicit System administration context for system changes. Routine
|
||||
supported operations should use the same typed management operations as the UI;
|
||||
arbitrary host-shell administration needs a distinct authenticated operator
|
||||
session. These boundaries must still permit authorized agents to configure the
|
||||
system efficiently. Do not require repeated consent for each step of an already
|
||||
authorized operation, or treat a skill as an access-control mechanism.
|
||||
|
||||
Initial web access is for the node owner. Public app sessions, guests, peer
|
||||
identities and iframe signers do not imply terminal authorization. Multi-user
|
||||
workspace sharing is outside the first release; reject unsupported mappings.
|
||||
|
||||
Prefer a dedicated terminal browser origin with a minimal self-hosted bundle,
|
||||
no app iframes or external scripts, and narrow authenticated handoff from the
|
||||
dashboard. A same-origin subpage reduces bundle complexity but does not isolate
|
||||
it from same-origin scripts. Resolve origin, certificate and Companion handling
|
||||
in the connection spike before shipping. The
|
||||
[xterm.js integration guide](https://xtermjs.org/docs/guides/security/) specifically
|
||||
requires application-level WebSocket authentication/origin handling and careful
|
||||
treatment of terminal output.
|
||||
|
||||
Remote terminal transport requires verified HTTPS/WSS. Existing plain-HTTP
|
||||
dashboard users get a working secure-terminal entry and SSH alternative; do not
|
||||
force a global dashboard HTTPS migration as a side effect. Retain current private
|
||||
management ingress controls, IPv6 support and explicit proxy trust. Never infer
|
||||
terminal authorization from a forwarded hostname or a private IP alone.
|
||||
|
||||
The protocol contract must include:
|
||||
|
||||
- Create/list/rename/end session operations with CSRF checks, stable owner
|
||||
binding and idempotent create/end behavior.
|
||||
- A single-use, short-lived attachment grant bound to session, owner and exact
|
||||
terminal origin. No reusable dashboard cookie or API key in a query string.
|
||||
- An authenticated WebSocket with bounded pre-auth time and no PTY output before
|
||||
authorization; validate Origin separately from CORS and revalidate on reconnect.
|
||||
- Input bytes, output bytes, resize, connection state and exit messages; bounded
|
||||
frame sizes, queue backpressure and slow-reader behavior.
|
||||
- Immediate attachment revocation on logout/credential revocation. No automatic
|
||||
input replay, automatic command rerun or unauthenticated reconnect.
|
||||
- Process-group cleanup only for explicit termination; metadata-only audit logs,
|
||||
excluding command contents, keystrokes, credentials and terminal output.
|
||||
- Terminal titles/links treated as untrusted text; external links require a user
|
||||
gesture; clipboard escape sequences cannot silently read/write the clipboard.
|
||||
|
||||
## Setup and tool distribution
|
||||
|
||||
Provide a searchable `archy` CLI and matching setup TUI; the existing
|
||||
`archipelago` executable remains the backend daemon. All `archy` commands in
|
||||
this document are proposed interfaces, not commands available today.
|
||||
|
||||
| Proposed command | Purpose |
|
||||
| --- | --- |
|
||||
| `archy setup` | Resume setup, show installed/available/deferred items |
|
||||
| `archy commands --json` | Agent-readable command descriptions, inputs, privileges and side effects |
|
||||
| `archy doctor` | Read-only, bounded health checks with actionable findings |
|
||||
| `archy session list` / `resume <id>` | Discover and attach to existing work |
|
||||
| `archy dev setup <profile>` | Install a declared, versioned toolchain profile |
|
||||
| `archy agent` | Launch selected agent in the selected workspace |
|
||||
| `archy app new <id>` | Create from a maintained Archipelago starter |
|
||||
| `archy app check <path>` | Manifest, build, integration and design checks |
|
||||
| `archy app preview <path>` | Start an isolated local preview and return its URL |
|
||||
| `archy app install <path> --node <target>` | Explicit candidate deployment through supported orchestration |
|
||||
| `archy system <operation>` | Discoverable adapters to supported system operations |
|
||||
| `archy skills status` | Show installed skill/doc/kit versions and local overrides |
|
||||
|
||||
Use one command catalog for the CLI, setup menu and skill references. Each entry
|
||||
declares what it reads/changes, required privilege, supported machines, expected
|
||||
output, progress and rollback/recovery behavior. Structured status is for agents;
|
||||
the human menu uses clear task names. Missing capability is a visible explanation.
|
||||
|
||||
First-run sequence: verify network/time/access status; create or select a workspace;
|
||||
set optional Git identity and editor; confirm installed tools; offer toolchain
|
||||
profiles; sign in to Codex; offer a sample app or system task. Users can skip and
|
||||
resume individual steps. Authentication is never an ISO build step. Run nothing
|
||||
interactive in noninteractive shells, SCP/SFTP, remote command execution or CI.
|
||||
|
||||
Ship offline: shell essentials, tmux, Git/ngit, Codex binary, local documentation,
|
||||
skills and design-kit assets. Developer profiles add the complete Node/frontend,
|
||||
Rust/backend or Python environment and build prerequisites. Offline capability
|
||||
must be stated precisely: shell/docs/Codex launch can work offline; model calls,
|
||||
uncached dependencies and external login require connectivity. Plan a full
|
||||
offline developer bundle as a separate profile if all build caches are required.
|
||||
|
||||
Use signed/versioned payloads and per-architecture hashes. Debian packages and
|
||||
user toolchains have separate ownership. Core tools update through qualified
|
||||
releases; opening a shell or running `codex` must not silently update binaries.
|
||||
Optional tool installs show download size, source, version and progress. Respect
|
||||
package-manager locks and existing mise/rustup/nvm installations. Do not prune a
|
||||
binary version beneath a running session.
|
||||
|
||||
State is versioned per machine and per user with pending/running/done/failed/skipped
|
||||
steps; write completion only after verification. Concurrent setup is serialized.
|
||||
Preserve user dotfiles through small managed includes and explicit overrides;
|
||||
preview changes and back up touched configuration during an explicit reset.
|
||||
Network interruption, disk exhaustion and reboot leave resumable state.
|
||||
|
||||
## Codex integration
|
||||
|
||||
Bundle a qualified stable Codex release for each supported architecture, verified
|
||||
against a pinned artifact. The official
|
||||
[CLI installation documentation](https://learn.chatgpt.com/docs/codex/cli)
|
||||
documents standalone and npm installation; the release implementation should
|
||||
resolve exact packaging and pin it rather than executing an unversioned installer
|
||||
on every node.
|
||||
|
||||
Use normal Codex authentication in the developer user's private environment.
|
||||
Support browser login and the official device-code option for remote/headless
|
||||
sessions when available; API-key login is another explicit option. The
|
||||
[authentication documentation](https://learn.chatgpt.com/docs/auth) describes
|
||||
these flows. Do not collect credentials in an Archipelago transcript or copy the
|
||||
node's wallet identity into agent configuration. The UI reports Installed,
|
||||
Sign-in required, Ready or Error based on actual results.
|
||||
|
||||
Honor the user's Codex configuration, permissions and model choice. A launcher
|
||||
selects a workspace and skill context; it does not inject unrestricted execution
|
||||
flags. A local skill is local guidance, not a claim that Codex inference is local.
|
||||
Explain the selected provider and what workspace content may be sent to it during
|
||||
setup, alongside any self-hosted alternative.
|
||||
|
||||
During an ordinary disconnect, resume the same running Codex process in tmux.
|
||||
After an ended session or reboot, offer the official `codex resume` workflow in
|
||||
the matching workspace. Do not automatically reissue the last task. Preserve
|
||||
agent history/configuration separately from disposable build caches, and keep
|
||||
credentials out of ordinary app export/support bundles.
|
||||
|
||||
## Local skills that enable real work
|
||||
|
||||
Ship a small coordinated skill family with one obvious entrypoint. Keep the
|
||||
instructions actionable and references focused; avoid copying the whole manual
|
||||
into every prompt. The skill-creator guidance informs this structure: precise
|
||||
triggers, reusable resources, progressive disclosure and behavioral validation.
|
||||
|
||||
| Skill | Trigger and outcome | Required resources |
|
||||
| --- | --- | --- |
|
||||
| `archipelago` | Configure, troubleshoot or change an Archipelago system; route app/UI work to the companion skills | System map, command catalog, config ownership, task recipes and recovery |
|
||||
| `archipelago-app` | Build, package, test or update an Archipelago app using the developer contract | Developer docs, manifest schema, starter assets, launch/auth/signer examples, lifecycle checks |
|
||||
| `archipelago-design` | Create or change an Archipelago UI, including apps, setup and Terminal | Shared tokens/components, pattern gallery, app shells, screenshots and visual checks |
|
||||
|
||||
For a system task, the agent should locate the correct config/operation, inspect
|
||||
current state, make the requested scoped change, validate it and report the result.
|
||||
Teach recipes for networking, SSH, tool installation, terminal preferences,
|
||||
service diagnosis and supported app configuration. Prefer maintained management
|
||||
commands; when direct configuration is necessary, explain owned versus generated
|
||||
files, the minimal affected service and reversal. Repository changes go into a
|
||||
separate branch/worktree; installed-node changes target an explicitly identified
|
||||
node and retain a scoped backup. Planning requests stay planning requests.
|
||||
|
||||
For an app task, the agent reads `docs/app-developer-guide.md` and
|
||||
`docs/app-manifest-spec.md`, follows the design plan, chooses a starter, implements
|
||||
the requested behavior, validates it and supplies a runnable preview. Package
|
||||
with pinned images/build contexts, rootless execution, declared storage/secrets,
|
||||
health checks and truthful interfaces. Include HTTP/HTTPS, iframe/Companion,
|
||||
first-run credentials and Nostr signer behavior where relevant. Publication and
|
||||
live installation are distinct requested steps.
|
||||
|
||||
The skill must explain non-obvious runtime facts: manifests copied only into
|
||||
`/opt/archipelago/apps` are replaced at backend start; runtime payload promotion
|
||||
and signed-catalog precedence matter. Installed status is not launch readiness.
|
||||
Pre-catalog testing must not replace the signed catalog. Test uninstall/reinstall
|
||||
with data preservation on a disposable target. Use `AGENTS.md` over stale testing
|
||||
examples. Payments, wallets and uninstall decisions retain their existing
|
||||
invariants; a generic repair must not reset them.
|
||||
|
||||
Proposed source layout: `skills/archipelago*` plus a versioned reference bundle and
|
||||
app starter assets. Ship managed copies under a versioned read-only system path
|
||||
and expose one discoverable link per skill per agent. Current official
|
||||
[Codex skill guidance](https://learn.chatgpt.com/docs/build-skills) supports
|
||||
repository `.agents/skills`, user `~/.agents/skills`, administrator
|
||||
`/etc/codex/skills`, and symlinked skill directories. Qualify discovery with the
|
||||
pinned CLI, including from outside this repository. Preserve local skills and
|
||||
overrides; avoid duplicate skill names from multiple discovery paths.
|
||||
|
||||
Each bundle records the source revision, supported Archipelago version, manifest
|
||||
schema and design-kit version. Installed skills use matching offline docs;
|
||||
repository development uses that checkout's docs. Version mismatch is visible.
|
||||
Future command names in this plan must not be taught as available until shipped.
|
||||
|
||||
## App creation and design integration
|
||||
|
||||
The default new app is a Vue/TypeScript app using the shared Archipelago UI kit,
|
||||
with a pinned container build, manifest, icon, tests and local preview. Supply a
|
||||
framework-neutral CSS/token starter for existing non-Vue apps; port semantic
|
||||
behavior deliberately instead of depending on dashboard globals.
|
||||
|
||||
Provide a complete example app with a useful list/detail/settings flow, persistent
|
||||
data, loading/empty/error/success states, a manifest and lifecycle evidence.
|
||||
Extend it with optional signer/media examples only when those features are used.
|
||||
The starter must render correctly both embedded and standalone. A third-party
|
||||
upstream app can retain its own UI; the consistency contract governs the app's
|
||||
Archipelago wrapper and newly authored Archipelago screens.
|
||||
|
||||
Local development data and ports are separate from production apps. Bind previews
|
||||
to loopback by default and provide an authenticated preview path for remote users.
|
||||
Development databases get unique project-scoped names, persistent volumes and
|
||||
credentials; they must not attach to live Bitcoin/LND or production app databases.
|
||||
|
||||
## Delivery sequence
|
||||
|
||||
| Phase | Deliverable | Exit evidence |
|
||||
| --- | --- | --- |
|
||||
| 0 | Finalize command/session contracts, origin/account choice and design baseline | Reviewed wireframes, reference screens, privilege map and pinned Omarchy inventory |
|
||||
| 1A | Persistent terminal service and browser/SSH attachment | Real shell + Codex TUI; close/reopen, network loss, manager restart and ownership tests |
|
||||
| 1B | Shared design foundation, gallery and starter | Dashboard-derived tokens/components; consistent app at desktop/mobile sizes |
|
||||
| 2 | Core setup/tool payload and Codex onboarding | Fresh/offline/upgrade/retry matrix; correct account state and user override preservation |
|
||||
| 3 | System/app/design skills and app-development commands | Realistic agent tasks produce correct scoped system changes and a consistent packaged app |
|
||||
| 4 | Integrated candidate qualification | Real node and Companion resume/design acceptance, resource tests and packaged ISO/OTA checks |
|
||||
| 5 | Remaining Omarchy parity adapters | Each inventory row supported or explicitly retained with reason and acceptance target |
|
||||
|
||||
1A and 1B can be independent implementation workstreams once phase 0 contracts
|
||||
are agreed. This planning session has not launched implementation agents.
|
||||
App-generation functionality is incomplete until 1B and the skill acceptance pass.
|
||||
|
||||
Expected code boundaries: Terminal UI/store/keyboard handling; new terminal
|
||||
service and typed management routes; shared CLI/setup catalog; versioned tool and
|
||||
skill packaging; design-kit extraction; app starter/validation; ISO and OTA
|
||||
integration. Keep each in a focused contribution. Coordinate shared frontend
|
||||
styles, backend routing and packaging files before implementation starts.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
| ID | Required proof |
|
||||
| --- | --- |
|
||||
| TERM-01 | Real PTY supports editors, completion, colors, Unicode, signals and Codex; no fake production commands |
|
||||
| TERM-02 | Start a long-running fixture and edit a file, close Terminal/tab/browser, reopen and resume the exact session and process |
|
||||
| TERM-03 | Offline/reconnect, mobile sleep, frontend reload, backend restart and attachment-service restart do not duplicate execution |
|
||||
| TERM-04 | A second device resumes after owner authentication; writer transfer is atomic; another identity cannot list/attach/terminate |
|
||||
| TERM-05 | End session stops only its process scope; logout revokes access; neither operation deletes workspace files |
|
||||
| TERM-06 | Reboot retains workspace metadata, labels interrupted sessions accurately and offers Codex conversation resume without rerunning commands |
|
||||
| TERM-07 | Terminal keyboard ownership, focus, text selection, paste, resize, mobile keyboard and screen-reader mode work |
|
||||
| AUTH-01 | Missing/expired/replayed grants, cross-origin sockets, forged proxy headers and app/guest credentials fail before shell I/O |
|
||||
| AUTH-02 | Developer user cannot read production wallets/secrets or control production container sockets; authorized system workflow works |
|
||||
| SETUP-01 | Fresh install and existing-node upgrade deliver the same core capabilities; Codex version works before network access |
|
||||
| SETUP-02 | Failed download, package lock, low disk, reboot and concurrent setup recover without false completion or broken existing tools |
|
||||
| SETUP-03 | Existing dotfiles, editor/agent choices, credentials, app data and uninstall decisions are preserved |
|
||||
| AGENT-01 | A system-change task uses the correct operation/config, verifies its result and preserves unrelated services |
|
||||
| AGENT-02 | An app-building task follows actual developer docs and passes manifest/build/launch/lifecycle checks |
|
||||
| DESIGN-01 | Agent-built apps satisfy the companion design plan using shared assets/components, including failure and mobile states |
|
||||
| PKG-01 | Exact candidate OTA and ISO contain matching tools, docs, skills and UI-kit versions; restore previous payload without removing user work |
|
||||
|
||||
Backend unit execution uses `scripts/test-backend-isolated.sh`. PTY/process and
|
||||
account-isolation integration tests run in disposable users/VMs, not a funded
|
||||
production node. Terminal continuity tests use observable process IDs/output and
|
||||
file checks, not a mocked “resumed” label. UI tests include 320/390/768/1440 pixel
|
||||
layouts, landscape, enlarged text and actual Companion input on a device.
|
||||
|
||||
Record source tests, disposable integration, actual-node acceptance and packaged
|
||||
artifact results separately. Preserve unfinished requirements in
|
||||
`docs/post-1.8.22-regressions-20261001.md` and the current release acceptance ledger;
|
||||
this feature plan closes none of them. Later publication follows ngit review and
|
||||
merge, then identical accepted main/tag objects on both ngit and Gitea, with the
|
||||
required mirror checks. No release version or publication date is reserved here.
|
||||
|
||||
## Decisions to resolve during design review
|
||||
|
||||
The proposed defaults are a dedicated developer account, tmux persistence,
|
||||
minimal terminal origin, bundled Codex, Bash, and a Vue starter with portable
|
||||
design tokens. The implementation review must settle terminal-origin/certificate
|
||||
handling on every supported ingress, the supported operator-shell model, the
|
||||
exact first-release tool profile, and the approved visual baseline. Native
|
||||
terminal emulators and the broader desktop parity backlog need separate device
|
||||
compatibility decisions. None of these questions prevents reviewing this plan.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Terminal UAT deployment runbook
|
||||
|
||||
Status: prepared, 2026-10-09. This runbook targets the physical Framework node
|
||||
(`framework-pt`) and does not authorize an OTA, catalog publication, or wallet
|
||||
mutation.
|
||||
|
||||
## Candidate contents
|
||||
|
||||
Build and record the backend binary, dashboard bundle, and source commit from
|
||||
the isolated terminal worktree. The backend must have `ARCHY_SESSION_STATE_DIR`
|
||||
set to the developer account's shared state directory (or use the default
|
||||
resolution in `api/handler/terminal.rs`). Preserve the existing web root and
|
||||
service binary before any replacement.
|
||||
|
||||
## Preconditions
|
||||
|
||||
1. Verify the Framework hostname and SSH host key through the operator's
|
||||
approved connection mechanism. A plain `ssh framework-pt` must not be used
|
||||
until host-key verification is available.
|
||||
2. Capture service/container state, boot ID, running binary digest, served UI
|
||||
digest, and the existing `/var/lib/archipelago/support` layout without
|
||||
printing credentials, wallet files, or environment contents.
|
||||
3. Create a timestamped protected rollback directory under
|
||||
`/var/lib/archipelago/support/terminal-uat-<timestamp>`.
|
||||
|
||||
## Acceptance flow
|
||||
|
||||
- Open the dashboard as the node owner; unauthenticated requests to
|
||||
`/api/terminal/sessions` and `/ws/terminal` return 401.
|
||||
- Create a named session, type `printf 'uat\n'`, close the terminal, reopen it,
|
||||
and resume the same session without a duplicate tmux process.
|
||||
- Refresh the browser and reconnect after a temporary network interruption.
|
||||
- Verify the terminal panel stays bounded while output grows, keeps an inner
|
||||
scroll position at the newest line, and supports drag, resize, minimize,
|
||||
refresh, and fullscreen controls without blocking dashboard navigation.
|
||||
- Open a second owner browser and verify inventory visibility; verify only one
|
||||
active attachment sends input at a time before enabling transfer controls.
|
||||
- Confirm explicit End stops the tmux process but preserves the workspace.
|
||||
- Reboot acceptance is separate: processes may stop, metadata must remain, and
|
||||
the UI must call this interrupted rather than a live resume.
|
||||
- Verify the Omarchy-derived agent skill files and app starter are present in
|
||||
the candidate source/artifact; do not treat a local npm install failure as a
|
||||
successful app build.
|
||||
|
||||
The node's reverse proxy must route `/api/terminal/` to the Archipelago daemon
|
||||
(`127.0.0.1:5678`) in both HTTP and HTTPS server blocks. `/ws` already carries
|
||||
the terminal WebSocket upgrade. Without the REST location, the SPA fallback
|
||||
returns `index.html` instead of the authenticated session response.
|
||||
|
||||
## Rollback
|
||||
|
||||
Stop exposing the new dashboard before restoring the previous UI/backend pair.
|
||||
Restore only from the protected receipt, verify the previous hashes and health,
|
||||
and leave terminal session metadata/workspaces untouched unless the operator
|
||||
explicitly requests session cleanup.
|
||||
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
@@ -0,0 +1,16 @@
|
||||
FROM node:22-alpine AS build
|
||||
WORKDIR /src
|
||||
COPY package.json vite.config.ts index.html ./
|
||||
COPY src ./src
|
||||
RUN npm install --ignore-scripts && npm run build
|
||||
|
||||
FROM nginx:1.27-alpine
|
||||
COPY --from=build /src/dist /usr/share/nginx/html
|
||||
COPY <<'EOF' /etc/nginx/conf.d/default.conf
|
||||
server {
|
||||
listen 8080;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
location / { try_files $uri $uri/ /index.html; }
|
||||
}
|
||||
EOF
|
||||
@@ -0,0 +1,11 @@
|
||||
# Archipelago app starter
|
||||
|
||||
This is a small standalone Vue/Vite app that demonstrates Archipelago's initial
|
||||
design contract: semantic `--archy-*` tokens, dark native controls, responsive
|
||||
cards, bottom actions, visible focus, 44px targets, and loading/empty/error/
|
||||
success states. It intentionally has no dashboard dependency or host handshake.
|
||||
|
||||
Run `npm install && npm run dev` from this directory. Before packaging, copy the
|
||||
source into a project, replace the demo state with a real API, pin dependencies,
|
||||
and validate the manifest using the repository app developer guide. The Docker
|
||||
file is a starter build example; it is not a substitute for lifecycle acceptance.
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#0a0a0a" />
|
||||
<title>Archipelago app</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,48 @@
|
||||
app:
|
||||
id: archipelago-app-starter
|
||||
name: Archipelago App Starter
|
||||
version: 0.1.0
|
||||
description: A minimal Archipelago design-system starter with responsive states.
|
||||
container:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
tag: localhost/archipelago-app-starter:0.1.0
|
||||
resources:
|
||||
cpu_limit: 1
|
||||
memory_limit: 128Mi
|
||||
disk_limit: 256Mi
|
||||
security:
|
||||
capabilities: []
|
||||
readonly_root: true
|
||||
no_new_privileges: true
|
||||
network_policy: isolated
|
||||
ports:
|
||||
- host: 8180
|
||||
container: 8080
|
||||
protocol: tcp
|
||||
bind: 127.0.0.1
|
||||
auth: gated
|
||||
volumes:
|
||||
- type: bind
|
||||
source: /var/lib/archipelago/archipelago-app-starter
|
||||
target: /data
|
||||
options: [rw]
|
||||
health_check:
|
||||
type: http
|
||||
endpoint: http://127.0.0.1:8080
|
||||
path: /
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
interfaces:
|
||||
main:
|
||||
name: Archipelago App Starter
|
||||
description: Responsive design-system starter
|
||||
type: ui
|
||||
port: 8180
|
||||
protocol: http
|
||||
path: /
|
||||
metadata:
|
||||
category: development
|
||||
tier: optional
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "archipelago-app-starter",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 127.0.0.1",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview --host 127.0.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"vite": "^7.2.2",
|
||||
"typescript": "~5.9.3",
|
||||
"vue": "^3.5.24"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<main class="archy-shell" aria-labelledby="page-title">
|
||||
<header class="archy-header">
|
||||
<div>
|
||||
<p class="archy-eyebrow">ARCHIPELAGO APP</p>
|
||||
<h1 id="page-title">Project workspace</h1>
|
||||
<p class="archy-muted">A small, useful starting point for a node app.</p>
|
||||
</div>
|
||||
<button class="archy-button archy-button-secondary" type="button" @click="reload">
|
||||
Refresh
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<section class="archy-card" aria-labelledby="items-title">
|
||||
<div class="archy-section-heading">
|
||||
<div>
|
||||
<p class="archy-eyebrow">YOUR DATA</p>
|
||||
<h2 id="items-title">Items</h2>
|
||||
</div>
|
||||
<button class="archy-button" type="button" @click="addItem">Add item</button>
|
||||
</div>
|
||||
|
||||
<div v-if="status === 'loading'" class="archy-state" role="status">Loading items…</div>
|
||||
<div v-else-if="status === 'error'" class="archy-state archy-state-error" role="alert">
|
||||
<strong>Items are unavailable.</strong>
|
||||
<span>Check the app service and try again.</span>
|
||||
<button class="archy-button archy-button-secondary" type="button" @click="reload">Try again</button>
|
||||
</div>
|
||||
<div v-else-if="items.length === 0" class="archy-state">
|
||||
<strong>No items yet.</strong>
|
||||
<span>Create one to see the app's normal success state.</span>
|
||||
<button class="archy-button archy-button-secondary" type="button" @click="addItem">Create first item</button>
|
||||
</div>
|
||||
<ul v-else class="archy-list" aria-live="polite">
|
||||
<li v-for="item in items" :key="item.id" class="archy-list-row">
|
||||
<div>
|
||||
<strong>{{ item.name }}</strong>
|
||||
<span class="archy-muted">{{ item.detail }}</span>
|
||||
</div>
|
||||
<span class="archy-status">Ready</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<p class="archy-footnote">This starter is standalone-safe and does not require a host handshake.</p>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
type Item = { id: number; name: string; detail: string }
|
||||
const status = ref<'ready' | 'loading' | 'error'>('ready')
|
||||
const items = ref<Item[]>([])
|
||||
let nextId = 1
|
||||
|
||||
function addItem() {
|
||||
status.value = 'ready'
|
||||
items.value.push({ id: nextId, name: `Item ${nextId}`, detail: 'Persist this through your app API.' })
|
||||
nextId += 1
|
||||
}
|
||||
|
||||
function reload() {
|
||||
status.value = 'loading'
|
||||
window.setTimeout(() => { status.value = 'ready' }, 250)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import './styles.css'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
@@ -0,0 +1,54 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--archy-canvas: #0a0a0a;
|
||||
--archy-surface: rgba(0, 0, 0, 0.65);
|
||||
--archy-surface-muted: rgba(255, 255, 255, 0.06);
|
||||
--archy-border: rgba(255, 255, 255, 0.18);
|
||||
--archy-text: rgba(255, 255, 255, 0.92);
|
||||
--archy-muted: rgba(255, 255, 255, 0.58);
|
||||
--archy-accent: #fb923c;
|
||||
--archy-success: #4ade80;
|
||||
--archy-danger: #f87171;
|
||||
--archy-radius-card: 16px;
|
||||
--archy-radius-control: 12px;
|
||||
--archy-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
|
||||
--archy-space-1: 4px;
|
||||
--archy-space-2: 8px;
|
||||
--archy-space-3: 12px;
|
||||
--archy-space-4: 16px;
|
||||
--archy-space-6: 24px;
|
||||
--archy-space-8: 32px;
|
||||
font-family: Avenir Next, system-ui, sans-serif;
|
||||
background: var(--archy-canvas);
|
||||
color: var(--archy-text);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; min-width: 320px; background: var(--archy-canvas); }
|
||||
button { font: inherit; }
|
||||
button:focus-visible { outline: 2px solid var(--archy-accent); outline-offset: 3px; }
|
||||
|
||||
.archy-shell { width: min(100% - 32px, 960px); margin: 0 auto; padding: 40px 0 56px; }
|
||||
.archy-header, .archy-section-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--archy-space-4); }
|
||||
.archy-header { margin-bottom: var(--archy-space-8); }
|
||||
.archy-eyebrow { margin: 0 0 var(--archy-space-2); color: var(--archy-accent); font-size: 0.72rem; font-weight: 800; letter-spacing: 0.12em; }
|
||||
h1, h2 { margin: 0; line-height: 1.15; }
|
||||
h1 { font-size: clamp(1.8rem, 5vw, 2.8rem); }
|
||||
h2 { font-size: 1.25rem; }
|
||||
.archy-muted, .archy-footnote { color: var(--archy-muted); }
|
||||
.archy-card { padding: var(--archy-space-6); background: var(--archy-surface); border: 1px solid var(--archy-border); border-radius: var(--archy-radius-card); box-shadow: var(--archy-shadow); }
|
||||
.archy-section-heading { align-items: center; margin-bottom: var(--archy-space-6); }
|
||||
.archy-button { min-height: 44px; padding: 10px 18px; border: 1px solid rgba(251, 146, 60, 0.35); border-radius: var(--archy-radius-control); background: rgba(251, 146, 60, 0.2); color: #fed7aa; cursor: pointer; }
|
||||
.archy-button:hover { background: rgba(251, 146, 60, 0.3); }
|
||||
.archy-button-secondary { border-color: var(--archy-border); background: var(--archy-surface-muted); color: var(--archy-text); }
|
||||
.archy-state { display: grid; gap: var(--archy-space-3); padding: var(--archy-space-6); border-radius: var(--archy-radius-control); background: var(--archy-surface-muted); color: var(--archy-muted); }
|
||||
.archy-state strong { color: var(--archy-text); }
|
||||
.archy-state-error { border: 1px solid rgba(248, 113, 113, 0.35); }
|
||||
.archy-state-error strong { color: var(--archy-danger); }
|
||||
.archy-list { display: grid; gap: var(--archy-space-2); padding: 0; margin: 0; list-style: none; }
|
||||
.archy-list-row { display: flex; align-items: center; justify-content: space-between; gap: var(--archy-space-4); padding: var(--archy-space-4); border-radius: var(--archy-radius-control); background: var(--archy-surface-muted); }
|
||||
.archy-list-row div { display: grid; gap: var(--archy-space-1); min-width: 0; }
|
||||
.archy-status { color: var(--archy-success); font-size: 0.8rem; }
|
||||
.archy-footnote { margin: var(--archy-space-4) 0 0; font-size: 0.8rem; }
|
||||
@media (max-width: 560px) { .archy-shell { width: min(100% - 24px, 960px); padding-top: 24px; } .archy-header { flex-direction: column; } .archy-header .archy-button { width: 100%; } .archy-card { padding: var(--archy-space-4); } .archy-section-heading { align-items: stretch; flex-direction: column; } .archy-section-heading .archy-button { width: 100%; } }
|
||||
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; } }
|
||||
@@ -0,0 +1,4 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({ plugins: [vue()] })
|
||||
Generated
+17
@@ -11,6 +11,8 @@
|
||||
"@scure/bip39": "^2.2.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"buffer": "^6.0.3",
|
||||
"d3": "^7.9.0",
|
||||
"dompurify": "^3.3.3",
|
||||
@@ -4574,6 +4576,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@xterm/addon-fit": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz",
|
||||
"integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@xterm/xterm": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@xterm/xterm": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/abbrev": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
"@scure/bip39": "^2.2.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"buffer": "^6.0.3",
|
||||
"d3": "^7.9.0",
|
||||
"dompurify": "^3.3.3",
|
||||
|
||||
@@ -1,298 +1,220 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="cli-popup">
|
||||
<div
|
||||
v-if="cliStore.isOpen"
|
||||
class="fixed inset-0 z-[2500] flex items-center justify-center p-4"
|
||||
@click.self="cliStore.close()"
|
||||
>
|
||||
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
|
||||
<div
|
||||
ref="panelRef"
|
||||
class="glass-card w-full max-w-2xl relative z-10 overflow-hidden flex flex-col"
|
||||
:style="panelStyle"
|
||||
@mousedown="onPanelMouseDown"
|
||||
>
|
||||
<!-- Header: terminal icon + title + app switcher -->
|
||||
<div class="flex items-center gap-3 px-4 py-3 border-b border-white/10">
|
||||
<div
|
||||
ref="dragHandleRef"
|
||||
class="flex items-center justify-center w-8 h-8 rounded cursor-grab hover:bg-white/10 transition-colors shrink-0"
|
||||
:class="{ 'cursor-grabbing': isDragging }"
|
||||
title="Drag to move"
|
||||
>
|
||||
<svg class="w-4 h-4 text-white/50" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 6h2v2H8V6zm0 5h2v2H8v-2zm0 5h2v2H8v-2zm5-10h2v2h-2V6zm0 5h2v2h-2v-2zm0 5h2v2h-2v-2z" />
|
||||
</svg>
|
||||
<div v-if="cliStore.isOpen" class="fixed inset-0 z-[2500] flex items-center justify-start p-4 pointer-events-none">
|
||||
<section ref="panelRef" data-archipelago-terminal class="glass-card terminal-panel relative z-10 w-full overflow-hidden pointer-events-auto md:ml-[300px]" :class="{ minimized }" :style="panelStyle" aria-label="Archipelago terminal">
|
||||
<header class="terminal-header sticky top-0 z-10 flex items-center gap-3 border-b border-white/10 px-4 py-3 bg-black/60 backdrop-blur-md md:bg-transparent md:backdrop-blur-none" @mousedown="startDrag">
|
||||
<div class="terminal-drag-handle" title="Drag to move">
|
||||
<svg class="w-4 h-4 text-white/50" fill="currentColor" viewBox="0 0 24 24"><path d="M8 6h2v2H8V6zm0 5h2v2H8v-2zm0 5h2v2H8v-2zm5-10h2v2h-2V6zm0 5h2v2h-2v-2zm0 5h2v2h-2v-2z" /></svg>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 flex-1 min-w-0">
|
||||
<svg class="w-5 h-5 text-white/60 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span class="text-white font-medium">CLI Access</span>
|
||||
<div class="terminal-title"><svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg><span>CLI Access</span><span class="terminal-host">{{ activeSession?.name || 'No session' }}</span></div>
|
||||
<div class="terminal-actions">
|
||||
<select v-model="selectedId" class="terminal-select" aria-label="Terminal session" @change="connectSelected">
|
||||
<option value="">Select session</option>
|
||||
<option v-for="session in sessions" :key="session.id" :value="session.id">{{ session.name }} · {{ session.state }}</option>
|
||||
</select>
|
||||
<button class="terminal-icon-button" type="button" @click="createSession">New</button>
|
||||
<button class="terminal-icon-button" type="button" aria-label="Refresh" title="Refresh" @click="refreshTerminal"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /></svg></button>
|
||||
<button class="terminal-icon-button" type="button" :aria-label="minimized ? 'Restore terminal' : 'Minimize terminal'" @click="minimized = !minimized"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="minimized ? 'M5 12h14' : 'M6 12h12'" /></svg></button>
|
||||
<button class="terminal-icon-button" type="button" :aria-label="fullscreen ? 'Windowed' : 'Fullscreen'" @click="toggleFullscreen"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="fullscreen ? 'M8 3v5H3m18 0h-5V3M3 16h5v5m8 0v-5h5' : 'M8 3H3v5m13-5h5v5M3 16v5h5m8 0h5v-5'" /></svg></button>
|
||||
<button class="terminal-icon-button" type="button" aria-label="Close" @click="closeTerminal"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg></button>
|
||||
</div>
|
||||
<kbd class="hidden sm:inline-flex px-2 py-1 text-xs text-white/50 bg-white/10 rounded">Esc</kbd>
|
||||
</header>
|
||||
<div v-show="!minimized" class="terminal-body">
|
||||
<div v-if="error" class="terminal-empty terminal-error"><strong>Terminal unavailable</strong><span>{{ error }}</span><button class="terminal-button" type="button" @click="loadSessions">Retry</button></div>
|
||||
<div v-else-if="!sessions.length" class="terminal-empty"><strong>Create your first developer session</strong><span>Sessions stay running when this window closes and can be resumed later.</span><button class="terminal-button terminal-primary" type="button" @click="createSession">Start session</button></div>
|
||||
<div v-else-if="!selectedId" class="terminal-empty"><strong>Choose a session</strong><span>Resume an existing workspace or start a new one.</span></div>
|
||||
<div v-else ref="terminalRef" class="terminal-output" tabindex="0" aria-label="Archipelago terminal session" @pointerdown.stop="focusInput" @click.stop="focusInput" @keydown.stop @keyup.stop @keypress.stop></div>
|
||||
</div>
|
||||
|
||||
<!-- Content: mock CLI in dev, SSH instructions in production -->
|
||||
<div class="flex-1 overflow-hidden flex flex-col min-h-0">
|
||||
<!-- Mock CLI interface (dev mode only) -->
|
||||
<div
|
||||
v-if="isDev"
|
||||
class="flex-1 flex flex-col min-h-0 p-4 bg-black/80 rounded-b-lg font-mono text-sm"
|
||||
>
|
||||
<div ref="outputRef" class="flex-1 overflow-y-auto text-green-400/90 whitespace-pre-wrap break-words mb-2 min-h-0">{{ mockOutput }}</div>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<span class="text-amber-400">archipelago@node</span>
|
||||
<span class="text-white/60">~</span>
|
||||
<span class="text-white/40">$</span>
|
||||
<input
|
||||
ref="cliInputRef"
|
||||
v-model="mockCommand"
|
||||
type="text"
|
||||
class="flex-1 bg-transparent text-white outline-none border-none"
|
||||
placeholder=" "
|
||||
@keydown.enter="runMockCommand"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SSH instructions (production only) -->
|
||||
<div v-else class="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
<p class="text-white/80 text-sm">
|
||||
Connect to this node via SSH to access the command line. Use the same host as this web interface.
|
||||
</p>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="p-3 rounded-lg bg-white/5 font-mono text-sm">
|
||||
<div class="text-white/50 text-xs uppercase tracking-wider mb-2">SSH Command</div>
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<code class="text-green-400 break-all">{{ sshCommand }}</code>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 px-2 py-1 rounded bg-white/10 text-white/80 hover:bg-white/20 hover:text-white text-xs transition-colors"
|
||||
@click="copyCommand"
|
||||
>
|
||||
{{ copied ? 'Copied!' : 'Copy' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-3 rounded-lg bg-white/5 text-sm space-y-1">
|
||||
<div class="text-white/50 text-xs uppercase tracking-wider mb-2">Connection Details</div>
|
||||
<div class="flex flex-col gap-1.5 text-white/80">
|
||||
<div class="flex justify-between gap-4">
|
||||
<span class="text-white/50">Host</span>
|
||||
<span class="font-mono text-green-400">{{ host }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4">
|
||||
<span class="text-white/50">User</span>
|
||||
<span class="font-mono">archipelago</span>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4">
|
||||
<span class="text-white/50">Password</span>
|
||||
<span class="font-mono">archipelago</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-white/50 text-xs">
|
||||
From the terminal menu you can install to disk, configure Bitcoin, Lightning, view logs, and more.
|
||||
</p>
|
||||
<p class="text-white/40 text-xs">
|
||||
Tip: Press <kbd class="px-1.5 py-0.5 rounded bg-white/10 font-mono text-[10px]">F</kbd> to open this anytime.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer v-if="selectedId && !minimized" class="terminal-input-bar">
|
||||
<span class="terminal-status" :class="{ connected: connected }">{{ connected ? 'Connected' : 'Reconnecting…' }}</span>
|
||||
<span class="terminal-hint">Click terminal to focus · drag the header to move · resize from the corner</span>
|
||||
</footer>
|
||||
</section>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { Terminal } from '@xterm/xterm'
|
||||
import { FitAddon } from '@xterm/addon-fit'
|
||||
import '@xterm/xterm/css/xterm.css'
|
||||
import { useCLIStore } from '@/stores/cli'
|
||||
import { useModalKeyboard } from '@/composables/useModalKeyboard'
|
||||
|
||||
type Session = { id: string; name: string; workspace: string; state: string; updated_at: number }
|
||||
const cliStore = useCLIStore()
|
||||
const panelRef = ref<HTMLElement | null>(null)
|
||||
const dragHandleRef = ref<HTMLElement | null>(null)
|
||||
const outputRef = ref<HTMLElement | null>(null)
|
||||
const cliInputRef = ref<HTMLInputElement | null>(null)
|
||||
const copied = ref(false)
|
||||
const minimized = ref(false)
|
||||
const fullscreen = ref(false)
|
||||
const position = ref({ x: 0, y: 0 })
|
||||
const dragging = ref<{ x: number; y: number; px: number; py: number } | null>(null)
|
||||
const sessions = ref<Session[]>([])
|
||||
const selectedId = ref('')
|
||||
const error = ref('')
|
||||
const connected = ref(false)
|
||||
const terminalRef = ref<HTMLElement | null>(null)
|
||||
let terminal: Terminal | null = null
|
||||
let fitAddon: FitAddon | null = null
|
||||
let resizeObserver: ResizeObserver | null = null
|
||||
let socket: WebSocket | null = null
|
||||
let renderedSession = ''
|
||||
let connectionGeneration = 0
|
||||
|
||||
const mockCommand = ref('')
|
||||
const mockOutput = ref(` ╔═══════════════════════════════════════════════════════════╗
|
||||
║ 🏝️ ARCHIPELAGO BITCOIN NODE OS ║
|
||||
║ Your sovereign Bitcoin infrastructure ║
|
||||
╚═══════════════════════════════════════════════════════════╝
|
||||
const activeSession = computed(() => sessions.value.find(session => session.id === selectedId.value))
|
||||
const panelStyle = computed(() => ({ transform: `translate(${position.value.x}px, ${position.value.y}px)` }))
|
||||
const wsUrl = (id: string) => `${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.host}/ws/terminal?session=${encodeURIComponent(id)}`
|
||||
function compactSnapshot(data: string) {
|
||||
const lines = data.split(/\r?\n/)
|
||||
const visible = (line: string) => line.replace(/\u001b\[[0-9;?]*[ -/]*[@-~]/g, '').trim().length > 0
|
||||
while (lines.length && !visible(lines[0] ?? '')) lines.shift()
|
||||
while (lines.length && !visible(lines[lines.length - 1] ?? '')) lines.pop()
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
System Status:
|
||||
─────────────────────────────────────────────────────────────
|
||||
Mode: 🟢 Installed
|
||||
Podman: 🟢 Installed
|
||||
Bitcoin: 🟢 Running (blocks: syncing)
|
||||
Lightning: 🟡 Stopped
|
||||
|
||||
Main Menu:
|
||||
─────────────────────────────────────────────────────────────
|
||||
r) Refresh - Update IP/status
|
||||
w) Open Web UI - Launch graphical interface
|
||||
1) Install to Disk - Permanently install Archipelago
|
||||
2) Setup Bitcoin Core - Configure Bitcoin full node
|
||||
3) Setup Lightning (LND) - Configure Lightning Network
|
||||
4) Setup BTCPay Server - Bitcoin payment processor
|
||||
5) View Logs - Monitor running services
|
||||
6) Network Settings - Configure networking
|
||||
7) System Info - View system information
|
||||
q) Quit
|
||||
|
||||
`)
|
||||
|
||||
const isDragging = ref(false)
|
||||
const dragStart = ref<{ x: number; y: number; panelX: number; panelY: number } | null>(null)
|
||||
|
||||
const SAVED_POSITION_KEY = 'archipelago-cli-position'
|
||||
const savedPosition = ref<{ x: number; y: number } | null>(null)
|
||||
|
||||
const isDev = import.meta.env.DEV
|
||||
const host = computed(() => window.location.hostname)
|
||||
const sshCommand = computed(() => `ssh archipelago@${host.value}`)
|
||||
|
||||
const panelStyle = computed(() => {
|
||||
const pos = savedPosition.value
|
||||
if (!pos) return {}
|
||||
return {
|
||||
transform: `translate(${pos.x}px, ${pos.y}px)`,
|
||||
margin: 0,
|
||||
}
|
||||
})
|
||||
|
||||
function loadSavedPosition() {
|
||||
async function loadSessions() {
|
||||
error.value = ''
|
||||
try {
|
||||
const raw = localStorage.getItem(SAVED_POSITION_KEY)
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw)
|
||||
savedPosition.value = { x: parsed.x ?? 0, y: parsed.y ?? 0 }
|
||||
} else {
|
||||
savedPosition.value = null
|
||||
}
|
||||
} catch (e) {
|
||||
if (import.meta.env.DEV) console.warn('Failed to load saved CLI position', e)
|
||||
savedPosition.value = null
|
||||
}
|
||||
const response = await fetch('/api/terminal/sessions', { credentials: 'include' })
|
||||
if (!response.ok) throw new Error(response.status === 401 ? 'Sign in to use the developer terminal.' : `Session service returned ${response.status}.`)
|
||||
sessions.value = await response.json()
|
||||
if (!selectedId.value && sessions.value[0]) selectedId.value = sessions.value[0].id
|
||||
if (selectedId.value) connectSelected()
|
||||
} catch (cause) { error.value = cause instanceof Error ? cause.message : 'Could not load terminal sessions.' }
|
||||
}
|
||||
|
||||
function savePosition(x: number, y: number) {
|
||||
savedPosition.value = { x, y }
|
||||
async function createSession() {
|
||||
error.value = ''
|
||||
try {
|
||||
localStorage.setItem(SAVED_POSITION_KEY, JSON.stringify({ x, y }))
|
||||
} catch (e) {
|
||||
if (import.meta.env.DEV) console.warn('Failed to save CLI position', e)
|
||||
const response = await fetch('/api/terminal/sessions', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Archipelago work' }) })
|
||||
if (!response.ok) throw new Error('Could not create a developer session.')
|
||||
const session = await response.json()
|
||||
sessions.value = [session, ...sessions.value]
|
||||
selectedId.value = session.id
|
||||
connectSelected()
|
||||
} catch (cause) { error.value = cause instanceof Error ? cause.message : 'Could not create terminal session.' }
|
||||
}
|
||||
|
||||
function closeTerminal() { disconnect(); cliStore.close() }
|
||||
function refreshTerminal() { if (selectedId.value) { terminal?.reset(); connectSelected() } else loadSessions() }
|
||||
function startDrag(event: MouseEvent) {
|
||||
if ((event.target as HTMLElement).closest('button, select, input')) return
|
||||
dragging.value = { x: event.clientX, y: event.clientY, px: position.value.x, py: position.value.y }
|
||||
}
|
||||
function onDrag(event: MouseEvent) {
|
||||
if (!dragging.value) return
|
||||
position.value = { x: dragging.value.px + event.clientX - dragging.value.x, y: dragging.value.py + event.clientY - dragging.value.y }
|
||||
}
|
||||
function stopDrag() { dragging.value = null }
|
||||
async function toggleFullscreen() {
|
||||
if (!panelRef.value) return
|
||||
if (document.fullscreenElement === panelRef.value) { await document.exitFullscreen?.(); fullscreen.value = false; return }
|
||||
try { await panelRef.value.requestFullscreen?.(); fullscreen.value = true } catch { fullscreen.value = false }
|
||||
}
|
||||
function disconnect() { socket?.close(); socket = null; connected.value = false }
|
||||
function connectSelected() {
|
||||
disconnect()
|
||||
if (!selectedId.value) return
|
||||
if (renderedSession !== selectedId.value) {
|
||||
terminal?.reset()
|
||||
renderedSession = selectedId.value
|
||||
}
|
||||
}
|
||||
|
||||
function runMockCommand() {
|
||||
const cmd = mockCommand.value.trim()
|
||||
if (!cmd) return
|
||||
mockOutput.value += `\n archipelago@node ~ $ ${cmd}\n`
|
||||
const lower = cmd.toLowerCase()
|
||||
if (lower === 'r' || lower === 'refresh') {
|
||||
mockOutput.value += ` Status refreshed.\n`
|
||||
} else if (lower === 'w' || lower.startsWith('web')) {
|
||||
mockOutput.value += ` Opening Web UI... (press C to return to CLI)\n`
|
||||
} else if (lower === 'q' || lower === 'quit' || lower === 'exit') {
|
||||
mockOutput.value += ` Goodbye! 🏝️\n`
|
||||
cliStore.close()
|
||||
} else if (lower === 'help' || lower === '?') {
|
||||
mockOutput.value += ` Type r, w, 1-7, or q. Press C to switch to Web UI.\n`
|
||||
} else {
|
||||
mockOutput.value += ` Unknown command. Type 'help' or 'r' for menu.\n`
|
||||
}
|
||||
mockCommand.value = ''
|
||||
nextTick(() => {
|
||||
outputRef.value?.scrollTo({ top: outputRef.value.scrollHeight, behavior: 'smooth' })
|
||||
})
|
||||
}
|
||||
|
||||
async function copyCommand() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(sshCommand.value)
|
||||
copied.value = true
|
||||
setTimeout(() => {
|
||||
copied.value = false
|
||||
}, 2000)
|
||||
} catch {
|
||||
// Fallback for older browsers
|
||||
const textarea = document.createElement('textarea')
|
||||
textarea.value = sshCommand.value
|
||||
document.body.appendChild(textarea)
|
||||
textarea.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(textarea)
|
||||
copied.value = true
|
||||
setTimeout(() => {
|
||||
copied.value = false
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
function onPanelMouseDown(e: MouseEvent) {
|
||||
if (!dragHandleRef.value?.contains(e.target as Node)) return
|
||||
isDragging.value = true
|
||||
const rect = panelRef.value?.getBoundingClientRect()
|
||||
if (!rect) return
|
||||
const currentX = savedPosition.value?.x ?? 0
|
||||
const currentY = savedPosition.value?.y ?? 0
|
||||
dragStart.value = { x: e.clientX, y: e.clientY, panelX: currentX, panelY: currentY }
|
||||
}
|
||||
|
||||
function onMouseMove(e: MouseEvent) {
|
||||
if (!dragStart.value) return
|
||||
const dx = e.clientX - dragStart.value.x
|
||||
const dy = e.clientY - dragStart.value.y
|
||||
savePosition(dragStart.value.panelX + dx, dragStart.value.panelY + dy)
|
||||
}
|
||||
|
||||
function onMouseUp() {
|
||||
isDragging.value = false
|
||||
dragStart.value = null
|
||||
}
|
||||
|
||||
useModalKeyboard(panelRef, computed(() => cliStore.isOpen), () => cliStore.close())
|
||||
|
||||
watch(
|
||||
() => cliStore.isOpen,
|
||||
(open) => {
|
||||
if (open) {
|
||||
loadSavedPosition()
|
||||
if (isDev) {
|
||||
nextTick(() => cliInputRef.value?.focus())
|
||||
const generation = ++connectionGeneration
|
||||
const connection = new WebSocket(wsUrl(selectedId.value))
|
||||
socket = connection
|
||||
connection.onopen = () => { if (generation !== connectionGeneration) return; connected.value = true; resizeTerminal() }
|
||||
connection.onclose = () => { if (generation === connectionGeneration) connected.value = false }
|
||||
connection.onerror = () => { if (generation === connectionGeneration) connected.value = false }
|
||||
connection.onmessage = event => {
|
||||
if (generation !== connectionGeneration) return
|
||||
try {
|
||||
const message = JSON.parse(event.data)
|
||||
if (message.type === 'output') {
|
||||
// The daemon sends a complete visible-pane snapshot, not a byte
|
||||
// stream. Replace the pane so every prompt does not duplicate the
|
||||
// previous screen into xterm scrollback.
|
||||
terminal?.reset()
|
||||
terminal?.write(compactSnapshot(message.data), () => terminal?.scrollToBottom())
|
||||
}
|
||||
}
|
||||
} catch { /* ignore malformed frames */ }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadSavedPosition()
|
||||
window.addEventListener('mousemove', onMouseMove)
|
||||
window.addEventListener('mouseup', onMouseUp)
|
||||
function resizeTerminal() {
|
||||
fitAddon?.fit()
|
||||
if (socket?.readyState === WebSocket.OPEN && terminal) socket.send(JSON.stringify({ type: 'resize', cols: terminal.cols, rows: terminal.rows }))
|
||||
}
|
||||
|
||||
function send(data: string) { if (socket?.readyState === WebSocket.OPEN) socket.send(JSON.stringify({ type: 'input', data })) }
|
||||
function focusInput() { nextTick(() => terminal?.focus()) }
|
||||
watch(() => cliStore.isOpen, open => { if (open) loadSessions(); else disconnect() })
|
||||
watch(terminalRef, element => {
|
||||
if (!element || !terminal || element.querySelector('.xterm')) return
|
||||
terminal.open(element)
|
||||
fitAddon?.fit()
|
||||
resizeObserver?.observe(element)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('mousemove', onDrag)
|
||||
window.addEventListener('mouseup', stopDrag)
|
||||
terminal = new Terminal({
|
||||
convertEol: true,
|
||||
cursorBlink: true,
|
||||
cursorInactiveStyle: 'none',
|
||||
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
|
||||
fontSize: 13,
|
||||
lineHeight: 1.45,
|
||||
scrollback: 10000,
|
||||
theme: { background: 'transparent', foreground: '#f3f4f6', cursor: '#f7931a', selectionBackground: 'rgba(247,147,26,.3)' },
|
||||
})
|
||||
fitAddon = new FitAddon()
|
||||
terminal.loadAddon(fitAddon)
|
||||
terminal.onData(data => send(data))
|
||||
resizeObserver = new ResizeObserver(() => resizeTerminal())
|
||||
if (terminalRef.value) {
|
||||
terminal.open(terminalRef.value)
|
||||
resizeObserver.observe(terminalRef.value)
|
||||
}
|
||||
nextTick(resizeTerminal)
|
||||
})
|
||||
function blurTerminalWhenOutside(event: PointerEvent) {
|
||||
if (panelRef.value && !panelRef.value.contains(event.target as Node)) terminal?.blur()
|
||||
}
|
||||
onMounted(() => window.addEventListener('pointerdown', blurTerminalWhenOutside, true))
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('mousemove', onMouseMove)
|
||||
window.removeEventListener('mouseup', onMouseUp)
|
||||
window.removeEventListener('mousemove', onDrag)
|
||||
window.removeEventListener('mouseup', stopDrag)
|
||||
window.removeEventListener('pointerdown', blurTerminalWhenOutside, true)
|
||||
resizeObserver?.disconnect()
|
||||
terminal?.dispose()
|
||||
disconnect()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cli-popup-enter-active,
|
||||
.cli-popup-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
.cli-popup-enter-from,
|
||||
.cli-popup-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
.terminal-panel { display: flex; flex-direction: column; width: min(960px, calc(100vw - 2rem)); height: min(680px, calc(100vh - 2rem)); min-width: 420px; min-height: 260px; max-width: calc(100vw - 2rem); max-height: calc(100vh - 2rem); resize: both; }
|
||||
.terminal-panel.minimized { width: auto; height: auto; min-width: 320px; resize: none; }
|
||||
.terminal-panel:fullscreen { width: 100vw; height: 100vh; max-width: none; max-height: none; border-radius: 0; }
|
||||
.terminal-header, .terminal-input-bar { display: flex; align-items: center; gap: 12px; padding: 13px 16px; }
|
||||
.terminal-header { cursor: grab; user-select: none; }
|
||||
.terminal-header:active { cursor: grabbing; }
|
||||
.terminal-title, .terminal-actions { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
||||
.terminal-title { flex: 1; font-weight: 500; } .terminal-host { color: rgba(255,255,255,.45); font-size: 12px; font-weight: 400; }
|
||||
.terminal-drag-handle { display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 6px; flex-shrink: 0; }
|
||||
.terminal-drag-handle:hover { background: rgba(255,255,255,.1); }
|
||||
.terminal-body { display: flex; flex: 1 1 0; min-height: 0; max-height: 100%; height: 0; overflow: hidden; }
|
||||
.terminal-output { box-sizing: border-box; flex: 1 1 auto; min-height: 0; min-width: 0; height: 100%; max-height: 100%; margin: 0; overflow: hidden; padding: 14px 16px; color: rgba(255,255,255,.9); outline: none; }
|
||||
.terminal-output :deep(.xterm) { height: 100%; width: 100%; }
|
||||
.terminal-output :deep(.xterm-viewport) { background: transparent !important; }
|
||||
.terminal-output:not(:focus-within) :deep(.xterm-cursor) { visibility: hidden !important; }
|
||||
.terminal-output:focus-within :deep(.xterm-cursor) { visibility: visible !important; }
|
||||
.terminal-input-bar { border-top: 1px solid rgba(255,255,255,.1); } .terminal-prompt { color: #f7931a; font-size: 20px; } .terminal-input { flex: 1; min-width: 0; border: 0; outline: 0; background: transparent; color: white; font: 13px ui-monospace, monospace; }
|
||||
.terminal-status { color: rgba(255,255,255,.5); font-size: 11px; } .terminal-status.connected { color: #92d6a0; }
|
||||
.terminal-hint { margin-left: auto; color: rgba(255,255,255,.35); font-size: 11px; }
|
||||
.terminal-select, .terminal-icon-button, .terminal-button { min-height: 36px; border-radius: 9px; border: 1px solid rgba(255,255,255,.12); background: rgba(255,255,255,.06); color: white; padding: 0 9px; font-size: 12px; }
|
||||
.terminal-icon-button:hover, .terminal-button:hover, .terminal-select:hover { background: rgba(255,255,255,.13); }
|
||||
.terminal-primary { background: #f7931a; border-color: #f7931a; color: #17100a; font-weight: 700; }
|
||||
.terminal-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; height: 100%; min-height: 300px; padding: 32px; text-align: center; color: rgba(255,255,255,.55); } .terminal-empty strong { color: white; font-size: 16px; } .terminal-error strong { color: #ffaaa6; }
|
||||
@media (max-width: 640px) { .terminal-panel { width: calc(100vw - 1rem); height: calc(100vh - 1rem); max-width: calc(100vw - 1rem); max-height: calc(100vh - 1rem); } .terminal-header { align-items: flex-start; flex-direction: column; } .terminal-actions { width: 100%; flex-wrap: wrap; } .terminal-select { flex: 1; min-width: 0; } .terminal-status { display: none; } }
|
||||
</style>
|
||||
|
||||
@@ -252,6 +252,7 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
|
||||
// ─── Keyboard Handler ───────────────────────────────────────
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if ((e.target as HTMLElement | null)?.closest?.('[data-archipelago-terminal]')) return
|
||||
const navKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Enter', 'Escape']
|
||||
if (!navKeys.includes(e.key)) return
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ export function useModalKeyboard(
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (!isOpen.value) return
|
||||
if ((e.target as HTMLElement | null)?.closest?.('[data-archipelago-terminal]')) return
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
restoreFocusRef?.value?.focus?.()
|
||||
|
||||
Executable
+96
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env bash
|
||||
# Idempotent Archipelago developer environment bootstrap.
|
||||
# Check by default; package installation requires --apply.
|
||||
set -euo pipefail
|
||||
|
||||
APPLY=0
|
||||
# Codex is part of the standard developer environment. Keep --codex as a
|
||||
# backwards-compatible no-op for existing setup commands.
|
||||
CODEX=1
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--apply) APPLY=1 ;;
|
||||
--codex) CODEX=1 ;;
|
||||
--help|-h) printf 'Usage: archy-developer-setup [--check] [--apply] [--codex]\n\nCodex is installed by default with --apply; authenticate with: codex login\n'; exit 0 ;;
|
||||
--check) ;;
|
||||
*) printf 'archy-developer-setup: unknown option: %s\n' "$arg" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
tools=(git tmux jq fzf rg fd nvim podman mise direnv)
|
||||
missing=()
|
||||
for tool in "${tools[@]}"; do
|
||||
if command -v "$tool" >/dev/null 2>&1; then
|
||||
printf 'ok\t%s\t%s\n' "$tool" "$(command -v "$tool")"
|
||||
else
|
||||
missing+=("$tool")
|
||||
printf 'missing\t%s\n' "$tool"
|
||||
fi
|
||||
done
|
||||
|
||||
if command -v codex >/dev/null 2>&1; then
|
||||
printf 'ok\tcodex\t%s\n' "$(command -v codex)"
|
||||
elif (( CODEX )); then
|
||||
missing+=(codex)
|
||||
printf 'missing\tcodex\n'
|
||||
fi
|
||||
|
||||
user_home=$(getent passwd "$(id -u)" | cut -d: -f6)
|
||||
codex_root=${CODEX_HOME:-$user_home/.codex}
|
||||
skill_source_dir=${ARCHY_SKILLS_DIR:-$PWD/.agents/skills}
|
||||
skills=(archipelago archipelago-app archipelago-design)
|
||||
missing_skills=()
|
||||
for skill in "${skills[@]}"; do
|
||||
source_skill="$skill_source_dir/$skill/SKILL.md"
|
||||
target_skill="$codex_root/skills/$skill/SKILL.md"
|
||||
if [ -f "$target_skill" ]; then
|
||||
printf 'ok\tskill:%s\t%s\n' "$skill" "$target_skill"
|
||||
elif [ -f "$source_skill" ]; then
|
||||
missing_skills+=("skill:$skill")
|
||||
printf 'missing\tskill:%s\t%s\n' "$skill" "$target_skill"
|
||||
else
|
||||
printf 'missing\tskill:%s\tsource unavailable (%s)\n' "$skill" "$source_skill"
|
||||
fi
|
||||
done
|
||||
|
||||
if (( ! APPLY )); then
|
||||
if [ "${#missing[@]}" -gt 0 ] || [ "${#missing_skills[@]}" -gt 0 ]; then
|
||||
printf '\nMissing: %s %s\n' "${missing[*]}" "${missing_skills[*]}"
|
||||
printf 'Re-run with --apply to install missing packages, including Codex.\n'
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! command -v apt-get >/dev/null 2>&1; then
|
||||
printf 'No apt-get found; install the missing tools using the node distribution package manager.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
packages=(git tmux jq fzf ripgrep fd-find neovim podman direnv nodejs npm)
|
||||
if [ "${#missing[@]}" -gt 0 ]; then
|
||||
sudo -n apt-get update
|
||||
sudo -n apt-get install -y "${packages[@]}"
|
||||
fi
|
||||
|
||||
if (( CODEX )) && ! command -v codex >/dev/null 2>&1; then
|
||||
command -v npm >/dev/null 2>&1 || { printf 'npm is required for Codex installation.\n' >&2; exit 1; }
|
||||
sudo -n npm install --global @openai/codex
|
||||
fi
|
||||
|
||||
if [ "${#missing_skills[@]}" -gt 0 ]; then
|
||||
mkdir -p "$codex_root/skills"
|
||||
for skill in "${skills[@]}"; do
|
||||
source_dir="$skill_source_dir/$skill"
|
||||
target_dir="$codex_root/skills/$skill"
|
||||
[ -f "$source_dir/SKILL.md" ] || continue
|
||||
mkdir -p "$target_dir"
|
||||
cp -a "$source_dir/." "$target_dir/"
|
||||
printf 'installed\tskill:%s\t%s\n' "$skill" "$target_dir"
|
||||
done
|
||||
fi
|
||||
|
||||
if command -v mise >/dev/null 2>&1; then
|
||||
mise trust --yes "$PWD" >/dev/null 2>&1 || true
|
||||
fi
|
||||
printf 'developer setup complete; Archipelago skills preloaded for Codex; authenticate interactively with: codex login\n'
|
||||
Executable
+118
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env bash
|
||||
# Persistent Archipelago developer sessions. Terminal attachments are disposable;
|
||||
# the tmux session owns the process and durable JSON owns its discoverable state.
|
||||
set -euo pipefail
|
||||
|
||||
STATE_DIR="${ARCHY_SESSION_STATE_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/archipelago/sessions}"
|
||||
mkdir -p "$STATE_DIR"
|
||||
die() { echo "archy-session: $*" >&2; exit 2; }
|
||||
need_tmux() { command -v tmux >/dev/null 2>&1 || die "tmux is required"; }
|
||||
need_python() { command -v python3 >/dev/null 2>&1 || die "python3 is required"; }
|
||||
valid_id() { [[ "$1" =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$ ]]; }
|
||||
session_file() { printf '%s/%s.json' "$STATE_DIR" "$1"; }
|
||||
|
||||
write_record() {
|
||||
local id=$1 name=$2 workspace=$3 state=$4 tmux_name=$5
|
||||
need_python
|
||||
python3 - "$STATE_DIR" "$id" "$name" "$workspace" "$state" "$tmux_name" <<'PY'
|
||||
import json, os, sys, tempfile, time
|
||||
state_dir, ident, name, workspace, state, tmux_name = sys.argv[1:]
|
||||
record = {"schema": 1, "id": ident, "name": name, "workspace": workspace,
|
||||
"state": state, "tmux": tmux_name, "updated_at": int(time.time())}
|
||||
fd, tmp = tempfile.mkstemp(prefix=f".{ident}.", suffix=".tmp", dir=state_dir)
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
||||
json.dump(record, handle, sort_keys=True); handle.write("\n"); handle.flush(); os.fsync(handle.fileno())
|
||||
os.replace(tmp, os.path.join(state_dir, ident + ".json"))
|
||||
finally:
|
||||
try: os.unlink(tmp)
|
||||
except FileNotFoundError: pass
|
||||
PY
|
||||
}
|
||||
|
||||
new_id() { need_python; python3 - <<'PY'
|
||||
import secrets
|
||||
print("s-" + secrets.token_hex(8))
|
||||
PY
|
||||
}
|
||||
|
||||
read_json() {
|
||||
python3 - "$1" "$2" <<'PY'
|
||||
import json, sys
|
||||
print(json.load(open(sys.argv[1], encoding="utf-8"))[sys.argv[2]])
|
||||
PY
|
||||
}
|
||||
|
||||
cmd_list() {
|
||||
need_tmux; need_python
|
||||
python3 - "$STATE_DIR" <<'PY'
|
||||
import json, os, subprocess, sys
|
||||
state_dir = sys.argv[1]
|
||||
for filename in sorted(os.listdir(state_dir)):
|
||||
if not filename.endswith(".json"): continue
|
||||
try: record = json.load(open(os.path.join(state_dir, filename), encoding="utf-8"))
|
||||
except (OSError, ValueError, KeyError): continue
|
||||
live = subprocess.run(["tmux", "has-session", "-t", record["tmux"]], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0
|
||||
effective = "detached" if live else ("interrupted" if record.get("state") == "running" else record.get("state", "ended"))
|
||||
print(f"{record['id']}\t{record['name']}\t{effective}\t{record['workspace']}")
|
||||
PY
|
||||
}
|
||||
|
||||
cmd_start() {
|
||||
need_tmux
|
||||
local name=${1:-Work} workspace=${2:-$HOME/Work}
|
||||
[[ -d "$workspace" ]] || die "workspace does not exist: $workspace"
|
||||
local id tmux_name; id=$(new_id); tmux_name="archy-$id"
|
||||
tmux new-session -d -s "$tmux_name" -c "$workspace"
|
||||
write_record "$id" "$name" "$workspace" running "$tmux_name"
|
||||
printf '%s\n' "$id"
|
||||
}
|
||||
|
||||
record_for() {
|
||||
local id=$1 file; valid_id "$id" || die "invalid session id"; file=$(session_file "$id")
|
||||
[[ -f "$file" ]] || die "session not found: $id"; printf '%s\n' "$file"
|
||||
}
|
||||
|
||||
cmd_attach() {
|
||||
need_tmux; local file=$1 tmux_name; tmux_name=$(read_json "$file" tmux)
|
||||
tmux has-session -t "$tmux_name" 2>/dev/null || die "session is not running; inspect with list"
|
||||
exec tmux attach-session -t "$tmux_name"
|
||||
}
|
||||
|
||||
cmd_rename() {
|
||||
local id=$1 name=$2 file=$3; local workspace tmux_name state=ended
|
||||
workspace=$(read_json "$file" workspace); tmux_name=$(read_json "$file" tmux); need_tmux
|
||||
tmux has-session -t "$tmux_name" 2>/dev/null && state=detached
|
||||
write_record "$id" "$name" "$workspace" "$state" "$tmux_name"
|
||||
}
|
||||
|
||||
cmd_end() {
|
||||
local id=$1 file=$2; local tmux_name name workspace
|
||||
tmux_name=$(read_json "$file" tmux); name=$(read_json "$file" name); workspace=$(read_json "$file" workspace); need_tmux
|
||||
tmux kill-session -t "$tmux_name" 2>/dev/null || true
|
||||
write_record "$id" "$name" "$workspace" ended "$tmux_name"
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat >&2 <<'EOF'
|
||||
Usage:
|
||||
archy-session list
|
||||
archy-session start [name] [workspace]
|
||||
archy-session attach <id>
|
||||
archy-session rename <id> <name>
|
||||
archy-session end <id>
|
||||
|
||||
Closing an attached terminal detaches it. end is the explicit destructive
|
||||
operation and never removes the workspace or its files.
|
||||
EOF
|
||||
}
|
||||
|
||||
command=${1:-}
|
||||
case "$command" in
|
||||
list) cmd_list ;;
|
||||
start) shift; cmd_start "$@" ;;
|
||||
attach) shift; [[ $# == 1 ]] || die "attach requires an id"; file=$(record_for "$1"); cmd_attach "$file" ;;
|
||||
rename) shift; [[ $# == 2 ]] || die "rename requires an id and name"; file=$(record_for "$1"); cmd_rename "$1" "$2" "$file" ;;
|
||||
end) shift; [[ $# == 1 ]] || die "end requires an id"; file=$(record_for "$1"); cmd_end "$1" "$file" ;;
|
||||
*) usage; exit 2 ;;
|
||||
esac
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
|
||||
output=$(PATH=/usr/bin:/bin "$ROOT/scripts/archy-developer-setup" --check 2>&1 || true)
|
||||
grep -qE '^(ok|missing)[[:space:]]' <<<"$output"
|
||||
! grep -q 'sudo -n apt-get' <<<"$output"
|
||||
printf 'archy-developer-setup check is non-mutating\n'
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
|
||||
export ARCHY_SESSION_STATE_DIR="$TMP/state"; mkdir -p "$TMP/work"
|
||||
if ! command -v tmux >/dev/null 2>&1; then echo "SKIP: tmux is unavailable"; exit 0; fi
|
||||
id=$("$ROOT/scripts/archy-session" start demo "$TMP/work")
|
||||
test -f "$TMP/state/$id.json"
|
||||
grep -q "$id" <("$ROOT/scripts/archy-session" list)
|
||||
"$ROOT/scripts/archy-session" rename "$id" renamed
|
||||
grep -q $'renamed\tdetached' <("$ROOT/scripts/archy-session" list)
|
||||
"$ROOT/scripts/archy-session" end "$id"
|
||||
grep -q $'renamed\tended' <("$ROOT/scripts/archy-session" list)
|
||||
test -d "$TMP/work"; echo "archy-session lifecycle passed"
|
||||
Reference in New Issue
Block a user