Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "luxctl"
version = "0.6.3"
version = "0.7.0"
edition = "2021"
description = "Learn by building - CLI for projectlighthouse.io"
license = "AGPL-3.0-only"
Expand Down
10 changes: 5 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
.PHONY: build run test fmt lint clean dev check all local\:me local\:get release\:build

# ==============================================================================
# Local API Testing
# ==============================================================================
.PHONY: build run test e2e fmt lint clean dev check all local\:me local\:get release\:build

LOCAL_API_URL := http://0.0.0.0:8000/api/v1
DEV_TOKEN_FILE := dev_token
Expand Down Expand Up @@ -43,6 +39,10 @@ run:
test:
cargo test

# Run E2E tests (requires local API running)
e2e:
cargo test --test e2e -- --ignored --nocapture

# Format code
fmt:
cargo fmt
Expand Down
12 changes: 10 additions & 2 deletions src/api/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ use crate::{config::Config, VERSION};

use super::types::{
ApiError, ApiUser, HealthCheckResponse, HintsResponse, Lab, PaginatedResponse,
SubmitAnswerRequest, SubmitAnswerResponse, SubmitAttemptRequest, SubmitAttemptResponse,
UnlockHintResponse,
RestartLabResponse, SubmitAnswerRequest, SubmitAnswerResponse, SubmitAttemptRequest,
SubmitAttemptResponse, UnlockHintResponse,
};

pub struct LighthouseAPIClient {
Expand Down Expand Up @@ -212,6 +212,14 @@ impl LighthouseAPIClient {
self.post::<SubmitAnswerResponse, _>(&endpoint, request, Some(headers))
.await
}

/// restart a lab from scratch (creates new attempt group)
pub async fn restart_lab(&self, slug: &str) -> Result<RestartLabResponse> {
let headers = self.auth_headers()?;
let endpoint = format!("labs/{}/restart", slug);
self.post::<RestartLabResponse, _>(&endpoint, &serde_json::json!({}), Some(headers))
.await
}
}

#[derive(Clone, Copy)]
Expand Down
13 changes: 13 additions & 0 deletions src/api/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,19 @@ pub struct SubmitAnswerResponse {
pub already_completed: Option<bool>,
}

/// response from restarting a lab
#[derive(Debug, Deserialize)]
pub struct RestartLabResponse {
pub message: String,
pub data: RestartLabData,
}

#[derive(Debug, Deserialize)]
pub struct RestartLabData {
pub attempt_group_id: i32,
pub created_at: String,
}

impl ApiUser {
pub fn id(&self) -> i32 {
self.id
Expand Down
57 changes: 57 additions & 0 deletions src/commands/lab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,60 @@ pub fn set_workspace(workspace: &str) -> Result<()> {

Ok(())
}

/// handle `luxctl lab restart`
pub async fn restart() -> Result<()> {
let config = Config::load()?;
if !config.has_auth_token() {
UI::error(
"not authenticated",
Some("run `luxctl auth --token $token`"),
);
return Ok(());
}

let mut state = LabState::load(config.expose_token())?;

let lab = match state.get_active() {
Some(l) => l.clone(),
None => {
UI::error("no active lab", None);
UI::note("run `luxctl lab start --slug <SLUG>` first");
return Ok(());
}
};

let client = LighthouseAPIClient::from_config(&config);

match client.restart_lab(&lab.slug).await {
Ok(response) => {
// refresh tasks from server

Copilot AI Jan 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected capitalization for 'refresh tasks from server' to 'Refresh tasks from server'.

Suggested change
// refresh tasks from server
// Refresh tasks from server

Copilot uses AI. Check for mistakes.
match client.lab_by_slug(&lab.slug).await {
Ok(refreshed_lab) => {
let tasks = refreshed_lab.tasks.as_deref().unwrap_or(&[]);
state.set_active(
&lab.slug,
&lab.name,
tasks,
&lab.workspace,
lab.runtime.as_deref(),
);
state.save(config.expose_token())?;
}
Err(_) => {
// if refresh fails, just clear local progress

Copilot AI Jan 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected capitalization for 'if refresh fails, just clear local progress' to 'If refresh fails, just clear local progress'.

Suggested change
// if refresh fails, just clear local progress
// If refresh fails, just clear local progress

Copilot uses AI. Check for mistakes.
state.clear_progress();
state.save(config.expose_token())?;
}
}

UI::success(&format!("restarted lab: {}", lab.name));
UI::info(&response.message);
}
Err(err) => {
UI::error("failed to restart lab", Some(&format!("{}", err)));
}
}

Ok(())
}
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ pub mod tasks;
pub mod ui;
pub mod validators;

pub const VERSION: &str = "0.6.3";
pub const VERSION: &str = "0.7.0";
5 changes: 5 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ enum LabAction {
#[arg(short = 'w', long)]
workspace: Option<String>,
},
/// Start fresh - reset all progress on the current lab
Restart,
}

#[derive(Subcommand)]
Expand Down Expand Up @@ -255,6 +257,9 @@ async fn main() -> Result<()> {
oops!("provide --runtime or --workspace to set");
}
}
LabAction::Restart => {
commands::lab::restart().await?;
}
},

Commands::Task { action } => match action {
Expand Down
10 changes: 10 additions & 0 deletions src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,16 @@ impl LabState {
});
}

/// reset all task progress to initial state (for lab restart)
pub fn clear_progress(&mut self) {
self.with_active_mut(|l| {
for task in &mut l.tasks {
task.status = TaskStatus::ChallengeAwaits;
task.points_earned = 0;
}
});
}

/// compute HMAC-SHA256 checksum of lab data
/// returns empty string if HMAC creation fails (should never happen for SHA256)
fn compute_checksum(lab: &Option<ActiveLab>, token: &str) -> String {
Expand Down
Loading