Compare commits
10 Commits
v0.1.62-ma
...
v0.1.72-ma
| Author | SHA1 | Date | |
|---|---|---|---|
| 3228e6cdd7 | |||
| 3344ce1cbf | |||
| d642cc64de | |||
| e3502876eb | |||
| 4f41f0d98b | |||
| c9dc232fc4 | |||
| 2d4fce935f | |||
| e739f6aaff | |||
| 550159fc63 | |||
| e3c874bc75 |
84
.gitea/workflows/backfill-releases.yml
Normal file
84
.gitea/workflows/backfill-releases.yml
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
name: Backfill Releases to GitHub
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
backfill:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Backfill all Gitea releases to GitHub
|
||||||
|
env:
|
||||||
|
GH_PAT: ${{ secrets.GH_PAT }}
|
||||||
|
GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
GITEA_API: https://repo.anhonesthost.net/api/v1
|
||||||
|
GITEA_REPO: cybercovellc/triple-c
|
||||||
|
GITHUB_REPO: shadowdao/triple-c
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "==> Fetching releases from Gitea..."
|
||||||
|
RELEASES=$(curl -sf \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
"$GITEA_API/repos/$GITEA_REPO/releases?limit=50")
|
||||||
|
|
||||||
|
echo "$RELEASES" | jq -c '.[]' | while read release; do
|
||||||
|
TAG=$(echo "$release" | jq -r '.tag_name')
|
||||||
|
NAME=$(echo "$release" | jq -r '.name')
|
||||||
|
BODY=$(echo "$release" | jq -r '.body')
|
||||||
|
IS_PRERELEASE=$(echo "$release" | jq -r '.prerelease')
|
||||||
|
IS_DRAFT=$(echo "$release" | jq -r '.draft')
|
||||||
|
|
||||||
|
EXISTS=$(curl -sf \
|
||||||
|
-H "Authorization: Bearer $GH_PAT" \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
|
"https://api.github.com/repos/$GITHUB_REPO/releases/tags/$TAG" \
|
||||||
|
-o /dev/null -w "%{http_code}" || true)
|
||||||
|
|
||||||
|
if [ "$EXISTS" = "200" ]; then
|
||||||
|
echo "==> Skipping $TAG (already exists on GitHub)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Creating release $TAG..."
|
||||||
|
RESPONSE=$(curl -sf -X POST \
|
||||||
|
-H "Authorization: Bearer $GH_PAT" \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
https://api.github.com/repos/$GITHUB_REPO/releases \
|
||||||
|
-d "{
|
||||||
|
\"tag_name\": \"$TAG\",
|
||||||
|
\"name\": \"$NAME\",
|
||||||
|
\"body\": $(echo "$BODY" | jq -Rs .),
|
||||||
|
\"draft\": $IS_DRAFT,
|
||||||
|
\"prerelease\": $IS_PRERELEASE
|
||||||
|
}")
|
||||||
|
|
||||||
|
UPLOAD_URL=$(echo "$RESPONSE" | jq -r '.upload_url' | sed 's/{?name,label}//')
|
||||||
|
|
||||||
|
echo "$release" | jq -c '.assets[]?' | while read asset; do
|
||||||
|
ASSET_NAME=$(echo "$asset" | jq -r '.name')
|
||||||
|
ASSET_ID=$(echo "$asset" | jq -r '.id')
|
||||||
|
|
||||||
|
echo " ==> Downloading $ASSET_NAME..."
|
||||||
|
DOWNLOAD_URL=$(echo "$asset" | jq -r '.browser_download_url')
|
||||||
|
curl -sfL -o "/tmp/$ASSET_NAME" \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
"$DOWNLOAD_URL"
|
||||||
|
|
||||||
|
echo " ==> Uploading $ASSET_NAME to GitHub..."
|
||||||
|
ENCODED_NAME=$(python3 -c "import urllib.parse, sys; print(urllib.parse.quote(sys.argv[1]))" "$ASSET_NAME")
|
||||||
|
curl -sf -X POST \
|
||||||
|
-H "Authorization: Bearer $GH_PAT" \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary "@/tmp/$ASSET_NAME" \
|
||||||
|
"$UPLOAD_URL?name=$ENCODED_NAME"
|
||||||
|
|
||||||
|
echo " Uploaded: $ASSET_NAME"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "==> Done: $TAG"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "==> Backfill complete."
|
||||||
@@ -20,15 +20,29 @@ jobs:
|
|||||||
build-linux:
|
build-linux:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Install Node.js
|
- name: Install Node.js 22
|
||||||
run: |
|
run: |
|
||||||
|
NEED_INSTALL=false
|
||||||
if command -v node >/dev/null 2>&1; then
|
if command -v node >/dev/null 2>&1; then
|
||||||
echo "Node.js already installed: $(node --version)"
|
NODE_MAJOR=$(node --version | sed 's/v\([0-9]*\).*/\1/')
|
||||||
|
OLD_NODE_DIR=$(dirname "$(which node)")
|
||||||
|
echo "Found Node.js $(node --version) at $(which node) (major: ${NODE_MAJOR})"
|
||||||
|
if [ "$NODE_MAJOR" -lt 22 ]; then
|
||||||
|
echo "Node.js ${NODE_MAJOR} is too old, removing before installing 22..."
|
||||||
|
sudo rm -f "${OLD_NODE_DIR}/node" "${OLD_NODE_DIR}/npm" "${OLD_NODE_DIR}/npx" "${OLD_NODE_DIR}/corepack"
|
||||||
|
hash -r
|
||||||
|
NEED_INSTALL=true
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo "Installing Node.js 22..."
|
echo "Node.js not found, installing 22..."
|
||||||
|
NEED_INSTALL=true
|
||||||
|
fi
|
||||||
|
if [ "$NEED_INSTALL" = true ]; then
|
||||||
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
|
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
|
||||||
sudo apt-get install -y nodejs
|
sudo apt-get install -y nodejs
|
||||||
|
hash -r
|
||||||
fi
|
fi
|
||||||
|
echo "Node.js at: $(which node)"
|
||||||
node --version
|
node --version
|
||||||
npm --version
|
npm --version
|
||||||
|
|
||||||
@@ -81,22 +95,27 @@ jobs:
|
|||||||
else
|
else
|
||||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
|
||||||
fi
|
fi
|
||||||
. "$HOME/.cargo/env"
|
export PATH="$HOME/.cargo/bin:$PATH"
|
||||||
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
|
||||||
rustc --version
|
rustc --version
|
||||||
cargo --version
|
cargo --version
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
working-directory: ./app
|
working-directory: ./app
|
||||||
run: npm ci
|
run: |
|
||||||
|
rm -rf node_modules package-lock.json
|
||||||
|
npm install
|
||||||
|
|
||||||
- name: Install Tauri CLI
|
- name: Install Tauri CLI
|
||||||
working-directory: ./app
|
working-directory: ./app
|
||||||
run: npx tauri --version || npm install @tauri-apps/cli
|
run: |
|
||||||
|
export PATH="$HOME/.cargo/bin:$PATH"
|
||||||
|
npx tauri --version || npm install @tauri-apps/cli
|
||||||
|
|
||||||
- name: Build Tauri app
|
- name: Build Tauri app
|
||||||
working-directory: ./app
|
working-directory: ./app
|
||||||
run: npx tauri build
|
run: |
|
||||||
|
export PATH="$HOME/.cargo/bin:$PATH"
|
||||||
|
npx tauri build
|
||||||
|
|
||||||
- name: Collect artifacts
|
- name: Collect artifacts
|
||||||
run: |
|
run: |
|
||||||
@@ -135,12 +154,21 @@ jobs:
|
|||||||
build-macos:
|
build-macos:
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Install Node.js
|
- name: Install Node.js 22
|
||||||
run: |
|
run: |
|
||||||
if command -v node &>/dev/null; then
|
NEED_INSTALL=false
|
||||||
echo "Node.js already installed: $(node --version)"
|
if command -v node >/dev/null 2>&1; then
|
||||||
|
NODE_MAJOR=$(node --version | sed 's/v\([0-9]*\).*/\1/')
|
||||||
|
echo "Found Node.js $(node --version) (major: ${NODE_MAJOR})"
|
||||||
|
if [ "$NODE_MAJOR" -lt 22 ]; then
|
||||||
|
echo "Node.js ${NODE_MAJOR} is too old, upgrading to 22..."
|
||||||
|
NEED_INSTALL=true
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo "Installing Node.js 22 via Homebrew..."
|
echo "Node.js not found, installing 22..."
|
||||||
|
NEED_INSTALL=true
|
||||||
|
fi
|
||||||
|
if [ "$NEED_INSTALL" = true ]; then
|
||||||
brew install node@22
|
brew install node@22
|
||||||
brew link --overwrite node@22
|
brew link --overwrite node@22
|
||||||
fi
|
fi
|
||||||
@@ -177,23 +205,28 @@ jobs:
|
|||||||
else
|
else
|
||||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
|
||||||
fi
|
fi
|
||||||
. "$HOME/.cargo/env"
|
export PATH="$HOME/.cargo/bin:$PATH"
|
||||||
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
|
||||||
rustup target add aarch64-apple-darwin x86_64-apple-darwin
|
rustup target add aarch64-apple-darwin x86_64-apple-darwin
|
||||||
rustc --version
|
rustc --version
|
||||||
cargo --version
|
cargo --version
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
working-directory: ./app
|
working-directory: ./app
|
||||||
run: npm ci
|
run: |
|
||||||
|
rm -rf node_modules
|
||||||
|
npm install
|
||||||
|
|
||||||
- name: Install Tauri CLI
|
- name: Install Tauri CLI
|
||||||
working-directory: ./app
|
working-directory: ./app
|
||||||
run: npx tauri --version || npm install @tauri-apps/cli
|
run: |
|
||||||
|
export PATH="$HOME/.cargo/bin:$PATH"
|
||||||
|
npx tauri --version || npm install @tauri-apps/cli
|
||||||
|
|
||||||
- name: Build Tauri app (universal)
|
- name: Build Tauri app (universal)
|
||||||
working-directory: ./app
|
working-directory: ./app
|
||||||
run: npx tauri build --target universal-apple-darwin
|
run: |
|
||||||
|
export PATH="$HOME/.cargo/bin:$PATH"
|
||||||
|
npx tauri build --target universal-apple-darwin
|
||||||
|
|
||||||
- name: Collect artifacts
|
- name: Collect artifacts
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
60
.gitea/workflows/sync-release.yml
Normal file
60
.gitea/workflows/sync-release.yml
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
name: Sync Release to GitHub
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Mirror release to GitHub
|
||||||
|
env:
|
||||||
|
GH_PAT: ${{ secrets.GH_PAT }}
|
||||||
|
GITHUB_REPO: shadowdao/triple-c
|
||||||
|
RELEASE_TAG: ${{ gitea.event.release.tag_name }}
|
||||||
|
RELEASE_NAME: ${{ gitea.event.release.name }}
|
||||||
|
RELEASE_BODY: ${{ gitea.event.release.body }}
|
||||||
|
IS_PRERELEASE: ${{ gitea.event.release.prerelease }}
|
||||||
|
IS_DRAFT: ${{ gitea.event.release.draft }}
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "==> Creating release $RELEASE_TAG on GitHub..."
|
||||||
|
|
||||||
|
RESPONSE=$(curl -sf -X POST \
|
||||||
|
-H "Authorization: Bearer $GH_PAT" \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
https://api.github.com/repos/$GITHUB_REPO/releases \
|
||||||
|
-d "{
|
||||||
|
\"tag_name\": \"$RELEASE_TAG\",
|
||||||
|
\"name\": \"$RELEASE_NAME\",
|
||||||
|
\"body\": $(echo "$RELEASE_BODY" | jq -Rs .),
|
||||||
|
\"draft\": $IS_DRAFT,
|
||||||
|
\"prerelease\": $IS_PRERELEASE
|
||||||
|
}")
|
||||||
|
|
||||||
|
UPLOAD_URL=$(echo "$RESPONSE" | jq -r '.upload_url' | sed 's/{?name,label}//')
|
||||||
|
echo "Release created. Upload URL: $UPLOAD_URL"
|
||||||
|
|
||||||
|
echo '${{ toJSON(gitea.event.release.assets) }}' | jq -c '.[]' | while read asset; do
|
||||||
|
ASSET_NAME=$(echo "$asset" | jq -r '.name')
|
||||||
|
ASSET_URL=$(echo "$asset" | jq -r '.browser_download_url')
|
||||||
|
|
||||||
|
echo "==> Downloading asset: $ASSET_NAME"
|
||||||
|
curl -sfL -o "/tmp/$ASSET_NAME" "$ASSET_URL"
|
||||||
|
|
||||||
|
echo "==> Uploading $ASSET_NAME to GitHub..."
|
||||||
|
ENCODED_NAME=$(python3 -c "import urllib.parse, sys; print(urllib.parse.quote(sys.argv[1]))" "$ASSET_NAME")
|
||||||
|
curl -sf -X POST \
|
||||||
|
-H "Authorization: Bearer $GH_PAT" \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary "@/tmp/$ASSET_NAME" \
|
||||||
|
"$UPLOAD_URL?name=$ENCODED_NAME"
|
||||||
|
|
||||||
|
echo " Uploaded: $ASSET_NAME"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "==> Release sync complete."
|
||||||
60
TODO.md
Normal file
60
TODO.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# TODO / Future Improvements
|
||||||
|
|
||||||
|
## In-App Auto-Update via `tauri-plugin-updater`
|
||||||
|
|
||||||
|
**Priority:** High
|
||||||
|
**Status:** Planned
|
||||||
|
|
||||||
|
Currently the app detects available updates via the Gitea API (`check_for_updates` command) but cannot apply them. Users must manually download and install the new version. On macOS and Linux this is a poor experience compared to Windows (where NSIS handles upgrades cleanly).
|
||||||
|
|
||||||
|
### Recommended approach: `tauri-plugin-updater`
|
||||||
|
|
||||||
|
Full in-app auto-update: detects, downloads, verifies, and applies updates seamlessly on all platforms. The user clicks "Update" and the app restarts with the new version.
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
1. **Generate a Tauri update signing key pair** (this is Tauri's own Ed25519 key, not OS code signing):
|
||||||
|
```bash
|
||||||
|
npx @tauri-apps/cli signer generate -w ~/.tauri/triple-c.key
|
||||||
|
```
|
||||||
|
Set `TAURI_SIGNING_PRIVATE_KEY` and `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` in CI.
|
||||||
|
|
||||||
|
2. **Add `tauri-plugin-updater`** to Rust and JS dependencies.
|
||||||
|
|
||||||
|
3. **Create an update endpoint** that returns Tauri's expected JSON format:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "v0.1.100",
|
||||||
|
"notes": "Changelog here",
|
||||||
|
"pub_date": "2026-03-01T00:00:00Z",
|
||||||
|
"platforms": {
|
||||||
|
"darwin-x86_64": { "signature": "...", "url": "https://..." },
|
||||||
|
"darwin-aarch64": { "signature": "...", "url": "https://..." },
|
||||||
|
"linux-x86_64": { "signature": "...", "url": "https://..." },
|
||||||
|
"windows-x86_64": { "signature": "...", "url": "https://..." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
This could be a static JSON file uploaded alongside release assets, or a small API that reads from Gitea releases and reformats.
|
||||||
|
|
||||||
|
4. **Configure the updater** in `tauri.conf.json`:
|
||||||
|
```json
|
||||||
|
"plugins": {
|
||||||
|
"updater": {
|
||||||
|
"endpoints": ["https://repo.anhonesthost.net/...update-endpoint..."],
|
||||||
|
"pubkey": "<public key from step 1>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Add frontend UI** for the update prompt (replace or enhance the existing update check flow).
|
||||||
|
|
||||||
|
6. **Update CI pipeline** to:
|
||||||
|
- Sign bundles with the Tauri key during build
|
||||||
|
- Upload `.sig` files alongside installers
|
||||||
|
- Generate/upload the update endpoint JSON
|
||||||
|
|
||||||
|
### References
|
||||||
|
- https://v2.tauri.app/plugin/updater/
|
||||||
|
- Existing update check code: `app/src-tauri/src/commands/update_commands.rs`
|
||||||
|
- Existing models: `app/src-tauri/src/models/update_info.rs`
|
||||||
@@ -1,23 +1,28 @@
|
|||||||
use bollard::Docker;
|
use bollard::Docker;
|
||||||
use std::sync::OnceLock;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
static DOCKER: OnceLock<Result<Docker, String>> = OnceLock::new();
|
static DOCKER: Mutex<Option<Docker>> = Mutex::new(None);
|
||||||
|
|
||||||
pub fn get_docker() -> Result<&'static Docker, String> {
|
pub fn get_docker() -> Result<Docker, String> {
|
||||||
let result = DOCKER.get_or_init(|| {
|
let mut guard = DOCKER.lock().map_err(|e| format!("Lock poisoned: {}", e))?;
|
||||||
Docker::connect_with_local_defaults()
|
if let Some(docker) = guard.as_ref() {
|
||||||
.map_err(|e| format!("Failed to connect to Docker daemon: {}", e))
|
return Ok(docker.clone());
|
||||||
});
|
|
||||||
match result {
|
|
||||||
Ok(docker) => Ok(docker),
|
|
||||||
Err(e) => Err(e.clone()),
|
|
||||||
}
|
}
|
||||||
|
let docker = Docker::connect_with_local_defaults()
|
||||||
|
.map_err(|e| format!("Failed to connect to Docker daemon: {}", e))?;
|
||||||
|
guard.replace(docker.clone());
|
||||||
|
Ok(docker)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn check_docker_available() -> Result<bool, String> {
|
pub async fn check_docker_available() -> Result<bool, String> {
|
||||||
let docker = get_docker()?;
|
let docker = get_docker()?;
|
||||||
match docker.ping().await {
|
match docker.ping().await {
|
||||||
Ok(_) => Ok(true),
|
Ok(_) => Ok(true),
|
||||||
Err(e) => Err(format!("Docker daemon not responding: {}", e)),
|
Err(_) => {
|
||||||
|
// Connection object exists but daemon not responding — clear cache
|
||||||
|
let mut guard = DOCKER.lock().map_err(|e| format!("Lock poisoned: {}", e))?;
|
||||||
|
*guard = None;
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,42 @@ After tasks run, check notifications with `triple-c-scheduler notifications` and
|
|||||||
### Timezone
|
### Timezone
|
||||||
Scheduled times use the container's configured timezone (check with `date`). If no timezone is configured, UTC is used."#;
|
Scheduled times use the container's configured timezone (check with `date`). If no timezone is configured, UTC is used."#;
|
||||||
|
|
||||||
|
/// Build the full CLAUDE_INSTRUCTIONS value by merging global + project
|
||||||
|
/// instructions, appending port mapping docs, and appending scheduler docs.
|
||||||
|
/// Used by both create_container() and container_needs_recreation() to ensure
|
||||||
|
/// the same value is produced in both paths.
|
||||||
|
fn build_claude_instructions(
|
||||||
|
global_instructions: Option<&str>,
|
||||||
|
project_instructions: Option<&str>,
|
||||||
|
port_mappings: &[PortMapping],
|
||||||
|
) -> Option<String> {
|
||||||
|
let mut combined = merge_claude_instructions(global_instructions, project_instructions);
|
||||||
|
|
||||||
|
if !port_mappings.is_empty() {
|
||||||
|
let mut port_lines: Vec<String> = Vec::new();
|
||||||
|
port_lines.push("## Available Port Mappings".to_string());
|
||||||
|
port_lines.push("The following ports are mapped from the host to this container. Use these container ports when starting services that need to be accessible from the host:".to_string());
|
||||||
|
for pm in port_mappings {
|
||||||
|
port_lines.push(format!(
|
||||||
|
"- Host port {} -> Container port {} ({})",
|
||||||
|
pm.host_port, pm.container_port, pm.protocol
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let port_info = port_lines.join("\n");
|
||||||
|
combined = Some(match combined {
|
||||||
|
Some(existing) => format!("{}\n\n{}", existing, port_info),
|
||||||
|
None => port_info,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
combined = Some(match combined {
|
||||||
|
Some(existing) => format!("{}\n\n{}", existing, SCHEDULER_INSTRUCTIONS),
|
||||||
|
None => SCHEDULER_INSTRUCTIONS.to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
combined
|
||||||
|
}
|
||||||
|
|
||||||
/// Compute a fingerprint string for the custom environment variables.
|
/// Compute a fingerprint string for the custom environment variables.
|
||||||
/// Sorted alphabetically so order changes do not cause spurious recreation.
|
/// Sorted alphabetically so order changes do not cause spurious recreation.
|
||||||
fn compute_env_fingerprint(custom_env_vars: &[EnvVar]) -> String {
|
fn compute_env_fingerprint(custom_env_vars: &[EnvVar]) -> String {
|
||||||
@@ -307,33 +343,12 @@ pub async fn create_container(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Claude instructions (global + per-project, plus port mapping info)
|
// Claude instructions (global + per-project, plus port mapping info + scheduler docs)
|
||||||
let mut combined_instructions = merge_claude_instructions(
|
let combined_instructions = build_claude_instructions(
|
||||||
global_claude_instructions,
|
global_claude_instructions,
|
||||||
project.claude_instructions.as_deref(),
|
project.claude_instructions.as_deref(),
|
||||||
|
&project.port_mappings,
|
||||||
);
|
);
|
||||||
if !project.port_mappings.is_empty() {
|
|
||||||
let mut port_lines: Vec<String> = Vec::new();
|
|
||||||
port_lines.push("## Available Port Mappings".to_string());
|
|
||||||
port_lines.push("The following ports are mapped from the host to this container. Use these container ports when starting services that need to be accessible from the host:".to_string());
|
|
||||||
for pm in &project.port_mappings {
|
|
||||||
port_lines.push(format!(
|
|
||||||
"- Host port {} -> Container port {} ({})",
|
|
||||||
pm.host_port, pm.container_port, pm.protocol
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let port_info = port_lines.join("\n");
|
|
||||||
combined_instructions = Some(match combined_instructions {
|
|
||||||
Some(existing) => format!("{}\n\n{}", existing, port_info),
|
|
||||||
None => port_info,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Scheduler instructions (always appended so all containers get scheduling docs)
|
|
||||||
let scheduler_docs = SCHEDULER_INSTRUCTIONS;
|
|
||||||
combined_instructions = Some(match combined_instructions {
|
|
||||||
Some(existing) => format!("{}\n\n{}", existing, scheduler_docs),
|
|
||||||
None => scheduler_docs.to_string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(ref instructions) = combined_instructions {
|
if let Some(ref instructions) = combined_instructions {
|
||||||
env_vars.push(format!("CLAUDE_INSTRUCTIONS={}", instructions));
|
env_vars.push(format!("CLAUDE_INSTRUCTIONS={}", instructions));
|
||||||
@@ -685,9 +700,10 @@ pub async fn container_needs_recreation(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Claude instructions ───────────────────────────────────────────────
|
// ── Claude instructions ───────────────────────────────────────────────
|
||||||
let expected_instructions = merge_claude_instructions(
|
let expected_instructions = build_claude_instructions(
|
||||||
global_claude_instructions,
|
global_claude_instructions,
|
||||||
project.claude_instructions.as_deref(),
|
project.claude_instructions.as_deref(),
|
||||||
|
&project.port_mappings,
|
||||||
);
|
);
|
||||||
let container_instructions = get_env("CLAUDE_INSTRUCTIONS");
|
let container_instructions = get_env("CLAUDE_INSTRUCTIONS");
|
||||||
if container_instructions.as_deref() != expected_instructions.as_deref() {
|
if container_instructions.as_deref() != expected_instructions.as_deref() {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { useUpdates } from "./hooks/useUpdates";
|
|||||||
import { useAppState } from "./store/appState";
|
import { useAppState } from "./store/appState";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const { checkDocker, checkImage } = useDocker();
|
const { checkDocker, checkImage, startDockerPolling } = useDocker();
|
||||||
const { loadSettings } = useSettings();
|
const { loadSettings } = useSettings();
|
||||||
const { refresh } = useProjects();
|
const { refresh } = useProjects();
|
||||||
const { loadVersion, checkForUpdates, startPeriodicCheck } = useUpdates();
|
const { loadVersion, checkForUpdates, startPeriodicCheck } = useUpdates();
|
||||||
@@ -22,8 +22,13 @@ export default function App() {
|
|||||||
// Initialize on mount
|
// Initialize on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadSettings();
|
loadSettings();
|
||||||
|
let stopPolling: (() => void) | undefined;
|
||||||
checkDocker().then((available) => {
|
checkDocker().then((available) => {
|
||||||
if (available) checkImage();
|
if (available) {
|
||||||
|
checkImage();
|
||||||
|
} else {
|
||||||
|
stopPolling = startDockerPolling();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
refresh();
|
refresh();
|
||||||
|
|
||||||
@@ -34,6 +39,7 @@ export default function App() {
|
|||||||
return () => {
|
return () => {
|
||||||
clearTimeout(updateTimer);
|
clearTimeout(updateTimer);
|
||||||
cleanup?.();
|
cleanup?.();
|
||||||
|
stopPolling?.();
|
||||||
};
|
};
|
||||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback, useRef } from "react";
|
||||||
import { useShallow } from "zustand/react/shallow";
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { useAppState } from "../store/appState";
|
import { useAppState } from "../store/appState";
|
||||||
@@ -59,6 +59,39 @@ export function useDocker() {
|
|||||||
[setImageExists],
|
[setImageExists],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
|
|
||||||
|
const startDockerPolling = useCallback(() => {
|
||||||
|
// Don't start if already polling
|
||||||
|
if (pollingRef.current) return () => {};
|
||||||
|
|
||||||
|
const interval = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const available = await commands.checkDocker();
|
||||||
|
if (available) {
|
||||||
|
clearInterval(interval);
|
||||||
|
pollingRef.current = null;
|
||||||
|
setDockerAvailable(true);
|
||||||
|
// Also check image once Docker is available
|
||||||
|
try {
|
||||||
|
const exists = await commands.checkImageExists();
|
||||||
|
setImageExists(exists);
|
||||||
|
} catch {
|
||||||
|
setImageExists(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Still not available, keep polling
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
pollingRef.current = interval;
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
pollingRef.current = null;
|
||||||
|
};
|
||||||
|
}, [setDockerAvailable, setImageExists]);
|
||||||
|
|
||||||
const pullImage = useCallback(
|
const pullImage = useCallback(
|
||||||
async (imageName: string, onProgress?: (msg: string) => void) => {
|
async (imageName: string, onProgress?: (msg: string) => void) => {
|
||||||
const unlisten = onProgress
|
const unlisten = onProgress
|
||||||
@@ -84,5 +117,6 @@ export function useDocker() {
|
|||||||
checkImage,
|
checkImage,
|
||||||
buildImage,
|
buildImage,
|
||||||
pullImage,
|
pullImage,
|
||||||
|
startDockerPolling,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user