use wsb::prelude::*;
use clap::{CommandFactory, Parser, Subcommand};
use clap_complete::{generate, Shell};
use colored::Colorize;
use std::env;
use std::fs;
use std::io::Write;
use std::io::IsTerminal;
use std::process::{Command, exit};
use std::time::Instant;
use wsb::logging::{
log_operation_complete, log_operation_error, log_operation_start, log_version_info,
log_warning, setup_logging,
};
use wsb::app::AppContext;
use wsb::state::{
detect_project_files, update_version_file, StateConfig, TemplateManager, VersionInfo,
WstemplateEngine,
};
use wsb::workspace;
use wsb::workspace_state::{WorkspaceState, WstemplateEntry};
#[derive(Parser, Debug)]
#[command(name = "wsb")]
#[command(version = env!("CARGO_PKG_VERSION"))]
#[command(about = "Workspace - All-in-one development tool suite")]
#[command(
after_help = "When no subcommand is provided, `wsb` defaults to `serve`.\nShell completions are automatically set up on first run."
)]
struct Args {
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand, Debug)]
enum Commands {
Refactor {
#[command(flatten)]
args: wsb::refac::Args,
},
Status {
#[arg(short, long)]
verbose: bool,
},
Checkout {
branch: String,
#[arg(short, long)]
create: bool,
#[arg(short, long)]
verbose: bool,
#[arg(short = 'n', long)]
dry_run: bool,
},
Pull {
#[arg(short, long)]
rebase: bool,
#[arg(short, long)]
verbose: bool,
#[arg(short = 'n', long)]
dry_run: bool,
},
Push {
#[arg(short, long)]
force_with_lease: bool,
#[arg(short, long)]
verbose: bool,
#[arg(short = 'n', long)]
dry_run: bool,
},
Create {
branch: String,
#[arg(short, long)]
from: Option<String>,
#[arg(short, long)]
push: bool,
#[arg(short, long)]
verbose: bool,
#[arg(short = 'n', long)]
dry_run: bool,
},
Commit {
#[arg(short, long)]
message: String,
#[arg(short, long)]
push: bool,
#[arg(short = 'a', long)]
stage_all: bool,
#[arg(short = 'x', long = "exclude", value_name = "ALIAS")]
exclude: Vec<String>,
#[arg(short = 'i', long = "include", value_name = "ALIAS")]
include: Vec<String>,
#[arg(short, long)]
verbose: bool,
#[arg(short = 'n', long)]
dry_run: bool,
},
Sync {
#[arg(short, long)]
init: bool,
#[arg(short, long)]
recursive: bool,
#[arg(short, long)]
verbose: bool,
#[arg(short = 'n', long)]
dry_run: bool,
},
Delete {
branch: String,
#[arg(short, long)]
remote: bool,
#[arg(short, long)]
force: bool,
#[arg(short, long)]
verbose: bool,
#[arg(short = 'n', long)]
dry_run: bool,
},
Fetch {
#[arg(short, long)]
all: bool,
#[arg(short, long)]
verbose: bool,
#[arg(short = 'n', long)]
dry_run: bool,
},
Branch {
#[arg(short, long)]
all: bool,
#[arg(short, long)]
verbose: bool,
},
SubmoduleInit {
#[arg(short, long)]
recursive: bool,
#[arg(short, long)]
verbose: bool,
#[arg(short = 'n', long)]
dry_run: bool,
},
Diff {
#[arg(long)]
base: Option<String>,
#[arg(short, long, default_value = "diffs")]
output: String,
#[arg(short, long)]
staged: bool,
#[arg(short, long)]
verbose: bool,
},
Stage {
#[arg(short, long)]
verbose: bool,
#[arg(short = 'n', long)]
dry_run: bool,
},
MergeBack {
#[arg(short, long, default_value = "Pre-merge commit")]
message: String,
#[arg(short, long)]
verbose: bool,
#[arg(short = 'n', long)]
dry_run: bool,
},
Template {
#[command(subcommand)]
action: TemplateAction,
},
Update {
#[arg(long)]
no_git: bool,
#[arg(long)]
git_add: bool,
#[arg(long)]
tag: Option<String>,
#[arg(long, short)]
quiet: bool,
},
Scrap {
paths: Vec<std::path::PathBuf>,
#[command(subcommand)]
command: Option<ScrapCommands>,
},
Unscrap {
name: Option<String>,
#[arg(short, long)]
force: bool,
#[arg(short = 't', long)]
to: Option<std::path::PathBuf>,
},
Ldiff {
#[arg(default_value = "░")]
substitute_char: String,
},
Compact {
input: Option<String>,
#[arg(short, long)]
output: Option<String>,
#[arg(long)]
token_mode: bool,
},
AuditPath,
Ignore {
line: String,
},
LastHash {
file: String,
},
Code {
#[command(subcommand)]
action: Option<CodeAction>,
},
Test {
#[arg(long)]
dry_run: bool,
#[arg(long)]
install: bool,
#[arg(last = true)]
args: Vec<String>,
},
Serve {
#[arg(short, long, default_value = "3000")]
port: u16,
#[arg(long)]
debug: bool,
#[arg(short, long)]
workspace: Option<String>,
#[arg(long)]
parent_pid: Option<u32>,
},
Sample {
#[arg(long)]
project: bool,
#[arg(long)]
data: bool,
#[arg(long)]
force: bool,
#[arg(short, long, default_value = "sample-project")]
output: String,
},
Init {
first_task: Option<String>,
#[arg(long)]
name: Option<String>,
},
Check {
#[arg(long)]
no_network: bool,
#[arg(long, short)]
quiet: bool,
},
Relationship {
#[command(subcommand)]
action: RelationshipAction,
},
Note {
#[command(subcommand)]
action: NoteAction,
},
Database {
#[command(subcommand)]
action: DatabaseAction,
},
Continuity {
#[command(subcommand)]
action: ContinuityAction,
},
Version {
#[command(subcommand)]
action: VersionAction,
},
Wstemplate {
#[command(subcommand)]
action: WstemplateAction,
},
Each {
#[arg(short, long)]
file: Option<std::path::PathBuf>,
#[arg(long, short)]
quiet: bool,
},
}
#[derive(Subcommand, Debug)]
enum RelationshipAction {
Link {
from_entity: String,
#[arg(short, long)]
from_type: String,
to_entity: String,
#[arg(short, long)]
to_type: String,
#[arg(short, long, default_value = "depends_on")]
relationship_type: String,
#[arg(short, long)]
description: Option<String>,
},
List {
entity_id: String,
#[arg(short, long)]
entity_type: String,
#[arg(short, long)]
relationship_type: Option<String>,
#[arg(long)]
include_resolved: bool,
},
Unlink {
dependency_id: String,
#[arg(short, long)]
force: bool,
},
Resolve {
dependency_id: String,
#[arg(short, long)]
description: Option<String>,
},
Stats {
#[arg(short, long)]
detailed: bool,
#[arg(short, long, default_value = "human")]
format: String,
},
}
#[derive(Subcommand, Debug)]
enum NoteAction {
Add {
#[arg(short, long)]
entity_type: String,
#[arg(short = 'i', long)]
entity_id: String,
title: String,
content: String,
#[arg(short = 't', long, default_value = "observation")]
note_type: String,
#[arg(long)]
tags: Option<String>,
},
AddProject {
title: String,
content: String,
#[arg(short = 't', long, default_value = "architecture")]
note_type: String,
#[arg(long)]
tags: Option<String>,
},
List {
#[arg(short, long)]
entity_type: Option<String>,
#[arg(short = 'i', long)]
entity_id: Option<String>,
#[arg(short = 't', long)]
note_type: Option<String>,
#[arg(long)]
project_wide: bool,
#[arg(long)]
pinned: bool,
},
Search {
query: String,
#[arg(short = 't', long)]
note_type: Option<String>,
#[arg(short, long, default_value = "human")]
format: String,
},
Update {
note_id: String,
#[arg(long)]
title: Option<String>,
#[arg(long)]
content: Option<String>,
#[arg(long)]
tags: Option<String>,
},
Delete {
note_id: String,
#[arg(short, long)]
force: bool,
},
Pin {
note_id: String,
},
Link {
source_note_id: String,
target_id: String,
#[arg(short = 't', long, default_value = "entity")]
target_type: String,
#[arg(long)]
entity_type: Option<String>,
#[arg(short = 'l', long, default_value = "reference")]
link_type: String,
},
Unlink {
link_id: String,
#[arg(short, long)]
force: bool,
},
ListLinks {
id: String,
#[arg(long)]
incoming: bool,
#[arg(long)]
outgoing: bool,
#[arg(short, long, default_value = "human")]
format: String,
},
}
#[derive(Subcommand, Debug)]
enum TemplateAction {
Add {
name: String,
#[arg(long)]
template: String,
#[arg(short, long)]
output: Option<String>,
#[arg(short, long)]
description: Option<String>,
},
List,
Show {
name: String,
},
Enable {
name: String,
#[arg(long)]
disable: bool,
},
Remove {
name: String,
},
Render,
GenerateDocs {
#[arg(default_value = "all")]
doc_type: String,
#[arg(short, long)]
output: Option<String>,
#[arg(short, long)]
force: bool,
},
InitDocs {
#[arg(short, long)]
force: bool,
},
}
#[derive(Subcommand, Debug)]
enum DatabaseAction {
Backup {
#[arg(short, long)]
backup_dir: Option<String>,
#[arg(short, long)]
compress: bool,
#[arg(short, long, default_value = "10")]
max_backups: usize,
},
List {
#[arg(short, long)]
backup_dir: Option<String>,
#[arg(short, long, default_value = "table")]
format: String,
},
Restore {
backup_id: String,
#[arg(short, long)]
target: Option<String>,
#[arg(short, long)]
force: bool,
},
Cleanup {
#[arg(short, long)]
backup_dir: Option<String>,
#[arg(short, long, default_value = "10")]
max_backups: usize,
#[arg(short = 'n', long)]
dry_run: bool,
},
Health {
#[arg(short, long)]
performance: bool,
},
}
#[derive(Subcommand, Debug)]
enum CodeAction {
Tree {
#[arg(short, long, default_value = "3")]
depth: usize,
#[arg(long)]
hidden: bool,
#[arg(long)]
sizes: bool,
#[arg(long)]
extensions: Option<String>,
#[arg(long)]
no_ignore: bool,
},
Search {
pattern: String,
#[arg(short, long)]
files: Vec<PathBuf>,
#[arg(short, long)]
language: Option<String>,
#[arg(short, long, default_value = "3")]
context: usize,
#[arg(short, long, default_value = "1000")]
max_matches: usize,
#[arg(long, default_value = "human")]
format: String,
},
Transform {
pattern: String,
replacement: String,
#[arg(short, long)]
files: Vec<PathBuf>,
#[arg(short, long)]
language: Option<String>,
#[arg(long)]
dry_run: bool,
#[arg(long)]
no_backup: bool,
#[arg(long, default_value = "100")]
max_changes: usize,
},
Patterns {
language: String,
#[arg(long, default_value = "search")]
category: String,
},
Analyze {
#[arg(short, long)]
files: Vec<PathBuf>,
#[arg(short, long)]
language: Option<String>,
#[arg(long, default_value = "structure")]
analysis_type: String,
#[arg(long, default_value = "human")]
format: String,
},
}
#[derive(Subcommand, Debug)]
enum ArtifactAction {
List {
#[arg(short, long)]
artifact_type: Option<String>,
#[arg(short, long)]
session: Option<String>,
#[arg(short, long)]
recent: Option<u32>,
#[arg(short, long)]
verbose: bool,
},
Clean {
#[arg(short, long, default_value = "logs")]
artifact_type: String,
#[arg(long)]
dry_run: bool,
#[arg(short, long)]
force: bool,
},
Archive {
session_id: String,
#[arg(short, long, default_value = "tar.gz")]
format: String,
#[arg(short, long)]
output: Option<String>,
#[arg(long)]
remove_originals: bool,
},
Organize {
#[arg(long)]
categorize: bool,
#[arg(long)]
manifest: bool,
#[arg(long)]
tag: bool,
},
Search {
query: String,
#[arg(short, long)]
content: bool,
#[arg(short, long)]
names: bool,
#[arg(short, long, default_value = "20")]
limit: u32,
},
Show {
artifact_path: String,
#[arg(short, long)]
content: bool,
#[arg(short, long)]
metadata: bool,
},
Export {
#[arg(short, long)]
sessions: Vec<String>,
#[arg(short, long, default_value = "json")]
format: String,
#[arg(short, long)]
output: Option<String>,
#[arg(long)]
include_content: bool,
},
}
#[derive(Subcommand, Debug)]
enum ContinuityAction {
Save {
session_id: String,
#[arg(short, long)]
focus: String,
#[arg(short, long)]
notes: Option<String>,
},
Load {
session_id: String,
#[arg(short, long, default_value = "summary")]
format: String,
},
Transfer {
from_session: String,
to_session: String,
#[arg(short, long)]
force: bool,
},
List {
#[arg(short, long)]
project: Option<String>,
#[arg(short, long, default_value = "table")]
format: String,
},
Snapshot {
#[arg(short, long)]
project: Option<String>,
#[arg(short, long, default_value = "summary")]
format: String,
},
}
#[derive(Subcommand, Debug)]
enum VersionAction {
Show {
#[arg(long)]
verbose: bool,
#[arg(short, long, default_value = "human")]
format: String,
},
Major {
version: u32,
},
Tag {
#[arg(long, default_value = "v")]
prefix: String,
#[arg(short, long)]
message: Option<String>,
},
Info {
#[arg(long)]
include_history: bool,
},
}
#[derive(Subcommand, Debug)]
enum WstemplateAction {
Add {
path: PathBuf,
#[arg(long)]
alias: Option<String>,
},
Remove {
alias: String,
},
ListEntries,
List,
Render,
}
#[derive(Subcommand, Debug)]
enum ScrapCommands {
#[command(alias = "ls")]
List {
#[arg(short, long, default_value = "date")]
sort: String,
},
Clean {
#[arg(short, long, default_value = "30")]
days: u64,
#[arg(short = 'n', long)]
dry_run: bool,
},
Purge {
#[arg(short, long)]
force: bool,
},
Find {
pattern: String,
#[arg(short, long)]
content: bool,
},
Archive {
#[arg(short, long)]
output: Option<std::path::PathBuf>,
#[arg(short, long)]
remove: bool,
},
}
fn main() {
let debug_mode =
std::env::args().any(|arg| arg == "--debug" || arg == "-v" || arg == "--verbose");
let _log_guard = match setup_logging(debug_mode) {
Ok(guard) => guard,
Err(e) => {
eprintln!("Failed to initialize logging: {}", e);
exit(1);
}
};
log_version_info(env!("CARGO_PKG_VERSION"), option_env!("GIT_HASH"));
if !is_running_as_git_hook() {
if let Err(e) = setup_shell_completions() {
log_warning("Shell completions", &format!("Failed to setup: {}", e));
}
}
if let Err(e) = run() {
error!(error = %e, "Application error");
eprintln!("{}: {:#}", "Error".red(), e);
exit(1);
}
info!("Workspace tool completed successfully");
}
fn run() -> Result<()> {
let start_time = Instant::now();
let args = Args::parse();
let command = args.command.unwrap_or(Commands::Serve {
port: 3000,
debug: false,
workspace: None,
parent_pid: None,
});
let cwd = std::env::current_dir().context("Failed to get current directory")?;
if let Commands::Init { first_task, name } = command {
let ctx = AppContext::bootstrap_for_init(&cwd)?;
return setup_new_project(first_task, name, &ctx);
}
if let Commands::Check { no_network, quiet } = command {
return run_check_command(&cwd, no_network, quiet);
}
let project_root = workspace::require_project_root(&cwd)?;
let ctx = AppContext::bootstrap(&project_root)?;
match command {
Commands::Refactor { args } => {
log_operation_start("refactor", &format!("root: {:?}", args.root_dir));
match wsb::run_refac(args) {
Ok(()) => log_operation_complete("refactor", start_time.elapsed()),
Err(error) => {
log_operation_error("refactor", &error);
eprintln!("{}: {:#}", "Error".red(), error);
exit(1);
}
}
}
Commands::Status { verbose } => {
run_fleet_or_repo_command(wsb::fleet::cli::Commands::Status, verbose, false)?;
}
Commands::Checkout { branch, create, verbose, dry_run } => {
run_fleet_or_repo_command(
wsb::fleet::cli::Commands::Checkout { branch, create },
verbose,
dry_run,
)?;
}
Commands::Pull { rebase, verbose, dry_run } => {
run_fleet_or_repo_command(
wsb::fleet::cli::Commands::Pull { rebase },
verbose,
dry_run,
)?;
}
Commands::Push { force_with_lease, verbose, dry_run } => {
run_fleet_or_repo_command(
wsb::fleet::cli::Commands::Push { force_with_lease },
verbose,
dry_run,
)?;
}
Commands::Create { branch, from, push, verbose, dry_run } => {
run_fleet_or_repo_command(
wsb::fleet::cli::Commands::Create { branch, from, push },
verbose,
dry_run,
)?;
}
Commands::Commit { message, push, stage_all, exclude, include, verbose, dry_run } => {
run_fleet_or_repo_command(
wsb::fleet::cli::Commands::Commit { message, push, stage_all, exclude, include },
verbose,
dry_run,
)?;
}
Commands::Sync { init, recursive, verbose, dry_run } => {
run_fleet_or_repo_command(
wsb::fleet::cli::Commands::Sync { init, recursive },
verbose,
dry_run,
)?;
}
Commands::Delete { branch, remote, force, verbose, dry_run } => {
run_fleet_or_repo_command(
wsb::fleet::cli::Commands::Delete { branch, remote, force },
verbose,
dry_run,
)?;
}
Commands::Fetch { all, verbose, dry_run } => {
run_fleet_or_repo_command(
wsb::fleet::cli::Commands::Fetch { all },
verbose,
dry_run,
)?;
}
Commands::Branch { all, verbose } => {
run_fleet_or_repo_command(
wsb::fleet::cli::Commands::Branch { all },
verbose,
false,
)?;
}
Commands::SubmoduleInit { recursive, verbose, dry_run } => {
run_fleet_or_repo_command(
wsb::fleet::cli::Commands::Init { recursive },
verbose,
dry_run,
)?;
}
Commands::Diff { base, output, staged, verbose } => {
run_fleet_or_repo_command(
wsb::fleet::cli::Commands::Diff { base, output, staged },
verbose,
false,
)?;
}
Commands::Stage { verbose, dry_run } => {
run_fleet_or_repo_command(
wsb::fleet::cli::Commands::Stage,
verbose,
dry_run,
)?;
}
Commands::MergeBack { message, verbose, dry_run } => {
run_fleet_or_repo_command(
wsb::fleet::cli::Commands::MergeBack { message },
verbose,
dry_run,
)?;
}
Commands::Template { action } => {
handle_template_command(action, &ctx)?;
}
Commands::Update {
no_git,
git_add,
tag,
quiet,
} => {
log_operation_start(
"update",
&format!("no_git: {}, git_add: {}, tag: {:?}", no_git, git_add, tag),
);
update_state(no_git, git_add, tag.as_deref(), &ctx.project_root, quiet)?;
log_operation_complete("update", start_time.elapsed());
}
Commands::Scrap { paths, command } => {
run_scrap_command(paths, command)?;
}
Commands::Unscrap { name, force, to } => {
run_unscrap_command(name, force, to)?;
}
Commands::Ldiff { substitute_char } => {
run_ldiff_command(substitute_char)?;
}
Commands::Compact { input, output, token_mode } => {
run_compact_command(input, output, token_mode)?;
}
Commands::AuditPath => {
run_audit_path_command()?;
}
Commands::Ignore { line } => {
run_ignore_command(&line)?;
}
Commands::LastHash { file } => {
run_last_hash_command(&file)?;
}
Commands::Code { action } => {
let action = action.unwrap_or(CodeAction::Tree {
depth: 3,
hidden: false,
sizes: false,
extensions: None,
no_ignore: false,
});
handle_code_command(action)?;
}
Commands::Test {
dry_run,
install,
args,
} => {
handle_test_command(dry_run, install, args, &ctx.project_root)?;
}
Commands::Serve { port, debug, workspace, parent_pid } => {
let serve_ctx = if let Some(ws) = workspace {
let ws_path = PathBuf::from(ws);
let abs_path = if ws_path.is_absolute() {
ws_path
} else {
std::env::current_dir()?.join(ws_path)
};
AppContext::bootstrap(&abs_path)?
} else {
ctx.clone()
};
run_serve_command(port, debug, parent_pid, serve_ctx)?;
}
Commands::Sample {
project,
data,
force,
output,
} => {
run_sample_command(project, data, force, output, &ctx)?;
}
Commands::Relationship { action } => {
run_relationship_command(action, &ctx)?;
}
Commands::Note { action } => {
run_note_command(action, &ctx)?;
}
Commands::Database { action } => {
run_database_command(action, &ctx)?;
}
Commands::Continuity { action } => {
run_continuity_command(action, &ctx)?;
}
Commands::Version { action } => {
run_version_command(action, &ctx)?;
}
Commands::Wstemplate { action } => {
handle_wstemplate_command(action, &ctx.project_root)?;
}
Commands::Each { file, quiet } => {
run_each_command(&ctx.project_root, file, quiet)?;
}
Commands::Init { .. } | Commands::Check { .. } => unreachable!(),
}
Ok(())
}
fn current_dir_is_fleet_root() -> Result<bool> {
let cwd = std::env::current_dir().context("Failed to get current directory")?;
Ok(cwd.join(".gitmodules").is_file())
}
fn run_fleet_or_repo_command(
command: wsb::fleet::cli::Commands,
verbose: bool,
dry_run: bool,
) -> Result<()> {
if current_dir_is_fleet_root()? {
wsb::fleet::execute(command, verbose, dry_run)?;
} else {
execute_repo_git_command(command, verbose, dry_run)?;
}
Ok(())
}
fn execute_repo_git_command(
command: wsb::fleet::cli::Commands,
_verbose: bool,
dry_run: bool,
) -> Result<()> {
use wsb::fleet::cli::Commands;
use wsb::fleet::git::{GitOps, SubmoduleStatus};
use wsb::fleet::output::Formatter;
let cwd = std::env::current_dir().context("Failed to get current directory")?;
let repo_root = wsb::git_backend::repo_root(&cwd)?;
let repo = wsb::git_backend::open_repo(&repo_root)?;
match command {
Commands::Status => {
let is_detached = GitOps::is_detached_head(&repo).map_err(|e| anyhow::anyhow!("{}", e))?;
let current_branch = if is_detached {
None
} else {
Some(GitOps::current_branch(&repo).map_err(|e| anyhow::anyhow!("{}", e))?)
};
let is_clean = GitOps::is_clean(&repo).map_err(|e| anyhow::anyhow!("{}", e))?;
let uncommitted_files = if is_clean {
Vec::new()
} else {
GitOps::get_uncommitted_files(&repo).map_err(|e| anyhow::anyhow!("{}", e))?
};
let (ahead, behind) = if is_detached {
(0, 0)
} else {
GitOps::get_ahead_behind(&repo).unwrap_or((0, 0))
};
let repo_name = repo_root
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("[REPOSITORY]")
.to_string();
Formatter::print_status(&[SubmoduleStatus {
name: repo_name,
path: ".".to_string(),
current_branch,
is_clean,
is_detached,
ahead,
behind,
uncommitted_files,
exists: true,
}]);
}
Commands::Checkout { branch, create } => {
if dry_run {
Formatter::print_warning("DRY RUN: No changes will be made");
Formatter::print_info(&format!("Would checkout '{}'", branch));
return Ok(());
}
if create {
wsb::git_backend::create_branch(&repo, &branch, None)?;
} else {
wsb::git_backend::checkout_branch(&repo, &branch)?;
}
Formatter::print_success(&format!("Checked out '{}'", branch));
}
Commands::Pull { rebase } => {
if dry_run {
Formatter::print_warning("DRY RUN: No changes will be made");
Formatter::print_info("Would pull latest changes");
return Ok(());
}
wsb::git_backend::pull(&repo, rebase)?;
Formatter::print_success("Pulled latest changes");
}
Commands::Push { force_with_lease } => {
if dry_run {
Formatter::print_warning("DRY RUN: No changes will be made");
Formatter::print_info("Would push current branch");
return Ok(());
}
match wsb::git_backend::push_current_branch(&repo, Some("origin"), true, force_with_lease)? {
wsb::git_backend::PushOutcome::Updated => {
Formatter::print_success("Pushed current branch");
}
wsb::git_backend::PushOutcome::UpToDate => {
Formatter::print_info("Current branch is already up to date");
}
}
}
Commands::Create { branch, from, push } => {
if dry_run {
Formatter::print_warning("DRY RUN: No changes will be made");
Formatter::print_info(&format!(
"Would create branch '{}'{}",
branch,
from.as_deref()
.map(|base| format!(" from '{}'", base))
.unwrap_or_default()
));
return Ok(());
}
wsb::git_backend::create_branch(&repo, &branch, from.as_deref())?;
Formatter::print_success(&format!("Created branch '{}'", branch));
if push {
match wsb::git_backend::push_current_branch(&repo, Some("origin"), true, false)? {
wsb::git_backend::PushOutcome::Updated => {
Formatter::print_success("Pushed new branch");
}
wsb::git_backend::PushOutcome::UpToDate => {
Formatter::print_info("New branch is already up to date");
}
}
}
}
Commands::Commit { message, push, stage_all, .. } => {
if dry_run {
Formatter::print_warning("DRY RUN: No changes will be made");
if stage_all {
Formatter::print_info("Would stage all changes (git add -A)");
}
Formatter::print_info(&format!("Would commit changes with message '{}'", message));
return Ok(());
}
if stage_all {
wsb::git_backend::stage_all(&repo)?;
}
match wsb::git_backend::commit_all(&repo, &message)? {
Some(sha) => Formatter::print_success(&format!("Committed changes: {}", sha)),
None => Formatter::print_info("Nothing to commit"),
}
if push {
match wsb::git_backend::push_current_branch(&repo, Some("origin"), true, false)? {
wsb::git_backend::PushOutcome::Updated => Formatter::print_success("Pushed commit"),
wsb::git_backend::PushOutcome::UpToDate => {
Formatter::print_info("Current branch is already up to date")
}
}
}
}
Commands::Sync { init, recursive } => {
if init || recursive {
anyhow::bail!(
"'wsb sync --init/--recursive' requires a fleet root with a .gitmodules file"
);
}
if dry_run {
Formatter::print_warning("DRY RUN: No changes will be made");
Formatter::print_info("Would fetch and pull current branch");
return Ok(());
}
wsb::git_backend::fetch(&repo, Some("origin"))?;
wsb::git_backend::pull(&repo, false)?;
Formatter::print_success("Fetched and pulled current branch");
}
Commands::Delete {
branch,
remote,
force,
} => {
if dry_run {
Formatter::print_warning("DRY RUN: No changes will be made");
Formatter::print_info(&format!("Would delete branch '{}'", branch));
if remote {
Formatter::print_info(&format!("Would delete remote branch '{}'", branch));
}
return Ok(());
}
wsb::git_backend::delete_local_branch(&repo, &branch, force)?;
Formatter::print_success(&format!("Deleted branch '{}'", branch));
if remote {
wsb::git_backend::delete_remote_branch(&repo, Some("origin"), &branch)?;
Formatter::print_success(&format!("Deleted remote branch '{}'", branch));
}
}
Commands::Fetch { all } => {
if dry_run {
Formatter::print_warning("DRY RUN: No changes will be made");
Formatter::print_info("Would fetch remote updates");
return Ok(());
}
if all {
wsb::git_backend::fetch_all(&repo)?;
} else {
wsb::git_backend::fetch(&repo, Some("origin"))?;
}
Formatter::print_success("Fetched remote updates");
}
Commands::Branch { all } => {
let current = wsb::git_backend::current_branch(&repo).ok();
let branches = if all {
let mut local = wsb::git_backend::local_branches(&repo)?;
local.extend(wsb::git_backend::remote_branches(&repo)?);
local
} else {
wsb::git_backend::local_branches(&repo)?
};
Formatter::print_branches("[REPOSITORY]", &branches, current.as_deref());
}
Commands::Init { recursive: _ } => {
anyhow::bail!("'wsb submodule-init' requires a fleet root with a .gitmodules file");
}
Commands::Diff {
base,
output,
staged,
} => {
let outdir = std::path::Path::new(&output);
let files = wsb::fleet::commands::diff::generate_per_file_diffs(
&repo_root,
base.as_deref(),
outdir,
staged,
)
.map_err(|e| anyhow::anyhow!("{}", e))?;
if files.is_empty() {
Formatter::print_info("No changes to diff");
} else {
Formatter::print_success(&format!(
"{} diff file(s) written to '{}'",
files.len(),
output
));
for file in &files {
println!(" {}.diff", file);
}
}
}
Commands::Stage => {
if dry_run {
Formatter::print_warning("DRY RUN: No changes will be made");
Formatter::print_info("Would stage all changes");
return Ok(());
}
wsb::git_backend::stage_all(&repo)?;
Formatter::print_success("Staged all changes");
}
Commands::MergeBack { message } => {
if dry_run {
Formatter::print_warning("DRY RUN: No changes will be made");
Formatter::print_info("Would merge current branch back into the default branch");
return Ok(());
}
let current_branch = wsb::git_backend::current_branch(&repo)?;
let default_branch = wsb::git_backend::get_default_branch(&repo)?;
if current_branch == default_branch {
anyhow::bail!("already on '{}'", default_branch);
}
if !wsb::git_backend::is_clean(&repo)? {
wsb::git_backend::stage_all(&repo)?;
}
let _ = wsb::git_backend::commit_all(&repo, &message)?;
let _ = wsb::git_backend::push_current_branch(&repo, Some("origin"), true, false)?;
wsb::git_backend::checkout_branch(&repo, &default_branch)?;
wsb::git_backend::merge_ff(&repo, ¤t_branch)?;
let _ = wsb::git_backend::push_current_branch(&repo, Some("origin"), false, false)?;
Formatter::print_success("Merge-back complete");
}
}
Ok(())
}
fn make_spinner(quiet: bool, msg: &'static str) -> indicatif::ProgressBar {
if quiet {
indicatif::ProgressBar::hidden()
} else {
let pb = indicatif::ProgressBar::new_spinner();
pb.set_style(
indicatif::ProgressStyle::with_template("{spinner:.cyan} {msg}")
.unwrap()
.tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
);
pb.set_message(msg);
pb.enable_steady_tick(std::time::Duration::from_millis(80));
pb
}
}
fn make_progress_bar(quiet: bool, total: u64) -> indicatif::ProgressBar {
if quiet {
indicatif::ProgressBar::hidden()
} else {
let pb = indicatif::ProgressBar::new(total);
pb.set_style(
indicatif::ProgressStyle::with_template(
"{spinner:.cyan} [{pos}/{len}] {wide_msg}",
)
.unwrap()
.tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
);
pb.enable_steady_tick(std::time::Duration::from_millis(80));
pb
}
}
fn update_state(no_git: bool, git_add: bool, tag: Option<&str>, project_root: &Path, quiet: bool) -> Result<()> {
if !no_git && !is_git_repository() {
anyhow::bail!(
"Not in a git repository. Run 'wsb init' here first, or use 'wsb update --no-git' for a non-git directory."
);
}
let pb = make_spinner(quiet, "loading configuration…");
let config = StateConfig::load(project_root)?;
let workspace_state = WorkspaceState::load(project_root)?;
if let Some(t) = tag {
wsb::state::validate_git_tag(t)?;
}
pb.set_message("calculating version…");
let mut version_info = if no_git {
calculate_version_without_git(project_root)?
} else {
calculate_version(project_root)?
};
if let Some(t) = tag {
version_info.full_version = format!("{}_{}", version_info.full_version, t);
}
pb.set_message(format!("writing {} → {}…", "version", &config.version_file));
update_version_file(&version_info, &config)?;
if !config.version_file.is_empty() {
info!("Updated version file: {}", config.version_file);
if !quiet {
pb.println(format!(" {} updated {}", "✓".green(), config.version_file));
}
}
pb.set_message("rendering .tera templates…");
let template_manager = TemplateManager::new(&workspace_state)?;
let project_name = workspace_state.project_name.as_deref();
let rendered_tera_files = template_manager.render_all_templates(&version_info, project_name)?;
if !rendered_tera_files.is_empty() {
info!(
"Rendered {} .tera templates: {}",
rendered_tera_files.len(),
rendered_tera_files.join(", ")
);
if !quiet {
pb.println(format!(" {} rendered {} .tera template(s)", "✓".green(), rendered_tera_files.len()));
for file in &rendered_tera_files {
pb.println(format!(" {}", file));
}
}
}
let full_version = version_info.full_version.clone();
pb.set_message("rendering .wstemplate files…");
let mut rendered_wstemplate_files: Vec<String> = Vec::new();
{
let entry = workspace_state.effective_wstemplate_entry();
let engine = WstemplateEngine::new(
version_info,
workspace_state.project_name.clone(),
entry.alias,
project_root.to_path_buf(),
entry.root,
);
let rendered = engine.render_relevant()?;
for r in &rendered {
info!(
"wstemplate: {} -> {}",
r.source_path.display(),
r.output_path.display()
);
rendered_wstemplate_files.push(r.output_path.display().to_string());
}
if !rendered.is_empty() && !quiet {
pb.println(format!(" {} rendered {} .wstemplate file(s)", "✓".green(), rendered.len()));
}
}
pb.set_message("saving workspace state…");
workspace_state.save(&project_root)?;
if !no_git && git_add && is_git_repository() {
pb.set_message("staging files…");
let mut files_to_add = Vec::new();
if !config.version_file.is_empty() {
files_to_add.push(config.version_file.clone());
}
for file in collect_project_files_for_git_add(&project_root, &config)? {
if !files_to_add.contains(&file) {
files_to_add.push(file);
}
}
for file in &rendered_tera_files {
if !files_to_add.contains(file) {
files_to_add.push(file.clone());
}
}
for file in &rendered_wstemplate_files {
if !files_to_add.contains(file) {
files_to_add.push(file.clone());
}
}
if !files_to_add.is_empty() {
let added_files = add_files_to_git(&files_to_add)?;
if !added_files.is_empty() {
info!(
"Added {} files to git staging area: {}",
added_files.len(),
added_files.join(", ")
);
if !quiet {
pb.println(format!(" {} staged {} file(s)", "✓".green(), added_files.len()));
for file in &added_files {
pb.println(format!(" {}", file));
}
}
}
}
}
if let Some(t) = tag {
if is_git_repository() {
pb.set_message(format!("creating tag {}…", t));
let tag_name = format!("v{}", full_version);
let tag_message = format!("Tagged version {}", full_version);
let cwd = std::env::current_dir().context("Failed to determine current directory")?;
let repo = wsb::git_backend::discover_repo(&cwd)?;
wsb::git_backend::create_tag(&repo, &tag_name, &tag_message)?;
info!("Created git tag: {} (tag: {})", tag_name, t);
if !quiet {
pb.println(format!(" {} created tag {}", "✓".green(), tag_name.green().bold()));
}
} else {
anyhow::bail!("--tag requires a git repository");
}
}
pb.finish_and_clear();
if !quiet {
println!("{} {}", "✓".green().bold(), full_version.bold());
}
Ok(())
}
fn collect_project_files_for_git_add(project_root: &Path, config: &StateConfig) -> Result<Vec<String>> {
let mut files = Vec::new();
if config.auto_detect_project_files {
for project_file in detect_project_files(project_root)? {
let path = project_file.path.to_string_lossy().to_string();
if !files.contains(&path) {
files.push(path);
}
}
}
for configured_path in &config.project_files {
let full_path = project_root.join(configured_path);
if full_path.exists() {
let path = full_path.to_string_lossy().to_string();
if !files.contains(&path) {
files.push(path);
}
}
}
Ok(files)
}
fn handle_wstemplate_command(action: WstemplateAction, project_root: &Path) -> Result<()> {
let mut workspace_state = WorkspaceState::load(project_root)?;
match action {
WstemplateAction::Add { path, alias } => {
let scan_root = path
.canonicalize()
.with_context(|| format!("Cannot resolve path: {}", path.display()))?;
let alias = alias.unwrap_or_else(|| WstemplateEngine::derive_alias(&project_root));
anyhow::ensure!(
!alias.is_empty()
&& alias.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
&& !alias.starts_with(|c: char| c.is_ascii_digit()),
"Invalid alias '{}': must match [a-z_][a-z0-9_]* (Tera identifier)",
alias
);
let entry = WstemplateEntry {
alias: alias.clone(),
root: scan_root.clone(),
};
workspace_state.set_wstemplate_entry(entry);
workspace_state.save(&project_root)?;
println!("{} wstemplate entries:", "1".bold());
println!(" {} \u{2192} {}", alias, scan_root.display());
}
WstemplateAction::Remove { alias } => match workspace_state.wstemplate_entry() {
Some(entry) if entry.alias == alias => {
workspace_state.clear_wstemplate_entry();
workspace_state.save(&project_root)?;
println!("Removed wstemplate entry '{}'", alias);
}
Some(entry) => {
anyhow::bail!(
"No entry with alias '{}'. This project's alias is '{}'.",
alias,
entry.alias
);
}
None => {
anyhow::bail!("No wstemplate entry configured for this project.");
}
},
WstemplateAction::ListEntries => match workspace_state.wstemplate_entry() {
Some(entry) => {
println!("{} wstemplate entries:", "1".bold());
println!(" {} \u{2192} {}", entry.alias, entry.root.display());
}
None => {
let entry = workspace_state.effective_wstemplate_entry();
println!("{} wstemplate entries:", "0".bold());
println!(
" effective default: {} \u{2192} {}",
entry.alias,
entry.root.display()
);
}
},
WstemplateAction::List => {
let entry = workspace_state.effective_wstemplate_entry();
let version_info = calculate_version(&project_root)?;
let engine = WstemplateEngine::new(
version_info,
workspace_state.project_name.clone(),
entry.alias,
project_root.to_path_buf(),
entry.root,
);
let project_roots = engine.discover_project_roots()?;
let templates = engine.discover_relevant(&project_roots)?;
if templates.is_empty() {
println!("No relevant .wstemplate files found.");
} else {
println!("{} relevant .wstemplate files:", templates.len());
for path in &templates {
println!(" {}", path.display());
}
}
}
WstemplateAction::Render => {
let entry = workspace_state.effective_wstemplate_entry();
let version_info = calculate_version(&project_root)?;
let engine = WstemplateEngine::new(
version_info,
workspace_state.project_name.clone(),
entry.alias,
project_root.to_path_buf(),
entry.root,
);
let rendered = engine.render_relevant()?;
if rendered.is_empty() {
println!("No templates to render.");
} else {
println!("Rendered {} templates:", rendered.len());
for r in &rendered {
println!(
" {} \u{2192} {}",
r.source_path.display(),
r.output_path.display()
);
}
}
}
}
Ok(())
}
fn run_scrap_command(paths: Vec<std::path::PathBuf>, command: Option<ScrapCommands>) -> Result<()> {
let mut args = Vec::new();
match command {
Some(ScrapCommands::List { sort }) => {
args.push("list".to_string());
args.push("--sort".to_string());
args.push(sort);
}
Some(ScrapCommands::Clean { days, dry_run }) => {
args.push("clean".to_string());
args.push("--days".to_string());
args.push(days.to_string());
if dry_run {
args.push("--dry-run".to_string());
}
}
Some(ScrapCommands::Purge { force }) => {
args.push("purge".to_string());
if force {
args.push("--force".to_string());
}
}
Some(ScrapCommands::Find { pattern, content }) => {
args.push("find".to_string());
args.push(pattern);
if content {
args.push("--content".to_string());
}
}
Some(ScrapCommands::Archive { output, remove }) => {
args.push("archive".to_string());
if let Some(output_path) = output {
args.push("--output".to_string());
args.push(output_path.to_string_lossy().to_string());
}
if remove {
args.push("--remove".to_string());
}
}
None => {
for path in paths {
args.push(path.to_string_lossy().to_string());
}
}
}
wsb::run_scrap(args)
}
fn run_unscrap_command(
name: Option<String>,
force: bool,
to: Option<std::path::PathBuf>,
) -> Result<()> {
let mut args = Vec::new();
if let Some(item_name) = name {
args.push(item_name);
}
if force {
args.push("--force".to_string());
}
if let Some(target_path) = to {
args.push("--to".to_string());
args.push(target_path.to_string_lossy().to_string());
}
wsb::run_unscrap(args)
}
fn run_ldiff_command(substitute_char: String) -> Result<()> {
let mut chars = substitute_char.chars();
let ch = chars.next().ok_or_else(|| anyhow::anyhow!("Substitute character cannot be empty"))?;
if chars.next().is_some() {
anyhow::bail!(
"Substitute character must be exactly one Unicode character, got: '{}'",
substitute_char
);
}
wsb::ldiff::process_stdin(ch)
}
fn run_compact_command(input: Option<String>, output: Option<String>, token_mode: bool) -> Result<()> {
let result = match input {
Some(path) => wsb::compact::compact_file(std::path::Path::new(&path), token_mode)
.with_context(|| format!("Failed to compact file: {}", path))?,
None => {
if std::io::stdin().is_terminal() {
eprintln!("Reading from stdin. Pipe input or provide a file path.");
eprintln!("Usage: wsb compact [FILE] [-o OUTPUT] [--token-mode]");
anyhow::bail!("No input provided and stdin is a terminal");
}
wsb::compact::compact_stdin(token_mode)
.with_context(|| "Failed to compact stdin")?
}
};
match output {
Some(path) => {
std::fs::write(&path, &result)
.with_context(|| format!("Failed to write output to: {}", path))?;
}
None => {
print!("{}", result);
}
}
Ok(())
}
fn run_audit_path_command() -> Result<()> {
let path_var = std::env::var("PATH").context("PATH environment variable not set")?;
let dirs: Vec<&str> = path_var.split(':').collect();
let mut all_bins: std::collections::HashMap<String, Vec<std::path::PathBuf>> = std::collections::HashMap::new();
for dir in &dirs {
let dir_path = std::path::Path::new(dir);
if let Ok(entries) = std::fs::read_dir(dir_path) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
let is_executable = std::fs::metadata(&path)
.map(|m| {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
m.permissions().mode() & 0o111 != 0
}
#[cfg(not(unix))]
{
true
}
})
.unwrap_or(false);
if is_executable {
all_bins.entry(name.to_string())
.or_default()
.push(path);
}
}
}
}
}
}
let mut shadowed: Vec<(&String, &Vec<std::path::PathBuf>)> = all_bins
.iter()
.filter(|(_, paths)| paths.len() > 1)
.collect();
shadowed.sort_by_key(|(name, _)| name.clone());
if shadowed.is_empty() {
println!("No shadowed executables found on PATH.");
return Ok(());
}
for (name, paths) in &shadowed {
println!("== {} ==", name);
for path in paths.iter() {
println!(" {}", path.display());
}
println!();
}
println!("{} shadowed executable(s) found.", shadowed.len());
Ok(())
}
fn run_ignore_command(line: &str) -> Result<()> {
let current_dir = std::env::current_dir().context("Cannot determine current directory")?;
let entries = std::fs::read_dir(¤t_dir)
.with_context(|| format!("Cannot read directory: {}", current_dir.display()))?;
let mut modified_count = 0;
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let gitignore = path.join(".gitignore");
if gitignore.exists() {
let content = std::fs::read_to_string(&gitignore)
.with_context(|| format!("Cannot read {}", gitignore.display()))?;
if !content.lines().any(|l| l == line) {
let mut new_content = content;
if !new_content.ends_with('\n') {
new_content.push('\n');
}
new_content.push_str(line);
new_content.push('\n');
std::fs::write(&gitignore, new_content)
.with_context(|| format!("Cannot write {}", gitignore.display()))?;
println!("Added '{}' to {}", line, gitignore.display());
modified_count += 1;
}
}
}
}
if modified_count == 0 {
println!("No .gitignore files needed updating (line already present or no .gitignore files found).");
} else {
println!("Updated {} .gitignore file(s).", modified_count);
}
Ok(())
}
fn run_last_hash_command(file: &str) -> Result<()> {
let output = std::process::Command::new("git")
.args(["log", "-n", "1", "--follow", "--pretty=format:%H", "--", file])
.output()
.context("Failed to execute git log")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("git log failed: {}", stderr.trim());
}
let hash = String::from_utf8_lossy(&output.stdout);
let hash = hash.trim();
if hash.is_empty() {
anyhow::bail!("No commit found for file: {}", file);
}
println!("{}", hash);
Ok(())
}
fn calculate_version(project_root: &std::path::Path) -> Result<VersionInfo> {
let version_info = wsb::state::calculate_workspace_version(project_root)?;
info!("Version calculated: {}", version_info.full_version);
Ok(version_info)
}
fn calculate_version_without_git(project_root: &std::path::Path) -> Result<VersionInfo> {
let version_path = project_root.join("version.txt");
let version_info = if version_path.exists() {
wsb::state::read_version_from_path(project_root)?
} else {
VersionInfo {
major_version: "v0".to_string(),
minor_version: 0,
patch_version: 0,
full_version: "0.0.0".to_string(),
}
};
info!(
"Version calculated without git state: {}",
version_info.full_version
);
Ok(version_info)
}
fn log_action(message: &str) {
match log_to_file(message) {
Ok(_) => {}
Err(_) => {}
}
}
fn log_to_file(message: &str) -> Result<()> {
info!("{}", message);
Ok(())
}
fn handle_template_command(action: TemplateAction, ctx: &AppContext) -> Result<()> {
match action {
TemplateAction::Add {
name,
template,
output,
description,
} => {
let project_root = ctx.project_root.clone();
let workspace_state = WorkspaceState::load(&project_root)?;
let mut template_manager = TemplateManager::new(&workspace_state)?;
let output = output.context("Missing required --output path")?;
let template_content = resolve_template_content(&template)?;
template_manager.add_template(&name, &template_content, &output, description)?;
println!("Added template '{}' → {}", name, output);
Ok(())
}
TemplateAction::List => {
let project_root = ctx.project_root.clone();
let workspace_state = WorkspaceState::load(&project_root)?;
let template_manager = TemplateManager::new(&workspace_state)?;
let mut templates = template_manager.list_templates();
templates.sort_by(|a, b| a.name.cmp(&b.name));
if templates.is_empty() {
println!("No templates configured");
return Ok(());
}
for template in templates {
let status = if template.enabled {
"enabled"
} else {
"disabled"
};
println!("{} → {} ({})", template.name, template.output_path, status);
if let Some(description) = &template.description {
println!(" {}", description);
}
}
Ok(())
}
TemplateAction::Show { name } => {
let project_root = ctx.project_root.clone();
let workspace_state = WorkspaceState::load(&project_root)?;
let template_manager = TemplateManager::new(&workspace_state)?;
let template = template_manager
.get_template(&name)
.ok_or_else(|| anyhow::anyhow!("Template '{}' not found", name))?;
let template_path = workspace_state
.workspace_dir()
.join("templates")
.join(&template.source_path);
let template_content = fs::read_to_string(&template_path).with_context(|| {
format!("Failed to read template source: {}", template_path.display())
})?;
println!("Template: {}", template.name);
println!("Output: {}", template.output_path);
println!("Enabled: {}", if template.enabled { "Yes" } else { "No" });
if let Some(description) = &template.description {
println!("Description: {}", description);
}
println!();
println!("{}", template_content);
Ok(())
}
TemplateAction::Enable { name, disable } => {
let project_root = ctx.project_root.clone();
let workspace_state = WorkspaceState::load(&project_root)?;
let mut template_manager = TemplateManager::new(&workspace_state)?;
let changed = template_manager.set_template_enabled(&name, !disable)?;
if !changed {
println!("Template '{}' not found", name);
return Ok(());
}
if disable {
println!("Template '{}' disabled", name);
} else {
println!("Template '{}' enabled", name);
}
Ok(())
}
TemplateAction::Remove { name } => {
let project_root = ctx.project_root.clone();
let workspace_state = WorkspaceState::load(&project_root)?;
let mut template_manager = TemplateManager::new(&workspace_state)?;
let removed = template_manager.remove_template(&name)?;
if removed {
println!("Removed template '{}'", name);
} else {
println!("Template '{}' not found", name);
}
Ok(())
}
TemplateAction::Render => {
let project_root = ctx.project_root.clone();
let workspace_state = WorkspaceState::load(&project_root)?;
let template_manager = TemplateManager::new(&workspace_state)?;
let rendered = template_manager.render_all_templates(
&calculate_version(&project_root)?,
workspace_state.project_name.as_deref(),
)?;
if rendered.is_empty() {
println!("No enabled templates to render");
} else {
println!("Rendered {} template(s)", rendered.len());
for output in rendered {
println!("{}", output);
}
}
Ok(())
}
TemplateAction::GenerateDocs {
doc_type,
output,
force,
} => {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async { handle_generate_docs(&doc_type, output.as_deref(), force, ctx).await })
}
TemplateAction::InitDocs { force } => handle_init_docs(force),
}
}
fn resolve_template_content(template: &str) -> Result<String> {
let template_path = Path::new(template);
if template_path.exists() {
return fs::read_to_string(template_path)
.with_context(|| format!("Failed to read template file: {}", template_path.display()));
}
let looks_like_file_path = !template.contains('\n')
&& !template.contains("{{")
&& !template.contains("{%")
&& (template.contains(std::path::MAIN_SEPARATOR) || template_path.extension().is_some());
if looks_like_file_path {
anyhow::bail!("Template file not found: {}", template);
}
Ok(template.to_string())
}
async fn handle_generate_docs(doc_type: &str, output_dir: Option<&str>, force: bool, ctx: &AppContext) -> Result<()> {
use tera::Tera;
let db = &ctx.db;
let project = db.project_repo.list(None, None).await?.into_iter().next().ok_or_else(|| {
anyhow::anyhow!("No active project found. Run 'wsb sample --create' first.")
})?;
let features = db.feature_repo.list(None, None).await?;
let sessions = db.work_session_repo.list(None, None).await?.into_iter().filter(|s| s.project_id == project.id).collect::<Vec<_>>();
let tasks = db.task_repo.list(None, None).await?;
let total_features = features.len();
let implemented_features = features
.iter()
.filter(|f| {
matches!(
f.state.as_str(),
"implemented_no_tests" | "implemented_failing_tests" | "implemented_passing_tests"
)
})
.count();
let tested_features = features
.iter()
.filter(|f| {
matches!(
f.state.as_str(),
"implemented_passing_tests" | "implemented_failing_tests" | "tests_broken"
)
})
.count();
let implementation_percentage = if total_features > 0 {
(implemented_features * 100) / total_features
} else {
0
};
let test_percentage = if total_features > 0 {
(tested_features * 100) / total_features
} else {
0
};
let mut tera = Tera::new("src/templates/*.tera")?;
tera.register_filter(
"feature_state_emoji",
|value: &tera::Value, _: &HashMap<String, tera::Value>| match value.as_str().unwrap_or("") {
"not_implemented" => Ok(tera::Value::String("❌".to_string())),
"implemented_no_tests" => Ok(tera::Value::String("🟠".to_string())),
"implemented_failing_tests" => Ok(tera::Value::String("🟡".to_string())),
"implemented_passing_tests" => Ok(tera::Value::String("🟢".to_string())),
"tests_broken" => Ok(tera::Value::String("⚠️".to_string())),
"critical_issue" => Ok(tera::Value::String("🔴".to_string())),
_ => Ok(tera::Value::String("❓".to_string())),
},
);
let output_path = output_dir.unwrap_or(".");
match doc_type {
"all" => {
generate_claude_md(
&tera,
&project,
&features,
&sessions,
&tasks,
implementation_percentage,
test_percentage,
output_path,
force,
)
.await?;
generate_features_md(
&tera,
&project,
&features,
total_features,
implementation_percentage,
test_percentage,
output_path,
force,
)
.await?;
generate_progress_md(&tera, &sessions, output_path, force).await?;
generate_status_report(
&project,
&features,
&tasks,
&sessions,
implementation_percentage,
test_percentage,
output_path,
force,
)
.await?;
}
"claude" => {
generate_claude_md(
&tera,
&project,
&features,
&sessions,
&tasks,
implementation_percentage,
test_percentage,
output_path,
force,
)
.await?;
}
"features" => {
generate_features_md(
&tera,
&project,
&features,
total_features,
implementation_percentage,
test_percentage,
output_path,
force,
)
.await?;
}
"progress" => {
generate_progress_md(&tera, &sessions, output_path, force).await?;
}
"status" => {
generate_status_report(
&project,
&features,
&tasks,
&sessions,
implementation_percentage,
test_percentage,
output_path,
force,
)
.await?;
}
_ => {
anyhow::bail!("Unknown documentation type: {}. Use 'claude', 'features', 'progress', 'status', or 'all'", doc_type);
}
}
println!(
"✅ Documentation generation complete for type: {}",
doc_type
);
Ok(())
}
async fn generate_claude_md(
tera: &tera::Tera,
project: &wsb::entities::schema_models::Project,
features: &[wsb::entities::schema_models::Feature],
sessions: &[wsb::entities::schema_models::Session],
_tasks: &[wsb::entities::schema_models::Task],
implementation_percentage: usize,
test_percentage: usize,
output_path: &str,
force: bool,
) -> Result<()> {
let mut context = tera::Context::new();
context.insert("project", project);
context.insert("features", features);
context.insert(
"recent_sessions",
&sessions.iter().take(3).collect::<Vec<_>>(),
);
context.insert("total_features", &features.len());
context.insert(
"implemented_features",
&features
.iter()
.filter(|f| {
matches!(
f.state.as_str(),
"implemented_no_tests"
| "implemented_failing_tests"
| "implemented_passing_tests"
)
})
.count(),
);
context.insert(
"tested_features",
&features
.iter()
.filter(|f| matches!(f.state.as_str(), "implemented_passing_tests"))
.count(),
);
context.insert("implementation_percentage", &implementation_percentage);
context.insert("test_percentage", &test_percentage);
context.insert("generated_at", &chrono::Utc::now());
let rendered = tera.render("claude_md.tera", &context)?;
let output_file = std::path::Path::new(output_path).join("CLAUDE.md");
write_doc_file(&output_file, &rendered, force)?;
println!("Generated: {}", output_file.display());
Ok(())
}
async fn generate_features_md(
tera: &tera::Tera,
project: &wsb::entities::schema_models::Project,
features: &[wsb::entities::schema_models::Feature],
total_features: usize,
implementation_percentage: usize,
test_percentage: usize,
output_path: &str,
force: bool,
) -> Result<()> {
use std::collections::BTreeMap;
let mut context = tera::Context::new();
context.insert("project", project);
context.insert("features", features);
context.insert("total_features", &total_features);
context.insert("implementation_percentage", &implementation_percentage);
context.insert("test_percentage", &test_percentage);
context.insert("generated_at", &chrono::Utc::now());
let mut features_by_category: BTreeMap<String, Vec<&wsb::entities::schema_models::Feature>> =
BTreeMap::new();
for feature in features {
let category = feature.category.as_deref().unwrap_or("General").to_string();
features_by_category
.entry(category)
.or_insert_with(Vec::new)
.push(feature);
}
context.insert("features_by_category", &features_by_category);
let mut feature_counts = HashMap::new();
for feature in features {
let state_name = format!("{:?}", feature.state);
*feature_counts.entry(state_name).or_insert(0) += 1;
}
context.insert("feature_counts", &feature_counts);
let rendered = tera.render("features_md.tera", &context)?;
let output_file = std::path::Path::new(output_path)
.join("internal")
.join("FEATURES.md");
if let Some(parent) = output_file.parent() {
std::fs::create_dir_all(parent)?;
}
write_doc_file(&output_file, &rendered, force)?;
println!("Generated: {}", output_file.display());
Ok(())
}
async fn generate_progress_md(
_tera: &tera::Tera,
sessions: &[wsb::entities::schema_models::Session],
output_path: &str,
force: bool,
) -> Result<()> {
let mut content = String::new();
content.push_str("# Progress Tracking - Database Generated\n\n");
content.push_str(&format!(
"**Generated**: {}\n",
chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
));
content.push_str("**Source**: Database-driven documentation generation\n\n");
content.push_str("## Recent Sessions\n\n");
for session in sessions.iter().take(5) {
content.push_str(&format!("### {}\n", &session.title));
content.push_str(&format!("**Status**: {}\n", &session.status));
content.push_str(&format!("**Date**: {}\n", &session.date));
if let Some(start_time) = &session.start_time {
content.push_str(&format!("**Started**: {} {}\n", &session.date, start_time));
}
if let Some(end_time) = &session.end_time {
content.push_str(&format!("**Ended**: {} {}\n", &session.date, end_time));
}
content.push_str("\n");
}
content.push_str("---\n\n*Generated from database entities*\n");
let output_file = std::path::Path::new(output_path)
.join("internal")
.join("PROGRESS_TRACKING.md");
if let Some(parent) = output_file.parent() {
std::fs::create_dir_all(parent)?;
}
write_doc_file(&output_file, &content, force)?;
println!("Generated: {}", output_file.display());
Ok(())
}
async fn generate_status_report(
project: &wsb::entities::schema_models::Project,
features: &[wsb::entities::schema_models::Feature],
tasks: &[wsb::entities::schema_models::Task],
sessions: &[wsb::entities::schema_models::Session],
implementation_percentage: usize,
test_percentage: usize,
output_path: &str,
force: bool,
) -> Result<()> {
let mut content = String::new();
content.push_str("# Project Status Report\n\n");
content.push_str(&format!(
"**Generated**: {}\n",
chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
));
content.push_str(&format!("**Project**: {}\n", project.name));
content.push_str(&format!(
"**Phase**: {}\n\n",
project.current_phase.as_deref().unwrap_or("Unknown")
));
content.push_str("## Implementation Status\n\n");
content.push_str(&format!("- **Total Features**: {}\n", features.len()));
content.push_str(&format!(
"- **Implementation Score**: {}%\n",
implementation_percentage
));
content.push_str(&format!("- **Test Coverage**: {}%\n", test_percentage));
content.push_str(&format!(
"- **Active Tasks**: {}\n",
tasks.iter().filter(|t| t.status == "in_progress").count()
));
content.push_str(&format!(
"- **Completed Tasks**: {}\n",
tasks.iter().filter(|t| t.status == "completed").count()
));
content.push_str(&format!("- **Total Sessions**: {}\n\n", sessions.len()));
content.push_str("## Feature State Distribution\n\n");
let mut state_counts = std::collections::HashMap::new();
for feature in features {
*state_counts.entry(&feature.state).or_insert(0) += 1;
}
for (state, count) in state_counts {
let emoji = match state.as_str() {
"not_implemented" => "❌",
"implemented_no_tests" => "🟠",
"implemented_failing_tests" => "🟡",
"implemented_passing_tests" => "🟢",
"tests_broken" => "⚠️",
"critical_issue" => "🔴",
_ => "❓",
};
content.push_str(&format!("- {} {}: {}\n", emoji, state, count));
}
content.push_str("\n---\n\n*Generated from database entities*\n");
let output_file = std::path::Path::new(output_path).join("PROJECT_STATUS.md");
write_doc_file(&output_file, &content, force)?;
println!("Generated: {}", output_file.display());
Ok(())
}
fn handle_init_docs(_force: bool) -> Result<()> {
info!("Documentation template initialization not yet implemented");
Ok(())
}
fn write_doc_file(file_path: &Path, content: &str, force: bool) -> Result<()> {
if file_path.exists() && !force {
eprintln!(
"{}: File {} already exists. Use --force to overwrite.",
"Error".red(),
file_path.display()
);
return Ok(());
}
if let Some(parent) = file_path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(file_path, content)?;
Ok(())
}
fn is_git_repository() -> bool {
wsb::git_backend::is_git_repository(&std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")))
}
fn ensure_git_repository() -> Result<()> {
if is_git_repository() {
return Ok(());
}
let cwd = std::env::current_dir().context("Failed to determine current directory")?;
wsb::git_backend::init_repository(&cwd)?;
println!("Initialized Git repository");
Ok(())
}
fn setup_new_project(first_task: Option<String>, name: Option<String>, ctx: &AppContext) -> Result<()> {
println!("{}", "Setting up new wsb workspace...".bold().green());
ensure_git_repository()?;
let project_root = std::env::current_dir().context("Failed to determine current directory")?;
let state = WorkspaceState::initialize(&project_root, name.as_deref())?;
if let Some(major) = wsb::state::read_major_version_from_file(&project_root) {
ctx.seed_major_version(major)?;
}
let exclude_file = project_root.join(".git/info/exclude");
if let Some(parent) = exclude_file.parent() {
std::fs::create_dir_all(parent)?;
}
let mut content = if exclude_file.exists() {
std::fs::read_to_string(&exclude_file)?
} else {
String::new()
};
if !content.contains(".wsb/") {
if !content.ends_with('\n') && !content.is_empty() {
content.push('\n');
}
content.push_str(".wsb/\n");
std::fs::write(exclude_file, content)?;
}
let hooks_dir = project_root.join(".git/hooks");
if hooks_dir.is_dir() {
let hook_path = hooks_dir.join("pre-commit");
let hook_content = "#!/bin/sh\nwsb update --quiet --git-add\n";
let needs_write = if hook_path.exists() {
let existing = std::fs::read_to_string(&hook_path).unwrap_or_default();
!existing.contains("wsb update --quiet")
} else {
true
};
if needs_write {
std::fs::write(&hook_path, hook_content)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&hook_path)?.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&hook_path, perms)?;
}
println!(" {} installed pre-commit hook: {}", "✓".green(), hook_path.display());
}
}
println!(
"{}",
format!(
"Workspace '{}' setup completed successfully!",
state.project_name.as_deref().unwrap_or("unknown")
)
.bold()
.green()
);
if let Some(task) = first_task {
println!("Ready to start with: {}", task.bold());
}
Ok(())
}
fn read_script_from_stdin() -> Result<String> {
use std::io::Read;
let mut script = String::new();
std::io::stdin()
.read_to_string(&mut script)
.context("Failed to read script from stdin")?;
Ok(script)
}
fn read_script_interactively() -> Result<String> {
use std::io::Read;
eprintln!("Enter shell script (Ctrl-D when done):");
let mut script = String::new();
std::io::stdin()
.read_to_string(&mut script)
.context("Failed to read script interactively")?;
Ok(script)
}
fn run_each_command(project_root: &Path, file: Option<std::path::PathBuf>, quiet: bool) -> Result<()> {
use wsb::fleet::config::gitmodules::GitModules;
use colored::Colorize;
let script: String = match file {
Some(ref path) => {
std::fs::read_to_string(path)
.with_context(|| format!("Failed to read script file: {}", path.display()))?
}
None => {
use std::io::IsTerminal;
if std::io::stdin().is_terminal() {
read_script_interactively()?
} else {
read_script_from_stdin()?
}
}
};
let script = script.trim();
if script.is_empty() {
anyhow::bail!("Script is empty — nothing to run");
}
let modules = match GitModules::parse(project_root) {
Ok(m) => m,
Err(wsb::fleet::error::SuperGitError::NoSubmodules) => {
anyhow::bail!(
"No submodules found in {} — 'wsb each' requires a fleet root with .gitmodules",
project_root.display()
);
}
Err(e) => return Err(anyhow::anyhow!(".gitmodules parse error: {:#}", e)),
};
let total = modules.submodules.len() as u64;
let pb = make_progress_bar(quiet, total);
for sub in &modules.submodules {
let sub_path = project_root.join(&sub.path);
if !sub_path.is_dir() {
pb.finish_and_clear();
anyhow::bail!(
"Submodule '{}': path '{}' does not exist — run 'git submodule update --init'",
sub.name,
sub.path.display()
);
}
pb.set_message(sub.name.clone());
let status = std::process::Command::new("sh")
.arg("-c")
.arg(script)
.current_dir(&sub_path)
.env("WSB_SUBMODULE_PATH", &sub_path)
.env("WSB_SUBMODULE_NAME", &sub.name)
.env("WSB_SUPERPROJECT_ROOT", project_root)
.status()
.with_context(|| format!("Failed to launch sh for submodule '{}'", sub.name))?;
if !status.success() {
pb.finish_and_clear();
let code = status.code().map(|c| c.to_string()).unwrap_or_else(|| "signal".to_string());
anyhow::bail!("Script failed in submodule '{}' (exit {})", sub.name, code);
}
pb.inc(1);
}
pb.finish_and_clear();
if !quiet {
println!("{} ran in {}/{} submodule(s)", "✓".green().bold(), total, total);
}
Ok(())
}
fn run_check_command(cwd: &Path, no_network: bool, quiet: bool) -> Result<()> {
use colored::Colorize;
use wsb::fleet::config::gitmodules::GitModules;
use wsb::workspace_state::WorkspaceState;
#[derive(Clone)]
struct CheckItem {
category: &'static str,
text: String,
}
let mut issues: Vec<CheckItem> = Vec::new();
let mut warnings: Vec<CheckItem> = Vec::new();
let mut ok_items: Vec<String> = Vec::new();
let pb = make_spinner(quiet, "checking workspace…");
macro_rules! ok {
($msg:expr) => {{
ok_items.push(format!(" {} {}", "✓".green(), $msg))
}};
}
macro_rules! issue {
($cat:literal, $msg:expr) => {{
issues.push(CheckItem { category: $cat, text: format!(" {} {}", "✗".red().bold(), $msg) })
}};
($msg:expr) => {{
issues.push(CheckItem { category: "General", text: format!(" {} {}", "✗".red().bold(), $msg) })
}};
}
macro_rules! warn_item {
($cat:literal, $msg:expr) => {{
warnings.push(CheckItem { category: $cat, text: format!(" {} {}", "⚠".yellow(), $msg) })
}};
($msg:expr) => {{
warnings.push(CheckItem { category: "General", text: format!(" {} {}", "⚠".yellow(), $msg) })
}};
}
pb.set_message("resolving workspace roots…");
let project_root = wsb::workspace::find_project_root(cwd);
let super_root = wsb::workspace::find_super_root(cwd);
let project_root = match project_root {
Some(ref p) => {
ok!(format!("project root: {}", p.display()));
p.clone()
}
None => {
issue!("Workspace", format!(
"no project root found walking up from {} — run 'wsb init' to create one",
cwd.display()
));
pb.finish_and_clear();
println!();
for i in &issues {
println!("{}", i.text);
}
std::process::exit(1);
}
};
match &super_root {
Some(sr) if sr == &project_root => {
ok!("super root: same as project root (no submodule nesting)");
}
Some(sr) => {
ok!(format!("super root: {}", sr.display()));
}
None => {
issue!("Workspace", "could not determine super root — not inside any git repository");
}
}
let wsb_dir = project_root.join(".wsb");
if wsb_dir.is_dir() {
ok!(format!(".wsb/ directory exists at {}", wsb_dir.display()));
} else {
issue!("Workspace", format!(
".wsb/ directory missing at {} — run 'wsb init'",
wsb_dir.display()
));
}
let state_json_path = wsb_dir.join("state.json");
if !state_json_path.exists() {
issue!("State", format!(
".wsb/state.json missing at {} — run 'wsb init'",
state_json_path.display()
));
} else {
ok!(format!(".wsb/state.json exists at {}", state_json_path.display()));
match std::fs::read_to_string(&state_json_path) {
Err(e) => issue!("State", format!(".wsb/state.json is unreadable: {}", e)),
Ok(content) => {
match serde_json::from_str::<serde_json::Value>(&content) {
Err(e) => issue!("State", format!(".wsb/state.json is not valid JSON: {}", e)),
Ok(v) => {
if v.get("version").is_none() {
issue!("State", ".wsb/state.json missing required field 'version'");
}
if v.get("project_root").is_none() {
issue!("State", ".wsb/state.json missing required field 'project_root'");
}
match v.get("wstemplate_entries").and_then(|e| e.as_array()) {
Some(entries) if entries.len() > 1 => {
issue!("State", format!(
".wsb/state.json has {} wstemplate_entries — must be at most 1 (each project registers only itself)",
entries.len()
));
}
_ => {}
}
match WorkspaceState::load(&project_root) {
Ok(state) => {
ok!(format!(
"state.json valid — project_name: {}",
state.project_name.as_deref().unwrap_or("<unset>")
));
if let Some(entry) = state.wstemplate_entry() {
ok!(format!(
"wstemplate alias: '{}', scan root: {}",
entry.alias,
entry.root.display()
));
if !entry.root.is_dir() {
issue!("State", format!(
"wstemplate scan root does not exist: {}",
entry.root.display()
));
}
} else {
ok!("no explicit wstemplate entry (alias will be derived from directory name)");
}
}
Err(e) => issue!("State", format!("state.json failed to load: {:#}", e)),
}
}
}
}
}
}
for subdir in &["templates", "logs"] {
let path = wsb_dir.join(subdir);
if path.is_dir() {
ok!(format!(".wsb/{subdir}/ exists"));
} else {
issue!("Workspace", format!(
".wsb/{subdir}/ missing — expected to be created by 'wsb init'"
));
}
}
let version_txt_path = project_root.join("version.txt");
if version_txt_path.exists() {
match std::fs::read_to_string(&version_txt_path) {
Err(e) => issue!("Version", format!("version.txt unreadable: {}", e)),
Ok(content) => {
let trimmed = content.trim();
let first_segment = trimmed.split('.').next().unwrap_or("").trim_start_matches('v');
match first_segment.parse::<u32>() {
Ok(major) => {
ok!(format!(
"version.txt exists — recorded version: {}, major: {}",
trimmed, major
));
}
Err(_) => {
issue!("Version", format!(
"version.txt exists but first segment '{}' is not a valid major version number (content: '{}')",
first_segment, trimmed
));
}
}
}
}
} else {
warn_item!("Version", format!(
"version.txt not found at {} — major version will default to 0 if wsb.db has no project row",
version_txt_path.display()
));
}
let db_root = super_root.as_deref().unwrap_or(&project_root);
let db_path = db_root.join(".wsb").join("wsb.db");
if db_path.exists() {
ok!(format!("wsb.db at super root: {}", db_path.display()));
if db_root != &project_root {
let local_db = project_root.join(".wsb").join("wsb.db");
if local_db.exists() {
issue!("Database", format!(
"wsb.db found at submodule level {} — only one wsb.db should exist at the super root {}",
local_db.display(),
db_path.display()
));
}
}
} else {
issue!("Database", format!(
"wsb.db not found at super root {} — will be created on first 'wsb serve' or database command",
db_path.display()
));
}
let exclude_path = project_root.join(".git").join("info").join("exclude");
if !exclude_path.exists() {
issue!("Git", format!(
".git/info/exclude missing at {} — .wsb/ may be accidentally tracked by git",
exclude_path.display()
));
} else {
match std::fs::read_to_string(&exclude_path) {
Err(e) => issue!("Git", format!(".git/info/exclude unreadable: {}", e)),
Ok(content) => {
if content.contains(".wsb/") {
ok!(".git/info/exclude contains .wsb/ (workspace not tracked by git)");
} else {
issue!("Git",
".git/info/exclude does not contain '.wsb/' — .wsb/ may be accidentally committed; run 'wsb init' to fix"
);
}
}
}
}
pb.set_message("checking pre-commit hook…");
let hook_path = project_root.join(".git").join("hooks").join("pre-commit");
if !hook_path.exists() {
issue!("Git", format!(
"pre-commit hook missing at {} — run 'wsb init' to install it (runs 'wsb update --quiet --git-add' on every commit)",
hook_path.display()
));
} else {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::metadata(&hook_path)?.permissions();
if perms.mode() & 0o111 == 0 {
issue!("Git", format!(
"pre-commit hook at {} is not executable — run: chmod +x {}",
hook_path.display(), hook_path.display()
));
} else {
ok!(format!("pre-commit hook is executable: {}", hook_path.display()));
}
}
match std::fs::read_to_string(&hook_path) {
Err(e) => issue!("Git", format!("pre-commit hook unreadable: {}", e)),
Ok(hook_content) => {
if !hook_content.contains("wsb update") {
issue!("Git", format!(
"pre-commit hook does not invoke 'wsb update' — run 'wsb init' to reinstall (expected: 'wsb update --quiet --git-add')"
));
} else if !hook_content.contains("--quiet") {
issue!("Git", format!(
"pre-commit hook invokes 'wsb update' without --quiet — edit {} to add --quiet",
hook_path.display()
));
} else {
ok!("pre-commit hook invokes 'wsb update --quiet' (correct)");
}
}
}
}
let gitmodules_path = project_root.join(".gitmodules");
let is_fleet_root = gitmodules_path.is_file();
if !is_fleet_root {
ok!("no .gitmodules — single-repo workspace (not a fleet root)");
} else {
ok!(format!(".gitmodules found at {}", gitmodules_path.display()));
match GitModules::parse(&project_root) {
Err(e) => {
issue!("Fleet", format!(".gitmodules parse error: {:#}", e));
}
Ok(modules) => {
ok!(format!("{} submodule(s) declared in .gitmodules", modules.submodules.len()));
let submodule_status_output = std::process::Command::new("git")
.args(["submodule", "status"])
.current_dir(&project_root)
.output()
.ok();
let mut all_aliases: Vec<(String, String)> = Vec::new();
if let Ok(root_state) = WorkspaceState::load(&project_root) {
if let Some(entry) = root_state.wstemplate_entry() {
all_aliases.push((entry.alias.clone(), project_root.display().to_string()));
}
}
for sub in &modules.submodules {
let sub_path = project_root.join(&sub.path);
let display = sub.path.display().to_string();
if no_network {
warn_item!("Fleet", format!(
"submodule '{}': skipping remote URL check (--no-network) — url: {}",
sub.name, sub.url
));
} else {
let ls_remote = std::process::Command::new("git")
.args(["ls-remote", "--exit-code", "--heads", &sub.url])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.output();
match ls_remote {
Ok(out) if out.status.success() => {
ok!(format!(
"submodule '{}': remote url reachable — {}",
sub.name, sub.url
));
}
Ok(out) => {
let stderr = String::from_utf8_lossy(&out.stderr);
issue!("Fleet", format!(
"submodule '{}': remote url unreachable — {} ({})",
sub.name,
sub.url,
stderr.trim()
));
}
Err(e) => {
issue!("Fleet", format!(
"submodule '{}': could not run 'git ls-remote' to check url {}: {}",
sub.name, sub.url, e
));
}
}
}
if !sub_path.is_dir() {
issue!("Fleet", format!(
"submodule '{}': path '{}' does not exist on disk — run 'git submodule update --init'",
sub.name, display
));
continue;
}
ok!(format!("submodule '{}': path exists at {}", sub.name, sub_path.display()));
let sub_git = sub_path.join(".git");
if !sub_git.exists() {
issue!("Fleet", format!(
"submodule '{}': no .git entry — submodule not initialized (run 'git submodule update --init')",
sub.name
));
} else {
ok!(format!("submodule '{}': .git entry present (initialized)", sub.name));
}
if let Some(ref branch) = sub.branch {
if branch == "." {
ok!(format!(
"submodule '{}': branch tracks superproject HEAD (branch = .)",
sub.name
));
} else if no_network {
warn_item!("Fleet", format!(
"submodule '{}': skipping branch '{}' existence check (--no-network)",
sub.name, branch
));
} else {
let branch_check = std::process::Command::new("git")
.args(["ls-remote", "--exit-code", "--heads", &sub.url, branch])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.output();
match branch_check {
Ok(out) if out.status.success() => {
ok!(format!(
"submodule '{}': declared branch '{}' exists on remote",
sub.name, branch
));
}
Ok(_) => {
issue!("Fleet", format!(
"submodule '{}': declared branch '{}' does not exist on remote {}",
sub.name, branch, sub.url
));
}
Err(e) => {
issue!("Fleet", format!(
"submodule '{}': could not check branch '{}' on remote: {}",
sub.name, branch, e
));
}
}
}
} else {
warn_item!("Fleet", format!(
"submodule '{}': no branch declared in .gitmodules — submodule will track a detached commit",
sub.name
));
}
if let Some(ref status_out) = submodule_status_output {
if status_out.status.success() {
let status_text = String::from_utf8_lossy(&status_out.stdout);
let sub_path_str = sub.path.to_string_lossy();
let line = status_text
.lines()
.find(|l| l.trim_start_matches(['-', '+', 'U', ' ']).contains(sub_path_str.as_ref()));
match line {
None => {
issue!("Fleet", format!(
"submodule '{}': not found in 'git submodule status' output — may not be registered in git index",
sub.name
));
}
Some(l) => {
let prefix = l.chars().next().unwrap_or(' ');
match prefix {
'-' => issue!("Fleet", format!(
"submodule '{}': not initialized (git submodule status shows '-') — run 'git submodule update --init'",
sub.name
)),
'+' => issue!("Fleet", format!(
"submodule '{}': checked out commit differs from superproject pointer (git submodule status shows '+') — run 'git submodule update'",
sub.name
)),
'U' => issue!("Fleet", format!(
"submodule '{}': merge conflict in submodule (git submodule status shows 'U')",
sub.name
)),
_ => ok!(format!(
"submodule '{}': commit pointer matches superproject (clean)",
sub.name
)),
}
}
}
} else {
let stderr = String::from_utf8_lossy(&status_out.stderr);
warn_item!("Fleet", format!(
"could not run 'git submodule status': {}",
stderr.trim()
));
}
}
let sub_wsb_dir = sub_path.join(".wsb");
if !sub_wsb_dir.is_dir() {
issue!("Fleet", format!(
"submodule '{}': .wsb/ missing at {} — run 'wsb init' inside that submodule",
sub.name, sub_wsb_dir.display()
));
} else {
ok!(format!("submodule '{}': .wsb/ exists", sub.name));
let sub_db = sub_wsb_dir.join("wsb.db");
if sub_db.exists() {
issue!("Database", format!(
"submodule '{}': wsb.db found at {} — only one wsb.db should exist at the super root",
sub.name, sub_db.display()
));
}
}
let sub_state_path = sub_wsb_dir.join("state.json");
if !sub_state_path.exists() {
issue!("State", format!(
"submodule '{}': .wsb/state.json missing — run 'wsb init' inside {}",
sub.name, sub_path.display()
));
} else {
ok!(format!("submodule '{}': .wsb/state.json exists", sub.name));
match WorkspaceState::load(&sub_path) {
Err(e) => issue!("State", format!(
"submodule '{}': state.json failed to load: {:#}",
sub.name, e
)),
Ok(state) => {
ok!(format!(
"submodule '{}': state.json valid — project_name: {}",
sub.name,
state.project_name.as_deref().unwrap_or("<unset>")
));
if let Some(entry) = state.wstemplate_entry() {
all_aliases.push((
entry.alias.clone(),
sub_path.display().to_string(),
));
}
}
}
}
let sub_version_txt = sub_path.join("version.txt");
if sub_version_txt.exists() {
match std::fs::read_to_string(&sub_version_txt) {
Err(e) => issue!("Version", format!(
"submodule '{}': version.txt unreadable: {}", sub.name, e
)),
Ok(content) => {
let trimmed = content.trim();
let first_seg = trimmed.split('.').next().unwrap_or("").trim_start_matches('v');
match first_seg.parse::<u32>() {
Ok(major) => ok!(format!(
"submodule '{}': version.txt — recorded version: {}, major: {}",
sub.name, trimmed, major
)),
Err(_) => issue!("Version", format!(
"submodule '{}': version.txt first segment '{}' is not a valid major version (content: '{}')",
sub.name, first_seg, trimmed
)),
}
}
}
} else {
warn_item!("Version", format!(
"submodule '{}': no version.txt — major version defaults to 0 if not already in wsb.db",
sub.name
));
}
let sub_exclude = sub_path.join(".git").join("info").join("exclude");
if sub_exclude.exists() {
match std::fs::read_to_string(&sub_exclude) {
Ok(c) if c.contains(".wsb/") => {
ok!(format!("submodule '{}': .git/info/exclude has .wsb/", sub.name));
}
Ok(_) => {
issue!("Git", format!(
"submodule '{}': .git/info/exclude does not contain '.wsb/' — .wsb/ may be tracked",
sub.name
));
}
Err(e) => {
issue!("Git", format!(
"submodule '{}': .git/info/exclude unreadable: {}",
sub.name, e
));
}
}
} else {
issue!("Git", format!(
"submodule '{}': .git/info/exclude missing — .wsb/ may be accidentally tracked",
sub.name
));
}
let sub_hook = sub_path.join(".git").join("hooks").join("pre-commit");
if !sub_hook.exists() {
issue!("Git", format!(
"submodule '{}': pre-commit hook missing — run 'wsb init' inside {} to install it",
sub.name, sub_path.display()
));
} else {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(meta) = std::fs::metadata(&sub_hook) {
if meta.permissions().mode() & 0o111 == 0 {
issue!("Git", format!(
"submodule '{}': pre-commit hook is not executable",
sub.name
));
}
}
}
match std::fs::read_to_string(&sub_hook) {
Err(e) => issue!("Git", format!(
"submodule '{}': pre-commit hook unreadable: {}", sub.name, e
)),
Ok(content) => {
if !content.contains("wsb update") {
issue!("Git", format!(
"submodule '{}': pre-commit hook does not invoke 'wsb update' — run 'wsb init' to reinstall",
sub.name
));
} else if !content.contains("--quiet") {
issue!("Git", format!(
"submodule '{}': pre-commit hook invokes 'wsb update' without --quiet",
sub.name
));
} else {
ok!(format!("submodule '{}': pre-commit hook correct", sub.name));
}
}
}
}
}
let mut seen_aliases: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
for (alias, source) in &all_aliases {
seen_aliases.entry(alias.clone()).or_default().push(source.clone());
}
for (alias, sources) in &seen_aliases {
if sources.len() > 1 {
issue!("Fleet", format!(
"duplicate wstemplate alias '{}' in {} projects: {}",
alias,
sources.len(),
sources.join(", ")
));
} else {
ok!(format!("wstemplate alias '{}' is unique", alias));
}
}
}
}
}
pb.finish_and_clear();
if !quiet {
println!();
for item in &ok_items {
println!("{}", item);
}
}
if !warnings.is_empty() {
let mut by_cat: std::collections::BTreeMap<&str, Vec<&str>> = std::collections::BTreeMap::new();
for w in &warnings {
by_cat.entry(w.category).or_default().push(&w.text);
}
println!();
println!("{} {} warning(s):", "⚠".yellow(), warnings.len());
for (cat, items) in &by_cat {
println!(" [{}]", cat.yellow());
for item in items {
println!("{}", item);
}
}
}
if !issues.is_empty() {
let mut by_cat: std::collections::BTreeMap<&str, Vec<&str>> = std::collections::BTreeMap::new();
for iss in &issues {
by_cat.entry(iss.category).or_default().push(&iss.text);
}
println!();
println!("{} {} issue(s) found:", "✗".red().bold(), issues.len());
for (cat, items) in &by_cat {
println!(" [{}]", cat.red().bold());
for item in items {
println!("{}", item);
}
}
println!();
std::process::exit(1);
} else {
println!();
println!("{}", "All checks passed.".green().bold());
}
Ok(())
}
fn add_files_to_git(files: &[String]) -> Result<Vec<String>> {
let cwd = std::env::current_dir().context("Failed to determine current directory")?;
let repo = wsb::git_backend::discover_repo(&cwd)?;
let repo_root = repo
.workdir()
.context("Bare repositories are not supported for staging")?;
let mut files_to_stage = Vec::new();
for file in files {
let file_path = std::path::Path::new(file);
let absolute = if file_path.is_absolute() {
file_path.to_path_buf()
} else {
cwd.join(file_path)
};
if !absolute.exists() {
warn!("File '{}' does not exist, skipping git add", file);
eprintln!(
"{} File '{}' does not exist, skipping git add",
"Warning".yellow(),
file
);
continue;
}
let relative_path = absolute
.strip_prefix(repo_root)
.with_context(|| format!("File '{}' is outside repository root", file))?
.to_string_lossy()
.to_string();
files_to_stage.push(relative_path);
}
let added_files = wsb::git_backend::stage_paths(&repo, &files_to_stage)?;
for file in &added_files {
log_action(&format!("Added file to git: {}", file));
}
Ok(added_files)
}
fn is_running_as_git_hook() -> bool {
if env::var("GIT_DIR").is_ok() {
return true;
}
if env::var("GIT_INDEX_FILE").is_ok() {
return true;
}
false
}
fn setup_shell_completions() -> Result<()> {
let shell = detect_shell()?;
generate_completion_file(shell)?;
show_completion_hint_if_needed(shell);
Ok(())
}
fn show_completion_hint_if_needed(shell: Shell) {
if env::var_os("WS_COMPLETIONS_LOADED").is_some() {
return;
}
let completion_dir = match get_completion_dir(shell) {
Ok(d) => d,
Err(_) => return,
};
let completion_file = match get_completion_file_path(shell, &completion_dir) {
Ok(f) => f,
Err(_) => return,
};
let hint_marker = completion_dir.join(".wsb-completion-hint-shown");
if hint_marker.exists() {
return;
}
print_activation_hint(shell, &completion_file);
let _ = fs::write(hint_marker, b"shown\n");
}
fn detect_shell() -> Result<Shell> {
if let Ok(shell) = env::var("SHELL") {
if shell.contains("bash") {
return Ok(Shell::Bash);
} else if shell.contains("zsh") {
return Ok(Shell::Zsh);
} else if shell.contains("fish") {
return Ok(Shell::Fish);
}
}
if env::var("ZSH_VERSION").is_ok() {
return Ok(Shell::Zsh);
} else if env::var("BASH_VERSION").is_ok() {
return Ok(Shell::Bash);
} else if env::var("FISH_VERSION").is_ok() {
return Ok(Shell::Fish);
}
Ok(Shell::Bash)
}
fn generate_completion_file(shell: Shell) -> Result<()> {
let mut completion_script = Vec::new();
{
let mut app = Args::command();
let name = app.get_name().to_string();
generate(shell, &mut app, name, &mut completion_script);
}
let completion_content = String::from_utf8(completion_script)
.context("Failed to convert completion script to string")?;
let completion_dir = get_completion_dir(shell)?;
fs::create_dir_all(&completion_dir).context("Failed to create completion directory")?;
let completion_file = get_completion_file_path(shell, &completion_dir)?;
fs::write(&completion_file, &completion_content)
.context("Failed to write completion script")?;
Ok(())
}
fn get_completion_dir(shell: Shell) -> Result<PathBuf> {
let data_home = env::var("XDG_DATA_HOME").unwrap_or_else(|_| {
let home = env::var("HOME").unwrap_or_default();
format!("{}/.local/share", home)
});
let dir = match shell {
Shell::Zsh => format!("{}/zsh/site-functions", data_home),
Shell::Bash => format!("{}/bash-completion/completions", data_home),
Shell::Fish => {
format!("{}/fish/completions", data_home)
}
Shell::PowerShell => format!("{}/powershell/completions", data_home),
_ => format!("{}/completions", data_home),
};
Ok(PathBuf::from(dir))
}
fn get_completion_file_path(shell: Shell, completion_dir: &std::path::Path) -> Result<PathBuf> {
let file_name = match shell {
Shell::Zsh => "_ws",
Shell::Bash => "wsb",
Shell::Fish => "wsb.fish",
Shell::PowerShell => "wsb.ps1",
_ => "wsb",
};
Ok(completion_dir.join(file_name))
}
fn print_activation_hint(shell: Shell, file_path: &std::path::Path) {
match shell {
Shell::Bash => {
eprintln!("# To enable wsb completions, add this to your ~/.bashrc:");
eprintln!("source '{}'", file_path.to_string_lossy());
}
Shell::Zsh => {
let completion_parent = file_path.parent().unwrap_or(std::path::Path::new(""));
eprintln!("# To enable wsb completions, add these lines to your ~/.zshrc:");
eprintln!("fpath=(\"{}\" $fpath)", completion_parent.to_string_lossy());
eprintln!("autoload -U compinit && compinit");
}
Shell::Fish => {
eprintln!(
"# wsb completions installed to: {}",
file_path.to_string_lossy()
);
}
Shell::PowerShell => {
eprintln!("# To enable wsb completions, add this to your PowerShell profile:");
eprintln!(". '{}'", file_path.to_string_lossy());
}
_ => {
eprintln!("# Completion script at: {}", file_path.to_string_lossy());
}
}
}
fn run_serve_command(port: u16, debug: bool, parent_pid: Option<u32>, ctx: AppContext) -> Result<()> {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?
.block_on(async { wsb::web::start_server(port, debug, parent_pid, ctx).await })
}
fn run_sample_command(project: bool, data: bool, force: bool, output: String, ctx: &AppContext) -> Result<()> {
println!("{}", "=== Sample Project & Data Creation ===".bold().blue());
if !project && !data {
println!(
"{} Specify --project to create sample project structure",
"📁".blue()
);
println!(
"{} Specify --data to populate database with test data",
"🗄️".blue()
);
println!(
"{} Use both flags to create complete sample environment",
"🚀".green()
);
return Ok(());
}
let output_path = std::path::Path::new(&output);
if output_path.exists() && !force {
println!(
"{} Output directory '{}' already exists (use --force to overwrite)",
"⚠️".yellow(),
output
);
return Ok(());
}
if project {
create_sample_project_in_dir(&output, force)?;
}
if data {
populate_sample_data_in_dir(&output, force)?;
}
if project && data {
info!(
"Sample project and data creation completed in: {}",
output_path.display()
);
println!(
"{} Sample project and data creation completed!",
"✅".green().bold()
);
println!(
"{} Sample project created in: {}",
"📁".blue(),
output_path
.canonicalize()
.unwrap_or_else(|_| output_path.to_path_buf())
.display()
);
println!("{} To start the dashboard:", "💡".blue());
println!(" cd {} && wsb serve --port 3000", output);
println!(
"{} Then access dashboard at http://localhost:3000",
"🌐".cyan()
);
}
Ok(())
}
fn create_sample_project(force: bool) -> Result<()> {
println!(
"{} Creating sample project structure...",
"📁".blue().bold()
);
if std::path::Path::new("CLAUDE.md").exists() && !force {
println!(
"{} CLAUDE.md already exists (use --force to overwrite)",
"⚠️".yellow()
);
return Ok(());
}
std::fs::create_dir_all("internal")?;
std::fs::create_dir_all(".wsb")?;
std::fs::create_dir_all("src")?;
std::fs::create_dir_all("tests")?;
std::fs::create_dir_all("docs")?;
let claude_content = r#"# Sample Project
## Project Overview
**Project Name**: Sample Dashboard Project
**Type**: Web dashboard with API backend
**Current Version**: 1.0.0
## Project Description
This is a sample project created to demonstrate the Workspace development suite capabilities including:
- Feature-centric development methodology
- Real-time project dashboard
- Comprehensive API endpoints
- Database-driven project management
## Current Status
**Development Phase**: Sample Data Demonstration
**Test Status**: ✅ Sample data populated
**Build Status**: ✅ Ready for development
## Key Features Working
- ✅ Project management dashboard
- ✅ Feature tracking and status monitoring
- ✅ Task management with state transitions
- ✅ Real-time API endpoints
- ✅ Database-backed storage
## Success Criteria
### Core Functionality
- ✅ Dashboard displays project metrics
- ✅ API endpoints return sample data
- ✅ Feature state management working
- ✅ Task tracking operational
### Quality Metrics
- ✅ All API endpoints responding
- ✅ Database queries optimized
- ✅ Sample data representative of real usage
## Next Steps
Use this sample project to:
1. Test dashboard functionality
2. Validate API endpoints
3. Experiment with feature management
4. Learn the development methodology
---
*Created by wsb sample command*"#;
std::fs::write("CLAUDE.md", claude_content)?;
println!(" {} Created CLAUDE.md", "✅".green());
let package_json = r#"{
"name": "sample-dashboard-project",
"version": "1.0.0",
"description": "Sample project for Workspace development suite",
"main": "index.js",
"scripts": {
"dev": "wsb serve",
"test": "wsb status --include-features --include-metrics"
},
"keywords": ["workspace", "dashboard", "sample"],
"author": "Workspace Development Suite",
"license": "MIT"
}"#;
std::fs::write("package.json", package_json)?;
println!(" {} Created package.json", "✅".green());
let readme_content = r#"# Sample Dashboard Project
This is a sample project created by the Workspace development suite to demonstrate:
- Feature-centric development methodology
- Real-time project dashboard
- API-driven development workflow
## Quick Start
1. View project status: `wsb status --include-features`
2. Start dashboard: `wsb serve`
3. Open browser: http://localhost:3000
## Commands
- `wsb sample --data` - Populate with more sample data
- `wsb feature list` - View all features
- `wsb task list` - View all tasks
- `wsb status --include-metrics` - View project metrics
This sample demonstrates real-world usage patterns and can be used as a template for new projects.
"#;
std::fs::write("README.md", readme_content)?;
println!(" {} Created README.md", "✅".green());
println!("{} Sample project structure created", "✅".green().bold());
Ok(())
}
fn populate_sample_data(force: bool, ctx: &AppContext) -> Result<()> {
println!(
"{} Populating database with sample data...",
"🗄️".blue().bold()
);
let db_path = std::path::Path::new(".wsb/wsb.db");
std::fs::create_dir_all(db_path.parent().unwrap())?;
if db_path.exists() && !force {
let output = std::process::Command::new("sqlite3")
.arg(&db_path)
.arg("SELECT COUNT(*) FROM features;")
.output()?;
if output.status.success() {
let count = String::from_utf8_lossy(&output.stdout)
.trim()
.parse::<i32>()
.unwrap_or(0);
if count > 0 {
println!(
"{} Database already has {} features (use --force to overwrite)",
"⚠️".yellow(),
count
);
return Ok(());
}
}
}
tokio::runtime::Runtime::new()?.block_on(async { populate_sample_data_async(force, ctx).await })
}
async fn populate_sample_data_async(force: bool, ctx: &AppContext) -> Result<()> {
let db = &ctx.db;
let test_data_path = std::path::Path::new("test_data.sql");
if test_data_path.exists() {
println!(
" {} Loading comprehensive data from test_data.sql...",
"📥".blue()
);
let test_data = std::fs::read_to_string(test_data_path)?;
let db_path = ctx.workspace_root.join(".wsb/wsb.db");
let db_path_str = db_path.to_string_lossy();
let output = std::process::Command::new("sqlite3")
.arg(&*db_path_str)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()?;
if let Some(stdin) = output.stdin.as_ref() {
use std::io::Write;
let mut stdin = stdin;
stdin.write_all(test_data.as_bytes())?;
}
let result = output.wait_with_output()?;
if !result.status.success() {
eprintln!("Warning: Some SQL statements may have failed");
}
println!(" {} Comprehensive test data loaded", "✅".green());
} else {
println!(" {} Creating minimal sample data...", "🔧".blue());
let _project = db.project_repo.list(None, None).await?.into_iter().next();
println!(" {} Creating features with diverse states...", "🎯".blue());
let feature_states = vec![
"NotImplemented",
"Planned",
"InProgress",
"Implemented",
"Tested",
"Deprecated",
];
let test_statuses = vec!["NotTested", "InProgress", "Passed", "Failed", "Skipped"];
let categories = vec![
"Frontend",
"Backend",
"Database",
"Security",
"Performance",
"Testing",
"Documentation",
"DevOps",
"Analytics",
"Mobile",
"AI/ML",
"Infrastructure",
"UX/UI",
"Integration",
"Monitoring",
];
let priorities = vec!["Low", "Medium", "High", "Critical"];
let feature_templates = vec![
(
"Dashboard",
"web dashboard for project visualization and monitoring",
),
(
"API Gateway",
"centralized API gateway for microservices communication",
),
(
"Authentication",
"secure user authentication and authorization system",
),
(
"Database Migration",
"automated database schema migration and versioning",
),
(
"Real-time Sync",
"real-time data synchronization across multiple clients",
),
(
"Cache Layer",
"distributed caching layer for improved performance",
),
(
"Monitoring",
"comprehensive application monitoring and alerting",
),
(
"Search Engine",
"full-text search capabilities with advanced filtering",
),
("File Upload", "secure file upload and processing system"),
(
"Notification Hub",
"multi-channel notification delivery system",
),
(
"User Management",
"complete user lifecycle management portal",
),
(
"Audit Trail",
"comprehensive audit logging and compliance tracking",
),
(
"Report Generator",
"automated report generation and distribution",
),
(
"Backup System",
"automated backup and disaster recovery solution",
),
(
"Load Balancer",
"intelligent load balancing and traffic distribution",
),
(
"Security Scanner",
"automated security vulnerability scanning",
),
(
"Performance Analytics",
"detailed performance metrics and optimization insights",
),
(
"Content Management",
"flexible content management and publishing system",
),
(
"Email Service",
"reliable email delivery and template management",
),
(
"Chat Integration",
"real-time chat and collaboration features",
),
(
"Payment Gateway",
"secure payment processing and transaction management",
),
(
"Inventory System",
"comprehensive inventory tracking and management",
),
(
"Workflow Engine",
"flexible business process automation engine",
),
("Data Pipeline", "scalable data processing and ETL pipeline"),
(
"Mobile App",
"native mobile application with offline capabilities",
),
(
"AI Recommendations",
"machine learning powered recommendation engine",
),
(
"Social Features",
"social networking and community engagement tools",
),
(
"Third-party Integration",
"seamless integration with external services",
),
(
"Analytics Dashboard",
"advanced analytics and business intelligence dashboard",
),
(
"Video Processing",
"automated video transcoding and streaming service",
),
(
"Geolocation Service",
"location-based services and mapping integration",
),
(
"Blockchain Integration",
"decentralized ledger and smart contract support",
),
(
"IoT Gateway",
"Internet of Things device management and data collection",
),
(
"Machine Learning",
"automated machine learning model training and deployment",
),
(
"GraphQL API",
"flexible GraphQL API with real-time subscriptions",
),
("Microservices", "containerized microservices architecture"),
(
"Edge Computing",
"distributed edge computing and CDN integration",
),
(
"Data Warehouse",
"scalable data warehouse and business intelligence platform",
),
(
"Compliance Engine",
"automated regulatory compliance and reporting system",
),
(
"Resource Scheduler",
"intelligent resource allocation and scheduling system",
),
];
let mut diverse_features = Vec::new();
for (i, (name, desc_suffix)) in feature_templates.iter().enumerate() {
let state = feature_states[i % feature_states.len()];
let test_status = test_statuses[i % test_statuses.len()];
let category = categories[i % categories.len()];
let priority = priorities[i % priorities.len()];
diverse_features.push((
format!(
"{} {}",
name,
if i > 0 {
format!("v{}", (i / feature_templates.len()) + 1)
} else {
String::new()
}
)
.trim()
.to_string(),
format!(
"Implement {} with {} priority focus",
desc_suffix,
priority.to_lowercase()
),
state,
test_status,
category,
priority,
));
}
for (title, desc, _state, _test_status, _category, _priority) in diverse_features {
let project = db.project_repo.list(None, None).await?.into_iter().next()
.ok_or_else(|| anyhow::anyhow!("No active project"))?;
let feature_code = format!("F{:05}", db.feature_repo.count().await? + 1);
let feature = wsb::persist::local::models::schema_models::Feature::new(
project.id.clone(),
feature_code,
title.clone(),
desc.clone(),
None,
).map_err(|e| anyhow::anyhow!("{}", e))?;
if let Err(e) = db.feature_repo.create(&feature).await {
eprintln!("Warning: Failed to create feature '{}': {}", title, e);
}
}
println!(" {} Creating tasks with diverse statuses...", "✅".blue());
let task_statuses = vec!["Pending", "InProgress", "Completed", "Blocked", "Cancelled"];
let task_categories = vec![
"feature",
"bug",
"maintenance",
"research",
"documentation",
"testing",
"deployment",
"security",
"performance",
"refactoring",
"integration",
"monitoring",
"infrastructure",
"training",
"compliance",
];
let task_templates = vec![
(
"Setup",
"initial project structure and configuration",
"infrastructure",
),
(
"Implement",
"core functionality and business logic",
"feature",
),
(
"Design",
"user interface and experience patterns",
"feature",
),
(
"Configure",
"development and production environments",
"infrastructure",
),
(
"Optimize",
"performance bottlenecks and resource usage",
"performance",
),
(
"Test",
"comprehensive test coverage and quality assurance",
"testing",
),
(
"Document",
"technical specifications and user guides",
"documentation",
),
(
"Deploy",
"automated deployment and release processes",
"deployment",
),
(
"Monitor",
"application health and performance metrics",
"monitoring",
),
(
"Secure",
"vulnerability assessment and security hardening",
"security",
),
(
"Integrate",
"third-party services and external APIs",
"integration",
),
(
"Refactor",
"code quality and architectural improvements",
"refactoring",
),
(
"Research",
"technology evaluation and proof of concepts",
"research",
),
(
"Train",
"team knowledge transfer and skill development",
"training",
),
(
"Maintain",
"system maintenance and technical debt reduction",
"maintenance",
),
("Fix", "critical bugs and production issues", "bug"),
(
"Validate",
"compliance requirements and regulatory standards",
"compliance",
),
(
"Scale",
"horizontal and vertical scaling capabilities",
"performance",
),
(
"Backup",
"data protection and disaster recovery procedures",
"infrastructure",
),
(
"Upgrade",
"dependency updates and version migrations",
"maintenance",
),
(
"Analyze",
"user behavior and system performance patterns",
"research",
),
(
"Automate",
"manual processes and workflow optimization",
"infrastructure",
),
(
"Review",
"code quality and architectural decisions",
"maintenance",
),
(
"Plan",
"feature roadmap and technical architecture",
"research",
),
(
"Coordinate",
"cross-team collaboration and resource allocation",
"feature",
),
(
"Evaluate",
"technology stack and tool effectiveness",
"research",
),
(
"Streamline",
"development workflow and productivity tools",
"infrastructure",
),
(
"Enhance",
"user experience and interface improvements",
"feature",
),
(
"Troubleshoot",
"production issues and system debugging",
"bug",
),
(
"Standardize",
"coding conventions and development practices",
"maintenance",
),
(
"Containerize",
"application deployment and orchestration",
"deployment",
),
(
"Profile",
"performance analysis and optimization opportunities",
"performance",
),
(
"Validate",
"data integrity and business rule enforcement",
"testing",
),
(
"Migrate",
"legacy system modernization and data transfer",
"infrastructure",
),
(
"Prototype",
"experimental features and innovation projects",
"research",
),
(
"Benchmark",
"performance comparison and competitive analysis",
"performance",
),
(
"Audit",
"security assessment and compliance verification",
"security",
),
(
"Customize",
"client-specific requirements and configurations",
"feature",
),
(
"Synchronize",
"data consistency across distributed systems",
"integration",
),
(
"Visualize",
"data presentation and dashboard development",
"feature",
),
];
let mut diverse_tasks = Vec::new();
for (i, (action, desc_suffix, default_category)) in task_templates.iter().enumerate() {
let status = task_statuses[i % task_statuses.len()];
let priority = priorities[i % priorities.len()];
let category = if i < task_categories.len() {
task_categories[i]
} else {
default_category
};
diverse_tasks.push((
format!("{} {} - Phase {}", action, desc_suffix, (i / 10) + 1),
format!(
"{} {} with {} priority and {} status tracking",
action,
desc_suffix,
priority.to_lowercase(),
status.to_lowercase()
),
status,
priority,
category,
));
}
for (title, desc, _status, _priority, _category) in diverse_tasks {
let project = db.project_repo.list(None, None).await?.into_iter().next()
.ok_or_else(|| anyhow::anyhow!("No active project"))?;
let features: Vec<_> = db.feature_repo.list(None, None).await?.into_iter()
.filter(|f| f.project_id == project.id).collect();
if let Some(feature) = features.first() {
let task_code = format!("T{:06}", db.task_repo.count().await? + 1);
let task = wsb::persist::local::models::schema_models::Task::new(
project.id.clone(),
task_code,
feature.id.clone(),
title.clone(),
"feature".to_string(),
).map_err(|e| anyhow::anyhow!("{}", e))?;
if let Err(e) = db.task_repo.create(&task).await {
eprintln!("Warning: Failed to create task '{}': {}", title, e);
}
}
}
println!(
" {} Comprehensive sample data created with {} diverse categories",
"✅".green(),
categories.len()
);
}
let project = db.project_repo.list(None, None).await?.into_iter().next()
.ok_or_else(|| anyhow::anyhow!("No active project"))?;
let features = db.feature_repo.list(None, None).await?.into_iter()
.filter(|f| f.project_id == project.id).collect::<Vec<_>>();
let tasks = db.task_repo.list(None, None).await?.into_iter()
.filter(|t| t.project_id == project.id).collect::<Vec<_>>();
println!(" {} {} features created", "📋".cyan(), features.len());
println!(" {} {} tasks created", "✅".cyan(), tasks.len());
Ok(())
}
fn run_relationship_command(action: RelationshipAction, ctx: &AppContext) -> Result<()> {
match action {
RelationshipAction::Link {
from_entity,
from_type,
to_entity,
to_type,
relationship_type,
description,
} => {
link_entities(
from_entity,
from_type,
to_entity,
to_type,
relationship_type,
description,
ctx,
)?;
}
RelationshipAction::List {
entity_id,
entity_type,
relationship_type,
include_resolved,
} => {
list_entity_relationships(entity_id, entity_type, relationship_type, include_resolved, ctx)?;
}
RelationshipAction::Unlink {
dependency_id,
force,
} => {
unlink_entities(dependency_id, force, ctx)?;
}
RelationshipAction::Resolve {
dependency_id,
description,
} => {
resolve_entity_relationship(dependency_id, description, ctx)?;
}
RelationshipAction::Stats { detailed, format } => {
show_relationship_stats(detailed, format, ctx)?;
}
}
Ok(())
}
fn create_sample_project_in_dir(output_dir: &str, force: bool) -> Result<()> {
println!(
"{} Creating sample project structure in {}...",
"📁".blue().bold(),
output_dir
);
let output_path = std::path::Path::new(output_dir);
if output_path.exists() && force {
std::fs::remove_dir_all(output_path)?;
println!(" {} Removed existing directory", "🗑️".yellow());
}
std::fs::create_dir_all(output_path.join("internal"))?;
std::fs::create_dir_all(output_path.join(".wsb"))?;
std::fs::create_dir_all(output_path.join("src"))?;
std::fs::create_dir_all(output_path.join("tests"))?;
std::fs::create_dir_all(output_path.join("docs"))?;
let claude_content = r#"# Sample Project
## Project Overview
**Project Name**: Sample Dashboard Project
**Type**: Web dashboard with API backend
**Current Version**: 1.0.0
## Project Description
This is a sample project created to demonstrate the Workspace development suite capabilities including:
- Feature-centric development methodology
- Real-time project dashboard
- Comprehensive API endpoints
- Database-driven project management
## Current Status
**Development Phase**: Sample Data Demonstration
**Test Status**: ✅ Sample data populated
**Build Status**: ✅ Ready for development
## Key Features Working
- ✅ Project management dashboard
- ✅ Feature tracking and status monitoring
- ✅ Task management with state transitions
- ✅ Real-time API endpoints
- ✅ Database-backed storage
## Success Criteria
### Core Functionality
- ✅ Dashboard displays project metrics
- ✅ API endpoints return sample data
- ✅ Feature state management working
- ✅ Task tracking operational
### Quality Metrics
- ✅ All API endpoints responding
- ✅ Database queries optimized
- ✅ Sample data representative of real usage
## Next Steps
Use this sample project to:
1. Test dashboard functionality
2. Validate API endpoints
3. Experiment with feature management
4. Learn the development methodology
---
*Created by wsb sample command*"#;
std::fs::write(output_path.join("CLAUDE.md"), claude_content)?;
println!(" {} Created CLAUDE.md", "✅".green());
let package_json = r#"{
"name": "sample-dashboard-project",
"version": "1.0.0",
"description": "Sample project for Workspace development suite",
"main": "index.js",
"scripts": {
"dev": "wsb serve",
"test": "wsb status --include-features --include-metrics"
},
"keywords": ["workspace", "dashboard", "sample"],
"author": "Workspace Development Suite",
"license": "MIT"
}"#;
std::fs::write(output_path.join("package.json"), package_json)?;
println!(" {} Created package.json", "✅".green());
let readme_content = r#"# Sample Dashboard Project
A comprehensive sample project demonstrating the Workspace development methodology with real project data.
## Features
This sample includes:
- **10 sample features** across different categories (Frontend, Backend, Database, Security, etc.)
- **10 sample tasks** with various statuses and priorities
- **4 development sessions** showing project evolution
- **5 notes** including architecture decisions and issues
- **5 dependencies** between features and tasks
- **4 projects** in different states
## Getting Started
1. **Start the dashboard server:**
```bash
wsb serve --port 3000
```
2. **Access the web dashboard:**
Open http://localhost:3000 in your browser
3. **Explore the data:**
- View project metrics and status
- Browse features by category and state
- Check task progress and dependencies
- Review development sessions and notes
## Sample Data Overview
The sample data covers all possible states and scenarios:
### Features (10 total)
- **States**: implemented, in_progress, planned, tested, not_implemented, deprecated
- **Categories**: Frontend, Backend, Database, Security, Performance, Testing, Documentation, DevOps, Analytics, Mobile
- **Priorities**: critical, high, medium, low
### Tasks (10 total)
- **Statuses**: completed, in_progress, pending, blocked, cancelled
- **Categories**: feature, infrastructure, testing, security, performance, etc.
### Projects (4 total)
- E-Commerce Platform (active)
- AI Analytics Engine (active)
- Legacy CRM System (archived)
- Modern CRM Platform (in development)
## Learning the Methodology
This sample demonstrates:
- Feature-driven development approach
- Comprehensive task tracking
- Project state management
- Development session documentation
- Dependency relationship modeling
- Multi-project organization
---
*Generated by Workspace Sample Generator*"#;
std::fs::write(output_path.join("README.md"), readme_content)?;
println!(" {} Created README.md", "✅".green());
println!(" {} Initializing git repository...", "🔧".yellow());
init_sample_git_repo(output_path)?;
println!(
"{} Sample project structure created in {}",
"✅".green().bold(),
output_dir
);
Ok(())
}
fn init_sample_git_repo(project_path: &std::path::Path) -> Result<()> {
let repo = wsb::git_backend::init_repository(project_path)?;
wsb::git_backend::configure_local_user(
&repo,
"Sample Developer",
"developer@sample-project.com",
)?;
create_sample_source_files(project_path)?;
let commit_msg = "Initial project setup
- Added basic project structure with package.json
- Created src/, docs/, tests/ directories
- Added project documentation and README
- Initialized workspace with .wsb/ directory";
wsb::git_backend::commit_all(&repo, commit_msg)?
.context("Failed to create initial sample project commit")?;
create_development_commits(project_path)?;
println!(
" {} Git repository initialized with sample commits",
"✅".green()
);
Ok(())
}
fn create_sample_source_files(project_path: &std::path::Path) -> Result<()> {
let app_js = r#"// Main application entry point
class DashboardApp {
constructor() {
this.apiBase = '/api';
this.currentUser = null;
this.init();
}
async init() {
await this.loadUserProfile();
this.setupEventListeners();
this.renderDashboard();
}
async loadUserProfile() {
try {
const response = await fetch(`${this.apiBase}/user/profile`);
this.currentUser = await response.json();
} catch (error) {
console.error('Failed to load user profile:', error);
}
}
setupEventListeners() {
document.getElementById('refresh-btn')?.addEventListener('click', () => {
this.refreshData();
});
}
renderDashboard() {
const container = document.getElementById('dashboard');
if (container) {
container.innerHTML = `
<h1>Welcome, ${this.currentUser?.name || 'User'}</h1>
<div class="metrics">
<div class="metric-card">
<h3>Active Projects</h3>
<span class="metric-value">12</span>
</div>
<div class="metric-card">
<h3>Tasks Completed</h3>
<span class="metric-value">84</span>
</div>
</div>
`;
}
}
async refreshData() {
console.log('Refreshing dashboard data...');
await this.loadUserProfile();
this.renderDashboard();
}
}
// Initialize app when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
new DashboardApp();
});
"#;
std::fs::write(project_path.join("src/app.js"), app_js)?;
let styles_css = r#"/* Dashboard Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f5f5f5;
color: #333;
}
#dashboard {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
h1 {
color: #2c3e50;
margin-bottom: 30px;
font-weight: 300;
}
.metrics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.metric-card {
background: white;
padding: 24px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center;
}
.metric-card h3 {
color: #666;
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
}
.metric-value {
font-size: 32px;
font-weight: 700;
color: #3498db;
}
#refresh-btn {
background: #3498db;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
}
#refresh-btn:hover {
background: #2980b9;
}
"#;
std::fs::write(project_path.join("src/styles.css"), styles_css)?;
let index_html = r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sample Dashboard</title>
<link rel="stylesheet" href="src/styles.css">
</head>
<body>
<div id="dashboard">
<div class="loading">Loading dashboard...</div>
</div>
<button id="refresh-btn">Refresh Data</button>
<script src="src/app.js"></script>
</body>
</html>
"#;
std::fs::write(project_path.join("index.html"), index_html)?;
let test_js = r#"// Dashboard App Tests
describe('DashboardApp', () => {
let app;
beforeEach(() => {
document.body.innerHTML = '<div id="dashboard"></div>';
app = new DashboardApp();
});
test('should initialize with correct API base', () => {
expect(app.apiBase).toBe('/api');
});
test('should render welcome message', () => {
app.currentUser = { name: 'Test User' };
app.renderDashboard();
const dashboard = document.getElementById('dashboard');
expect(dashboard.innerHTML).toContain('Welcome, Test User');
});
test('should handle missing user gracefully', () => {
app.currentUser = null;
app.renderDashboard();
const dashboard = document.getElementById('dashboard');
expect(dashboard.innerHTML).toContain('Welcome, User');
});
});
"#;
std::fs::write(project_path.join("tests/app.test.js"), test_js)?;
let api_docs = r#"# API Documentation
## Overview
This document describes the REST API endpoints for the sample dashboard application.
## Authentication
All API endpoints require authentication via Bearer token in the Authorization header:
```
Authorization: Bearer <your-token>
```
## Endpoints
### User Profile
**GET /api/user/profile**
Returns the current user's profile information.
Response:
```json
{
"id": "user-123",
"name": "John Doe",
"email": "john@example.com",
"role": "developer",
"avatar_url": "https://example.com/avatar.jpg"
}
```
### Projects
**GET /api/projects**
Returns a list of all projects.
Query Parameters:
- `status` - Filter by project status (active, archived)
- `limit` - Number of results to return (default: 20)
Response:
```json
{
"projects": [
{
"id": "proj-123",
"name": "Sample Project",
"status": "active",
"created_at": "2024-01-15T10:00:00Z"
}
],
"total": 1
}
```
### Tasks
**POST /api/tasks**
Creates a new task.
Request Body:
```json
{
"title": "Implement feature X",
"description": "Add the new feature to the dashboard",
"priority": "high",
"assignee": "user-123"
}
```
## Error Responses
All errors follow this format:
```json
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request parameters",
"details": ["Missing required field: title"]
}
}
```
"#;
std::fs::write(project_path.join("docs/api.md"), api_docs)?;
Ok(())
}
fn create_development_commits(project_path: &std::path::Path) -> Result<()> {
let auth_js = r#"// User authentication module
class AuthManager {
constructor(apiBase) {
this.apiBase = apiBase;
this.token = localStorage.getItem('auth_token');
}
async login(email, password) {
const response = await fetch(`${this.apiBase}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await response.json();
if (data.token) {
this.token = data.token;
localStorage.setItem('auth_token', this.token);
}
return data;
}
logout() {
this.token = null;
localStorage.removeItem('auth_token');
}
isAuthenticated() {
return !!this.token;
}
getAuthHeaders() {
return this.token ? { 'Authorization': `Bearer ${this.token}` } : {};
}
}
"#;
std::fs::write(project_path.join("src/auth.js"), auth_js)?;
commit_sample_change(
project_path,
&["src/auth.js"],
"Add user authentication module
- Implement AuthManager class for login/logout
- Add token-based authentication support
- Store auth tokens in localStorage
- Provide helper methods for authenticated requests",
)?;
let updated_app = r#"// Main application entry point
class DashboardApp {
constructor() {
this.apiBase = '/api';
this.currentUser = null;
this.authManager = new AuthManager(this.apiBase);
this.init();
}
async init() {
if (!this.authManager.isAuthenticated()) {
this.showLoginForm();
return;
}
await this.loadUserProfile();
this.setupEventListeners();
this.renderDashboard();
}
async loadUserProfile() {
try {
const response = await fetch(`${this.apiBase}/user/profile`, {
headers: this.authManager.getAuthHeaders()
});
this.currentUser = await response.json();
} catch (error) {
console.error('Failed to load user profile:', error);
this.authManager.logout();
this.showLoginForm();
}
}
setupEventListeners() {
document.getElementById('refresh-btn')?.addEventListener('click', () => {
this.refreshData();
});
document.getElementById('logout-btn')?.addEventListener('click', () => {
this.authManager.logout();
this.showLoginForm();
});
}
renderDashboard() {
const container = document.getElementById('dashboard');
if (container) {
container.innerHTML = `
<div class="header">
<h1>Welcome, ${this.currentUser?.name || 'User'}</h1>
<button id="logout-btn">Logout</button>
</div>
<div class="metrics">
<div class="metric-card">
<h3>Active Projects</h3>
<span class="metric-value">12</span>
</div>
<div class="metric-card">
<h3>Tasks Completed</h3>
<span class="metric-value">84</span>
</div>
<div class="metric-card">
<h3>Team Members</h3>
<span class="metric-value">6</span>
</div>
</div>
`;
this.setupEventListeners();
}
}
showLoginForm() {
const container = document.getElementById('dashboard');
if (container) {
container.innerHTML = `
<div class="login-form">
<h2>Login</h2>
<form id="login-form">
<input type="email" id="email" placeholder="Email" required>
<input type="password" id="password" placeholder="Password" required>
<button type="submit">Login</button>
</form>
</div>
`;
document.getElementById('login-form')?.addEventListener('submit', async (e) => {
e.preventDefault();
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;
try {
await this.authManager.login(email, password);
this.init();
} catch (error) {
alert('Login failed: ' + error.message);
}
});
}
}
async refreshData() {
console.log('Refreshing dashboard data...');
await this.loadUserProfile();
this.renderDashboard();
}
}
// Initialize app when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
new DashboardApp();
});
"#;
std::fs::write(project_path.join("src/app.js"), updated_app)?;
commit_sample_change(
project_path,
&["src/app.js"],
"Integrate authentication into dashboard
- Add login/logout functionality to main app
- Require authentication before showing dashboard
- Add logout button to dashboard header
- Handle authentication errors gracefully
- Show login form for unauthenticated users",
)?;
let updated_css = r#"/* Dashboard Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f5f5f5;
color: #333;
}
#dashboard {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid #ddd;
}
.header h1 {
color: #2c3e50;
font-weight: 300;
margin: 0;
}
#logout-btn {
background: #e74c3c;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
#logout-btn:hover {
background: #c0392b;
}
.metrics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.metric-card {
background: white;
padding: 24px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.metric-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.metric-card h3 {
color: #666;
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.metric-value {
font-size: 32px;
font-weight: 700;
color: #3498db;
}
.login-form {
max-width: 400px;
margin: 100px auto;
padding: 40px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
.login-form h2 {
text-align: center;
margin-bottom: 30px;
color: #2c3e50;
font-weight: 300;
}
.login-form input {
width: 100%;
padding: 12px 16px;
margin-bottom: 16px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s ease;
}
.login-form input:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
}
.login-form button {
width: 100%;
padding: 12px;
background: #3498db;
color: white;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: background 0.3s ease;
}
.login-form button:hover {
background: #2980b9;
}
#refresh-btn {
background: #27ae60;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: background 0.3s ease;
}
#refresh-btn:hover {
background: #219a52;
}
/* Responsive Design */
@media (max-width: 768px) {
#dashboard {
padding: 15px;
}
.header {
flex-direction: column;
gap: 15px;
align-items: flex-start;
}
.header h1 {
font-size: 24px;
}
.metrics {
grid-template-columns: 1fr;
gap: 15px;
}
.metric-card {
padding: 20px;
}
.metric-value {
font-size: 28px;
}
.login-form {
margin: 50px 20px;
padding: 30px 20px;
}
}
@media (max-width: 480px) {
.metric-card {
padding: 16px;
}
.metric-value {
font-size: 24px;
}
.login-form {
margin: 30px 15px;
padding: 25px 15px;
}
}
"#;
std::fs::write(project_path.join("src/styles.css"), updated_css)?;
commit_sample_change(
project_path,
&["src/styles.css"],
"Add responsive design and improved styling
- Implement responsive design for mobile devices
- Add hover effects and smooth transitions
- Improve login form styling and layout
- Add header with logout button styling
- Enhance metric cards with better visual hierarchy
- Add media queries for tablet and mobile breakpoints",
)?;
Ok(())
}
fn commit_sample_change(project_path: &Path, paths: &[&str], message: &str) -> Result<()> {
let repo = wsb::git_backend::open_repo(project_path)?;
let paths: Vec<String> = paths.iter().map(|path| (*path).to_string()).collect();
wsb::git_backend::stage_paths(&repo, &paths)?;
wsb::git_backend::commit_all(&repo, message)?
.with_context(|| format!("Failed to commit sample change in {}", project_path.display()))?;
Ok(())
}
fn populate_sample_data_in_dir(output_dir: &str, force: bool) -> Result<()> {
println!(
"{} Populating database with sample data in {}...",
"🗄️".blue().bold(),
output_dir
);
let output_path = std::path::Path::new(output_dir);
let db_path = output_path.join(".wsb/wsb.db");
if db_path.exists() && !force {
let output = std::process::Command::new("sqlite3")
.arg(&db_path)
.arg("SELECT COUNT(*) FROM features;")
.output();
if let Ok(output) = output {
if output.status.success() {
let count = String::from_utf8_lossy(&output.stdout)
.trim()
.parse::<i32>()
.unwrap_or(0);
if count > 0 {
println!(
"{} Database already has {} features (use --force to overwrite)",
"⚠️".yellow(),
count
);
return Ok(());
}
}
}
}
tokio::runtime::Runtime::new()?
.block_on(async { populate_sample_data_in_dir_async(output_dir, force).await })
}
async fn populate_sample_data_in_dir_async(output_dir: &str, _force: bool) -> Result<()> {
let output_path = std::path::Path::new(output_dir);
let abs_path = if output_path.is_absolute() {
output_path.to_path_buf()
} else {
std::env::current_dir()?.join(output_path)
};
let ctx = AppContext::bootstrap(&abs_path)?;
let db = &ctx.db;
let current_project = db.project_repo.list(None, None).await?.into_iter().next()
.ok_or_else(|| anyhow::anyhow!("No active project"))?;
let project_id = ¤t_project.id;
let test_data_sql = format!(
r#"-- Clear existing sample data (keep project)
DELETE FROM entity_audit_trails;
DELETE FROM feature_state_transitions;
DELETE FROM note_links;
DELETE FROM notes;
DELETE FROM dependencies;
DELETE FROM tests;
DELETE FROM templates;
DELETE FROM directives;
DELETE FROM milestones;
DELETE FROM sessions;
DELETE FROM tasks;
DELETE FROM features;
-- Insert sample features for current project
INSERT INTO features (id, project_id, code, name, description, category, state, test_status, priority, implementation_notes, test_evidence, created_at, updated_at, completed_at, estimated_effort, actual_effort) VALUES
('feat-001', '{project_id}', 'F-001', 'User Authentication Portal', 'Secure login system with multi-factor authentication and SSO integration', 'Frontend', 'tested_passing', 'All authentication tests passing', 'critical', 'Implemented using OAuth 2.0 and JWT tokens', 'All authentication tests passing', '2024-06-01T09:00:00Z', '2024-07-15T16:30:00Z', '2024-07-15T16:30:00Z', 40, 45),
('feat-002', '{project_id}', 'F-002', 'Dashboard Analytics Widget', 'Interactive dashboard with real-time metrics and customizable charts', 'Frontend', 'in_progress', 'Unit tests 70% complete', 'high', 'Using Chart.js and WebSocket for real-time updates', 'Unit tests 70% complete', '2024-06-15T10:00:00Z', '2024-08-01T14:20:00Z', NULL, 32, 28),
('feat-003', '{project_id}', 'F-003', 'Mobile Responsive Layout', 'Responsive design system supporting all device sizes', 'Frontend', 'tested_passing', 'Cross-browser testing completed', 'medium', 'Bootstrap 5 with custom breakpoints', 'Cross-browser testing completed', '2024-05-20T11:30:00Z', '2024-07-30T09:15:00Z', '2024-07-28T15:45:00Z', 24, 22),
('feat-004', '{project_id}', 'F-004', 'Progressive Web App', 'PWA capabilities with offline support and push notifications', 'Frontend', 'not_implemented', 'Not yet implemented', 'medium', NULL, NULL, '2024-07-01T08:00:00Z', '2024-07-01T08:00:00Z', NULL, 48, NULL),
('feat-005', '{project_id}', 'F-005', 'GraphQL API Gateway', 'Unified GraphQL endpoint aggregating multiple microservices', 'Backend', 'tested_passing', 'Load testing completed', 'critical', 'Apollo Server with federation and caching', 'Load testing completed', '2024-05-01T09:30:00Z', '2024-07-20T11:45:00Z', '2024-07-18T14:20:00Z', 60, 65),
('feat-006', '{project_id}', 'F-006', 'Payment Processing Service', 'Secure payment gateway with multiple provider support', 'Backend', 'tested_failing', 'Payment tests failing on edge cases', 'critical', 'Stripe and PayPal integration with webhook handling', 'Payment tests failing on edge cases', '2024-06-10T10:15:00Z', '2024-08-02T16:00:00Z', NULL, 80, 72),
('feat-007', '{project_id}', 'F-007', 'Inventory Management API', 'RESTful API for product catalog and stock management', 'Backend', 'tested_passing', 'API documentation complete', 'high', 'CRUD operations with optimistic locking', 'API documentation complete', '2024-05-15T14:00:00Z', '2024-07-10T10:30:00Z', '2024-07-08T16:45:00Z', 45, 42),
('feat-008', '{project_id}', 'F-008', 'Machine Learning Pipeline', 'Automated ML pipeline for recommendation engine', 'Backend', 'not_implemented', 'Not yet started', 'medium', NULL, NULL, '2024-07-15T09:00:00Z', '2024-07-15T09:00:00Z', NULL, 120, NULL),
('feat-009', '{project_id}', 'F-009', 'Search Functionality', 'Full-text search with filtering and pagination', 'Backend', 'in_progress', 'Integration tests passing', 'high', 'Elasticsearch with custom analyzers', 'Integration tests passing', '2024-07-20T11:00:00Z', '2024-08-01T14:30:00Z', NULL, 56, 48),
('feat-010', '{project_id}', 'F-010', 'Admin Dashboard', 'Administrative interface for system management', 'Frontend', 'tested_passing', 'UI tests complete', 'medium', 'React admin panel with role-based access', 'UI tests complete', '2024-06-01T10:00:00Z', '2024-07-25T16:00:00Z', '2024-07-25T16:00:00Z', 36, 34);
-- Insert comprehensive tasks
INSERT INTO tasks (id, project_id, code, title, description, category, status, priority, acceptance_criteria, validation_steps, evidence, assigned_to, created_at, updated_at, started_at, completed_at, estimated_effort, actual_effort) VALUES
('task-001', '{project_id}', 'TASK-001', 'Setup Production Infrastructure', 'Configure production AWS environment with security groups and VPC', 'infrastructure', 'completed', 'critical', 'Production environment accessible and secure', '1. VPC configured\n2. Security groups configured\n3. IAM roles configured', 'Infrastructure documentation completed', 'devops-team', '2024-03-01T09:00:00Z', '2024-03-15T16:30:00Z', '2024-03-01T09:30:00Z', '2024-03-15T16:30:00Z', 40, 42),
('task-002', '{project_id}', 'TASK-002', 'Database Schema Design', 'Design and implement normalized database schema', 'infrastructure', 'completed', 'critical', 'Schema supports all business requirements', '1. All entities normalized\n2. Foreign keys in place\n3. Indexes optimized', 'Schema documentation completed', 'backend-team', '2024-03-10T10:00:00Z', '2024-03-25T14:20:00Z', '2024-03-15T11:00:00Z', '2024-03-25T14:20:00Z', 32, 35),
('task-003', '{project_id}', 'TASK-003', 'User Authentication Implementation', 'Implement secure user registration and login system', 'feature', 'completed', 'critical', 'Users can register, login, and access protected resources', '1. Registration flow works\n2. Login with MFA functional\n3. JWT tokens validated', 'All authentication tests passing', 'fullstack-team', '2024-04-01T08:30:00Z', '2024-04-20T17:45:00Z', '2024-04-01T09:00:00Z', '2024-04-20T17:45:00Z', 48, 52),
('task-004', '{project_id}', 'TASK-004', 'Payment Gateway Integration', 'Integrate Stripe and PayPal payment processing', 'feature', 'in_progress', 'critical', 'Secure payment processing with proper error handling', '1. Stripe integration functional\n2. PayPal integration working\n3. Webhook handlers implemented', 'Stripe integration 90% complete', 'backend-team', '2024-06-01T09:00:00Z', '2024-08-02T15:30:00Z', '2024-06-01T09:30:00Z', NULL, 56, 48),
('task-005', '{project_id}', 'TASK-005', 'Mobile App Development', 'Develop React Native mobile application', 'feature', 'in_progress', 'high', 'Mobile app functional on iOS and Android', '1. App builds successfully\n2. Core features working\n3. App store guidelines met', 'iOS version 70% complete', 'mobile-team', '2024-05-15T10:00:00Z', '2024-08-01T14:15:00Z', '2024-05-20T08:00:00Z', NULL, 80, 65),
('task-006', '{project_id}', 'TASK-006', 'API Performance Optimization', 'Optimize API response times and database queries', 'performance', 'in_progress', 'high', 'API response times under 200ms for 95th percentile', '1. Load testing shows improvement\n2. Database optimization complete\n3. Caching strategy implemented', 'Database optimization 60% complete', 'backend-team', '2024-06-15T11:00:00Z', '2024-08-02T12:45:00Z', '2024-06-20T09:00:00Z', NULL, 44, 38),
('task-007', '{project_id}', 'TASK-007', 'Implement Search Functionality', 'Add full-text search with filtering and pagination', 'feature', 'pending', 'medium', 'Users can search and filter content effectively', '1. Search results relevant\n2. Filters work correctly\n3. Pagination handles large result sets', NULL, 'fullstack-team', '2024-07-01T10:00:00Z', '2024-07-15T16:20:00Z', NULL, NULL, 36, NULL),
('task-008', '{project_id}', 'TASK-008', 'Create Admin Dashboard', 'Build administrative interface for system management', 'feature', 'pending', 'medium', 'Administrators can manage users and settings', '1. User management functional\n2. System settings configurable\n3. Audit logs accessible', NULL, 'frontend-team', '2024-07-10T09:30:00Z', '2024-07-20T11:45:00Z', NULL, NULL, 40, NULL),
('task-009', '{project_id}', 'TASK-009', 'Implement Email Notifications', 'Set up transactional email system with templates', 'feature', 'in_progress', 'medium', 'System sends relevant notifications to users', '1. Email templates render correctly\n2. Delivery tracking functional\n3. Unsubscribe mechanism works', 'Email service configured', 'backend-team', '2024-06-20T08:00:00Z', '2024-08-01T13:30:00Z', '2024-06-25T10:00:00Z', NULL, 24, 20),
('task-010', '{project_id}', 'TASK-010', 'Third-party API Integration', 'Integrate external APIs for enhanced functionality', 'integration', 'blocked', 'high', 'External APIs properly integrated with error handling', '1. API calls successful\n2. Rate limiting respected\n3. Error scenarios handled', 'Blocked pending payment system completion', 'integration-team', '2024-07-01T11:00:00Z', '2024-07-25T15:00:00Z', NULL, NULL, 32, NULL);
-- Insert sessions
INSERT INTO sessions (id, project_id, title, description, state, started_at, ended_at, summary, achievements, created_at, updated_at) VALUES
('session-001', '{project_id}', 'Sprint 1 Development', 'Initial development sprint focusing on core authentication', 'completed', '2024-03-01T09:00:00Z', '2024-03-15T17:00:00Z', 'Successfully implemented user authentication system', 'Authentication system, database schema, production infrastructure', '2024-03-01T09:00:00Z', '2024-03-15T17:00:00Z'),
('session-002', '{project_id}', 'Sprint 2 Development', 'Payment system integration and testing', 'completed', '2024-03-16T09:00:00Z', '2024-03-30T17:00:00Z', 'Made significant progress on payment integration', 'GraphQL API, inventory management, testing framework', '2024-03-16T09:00:00Z', '2024-03-30T17:00:00Z'),
('session-003', '{project_id}', 'Sprint 3 Development', 'Performance optimization and monitoring setup', 'completed', '2024-04-01T09:00:00Z', '2024-04-15T17:00:00Z', 'Implemented comprehensive monitoring', 'CDN integration, monitoring dashboard, container orchestration', '2024-04-01T09:00:00Z', '2024-04-15T17:00:00Z'),
('session-004', '{project_id}', 'Sprint 4 Development', 'Mobile app development and API enhancements', 'active', '2024-07-15T09:00:00Z', NULL, NULL, NULL, '2024-07-15T09:00:00Z', '2024-08-02T16:00:00Z');
-- Insert dependencies
INSERT INTO dependencies (id, project_id, from_entity_id, from_entity_type, to_entity_id, to_entity_type, dependency_type, description, created_at) VALUES
('dep-001', '{project_id}', 'feat-002', 'feature', 'feat-001', 'feature', 'requires', 'Dashboard requires user authentication', '2024-06-15T10:00:00Z'),
('dep-002', '{project_id}', 'feat-004', 'feature', 'feat-001', 'feature', 'requires', 'PWA requires authentication system', '2024-07-01T08:00:00Z'),
('dep-003', '{project_id}', 'feat-007', 'feature', 'feat-001', 'feature', 'requires', 'Payment system requires authentication', '2024-06-10T10:15:00Z'),
('dep-004', '{project_id}', 'task-002', 'task', 'task-001', 'task', 'requires', 'Database schema requires infrastructure', '2024-03-10T10:00:00Z'),
('dep-005', '{project_id}', 'task-003', 'task', 'task-002', 'task', 'requires', 'Authentication requires database schema', '2024-04-01T08:30:00Z');
-- Insert notes
INSERT INTO notes (id, project_id, entity_id, entity_type, note_type, title, content, tags, author, is_project_wide, is_pinned, created_at, updated_at, metadata) VALUES
('note-001', '{project_id}', 'feat-001', 'feature', 'architecture', 'Authentication Architecture Decision', 'Decided to use OAuth 2.0 with PKCE for mobile clients and standard authorization code flow for web clients.', NULL, 'tech_lead', FALSE, FALSE, '2024-03-15T14:30:00Z', '2024-03-15T14:30:00Z', NULL),
('note-002', '{project_id}', 'feat-007', 'feature', 'decision', 'Payment Provider Selection', 'After evaluating Stripe, PayPal, and Square, decided on Stripe as primary with PayPal as secondary.', NULL, 'product_owner', FALSE, TRUE, '2024-06-10T15:20:00Z', '2024-06-10T15:20:00Z', NULL),
('note-003', '{project_id}', 'feat-002', 'feature', 'issue', 'Performance Bottleneck Identified', 'Database queries for user dashboard are taking 2-3 seconds due to N+1 problem.', '["performance", "database"]', 'dev_team', FALSE, TRUE, '2024-07-25T10:15:00Z', '2024-07-25T10:15:00Z', NULL),
('note-004', '{project_id}', 'task-004', 'task', 'issue', 'Payment Webhook Failures', 'Stripe webhooks are failing intermittently due to timeout issues.', '["payment", "webhook", "urgent"]', 'dev_team', FALSE, FALSE, '2024-07-28T14:30:00Z', '2024-07-28T14:30:00Z', NULL),
('note-005', '{project_id}', 'feat-004', 'feature', 'reference', 'Progressive Web App Enhancement', 'Consider implementing advanced PWA features like background sync and push notifications.', '["pwa", "enhancement"]', 'designer', FALSE, FALSE, '2024-07-01T12:00:00Z', '2024-07-01T12:00:00Z', NULL),
('note-006', '{project_id}', 'task-001', 'task', 'progress', 'Infrastructure Setup Complete', 'AWS infrastructure is fully configured with auto-scaling, monitoring, and backup systems operational.', '["infrastructure", "aws"]', 'devops_team', FALSE, FALSE, '2024-03-15T16:00:00Z', '2024-03-15T16:00:00Z', NULL),
('note-007', '{project_id}', 'feat-008', 'feature', 'evidence', 'Performance Benchmarks', 'API response times: 95th percentile under 150ms, throughput 5000 requests/second with caching layer.', '["performance", "metrics"]', 'qa_team', FALSE, FALSE, '2024-07-08T14:30:00Z', '2024-07-08T14:30:00Z', NULL),
('note-008', '{project_id}', NULL, NULL, 'architecture', 'Microservices Architecture Decision', 'Adopted microservices architecture with API gateway, service mesh, and distributed tracing for scalability.', '["architecture", "microservices"]', 'architect', TRUE, TRUE, '2024-02-15T10:00:00Z', '2024-02-15T10:00:00Z', NULL);
-- Insert directives (using correct column names: rule instead of description)
INSERT INTO directives (id, project_id, code, title, rule, category, priority, context, rationale, examples, created_at, updated_at) VALUES
('dir-001', '{project_id}', 'DEV-001', 'Code Review Mandatory', 'All code changes must undergo peer review before merging to main branch', 'development', 'high', 'All pull requests and merge requests', 'Ensure code quality and knowledge sharing', 'Pull requests blocked without approvals, CI/CD pipeline enforces checks', '2024-03-01T09:00:00Z', '2024-07-15T14:30:00Z'),
('dir-002', '{project_id}', 'SEC-001', 'Secret Management Policy', 'No secrets or API keys in source code, use environment variables or secure vaults', 'security', 'high', 'All code commits and deployments', 'Prevent security breaches and credential exposure', 'AWS Secrets Manager, HashiCorp Vault, environment-specific configurations', '2024-03-01T09:30:00Z', '2024-06-20T11:15:00Z'),
('dir-003', '{project_id}', 'TEST-001', 'Minimum Test Coverage', 'Maintain minimum 80% code coverage for all modules', 'testing', 'high', 'All production code modules', 'Ensure code reliability and catch regressions', 'Jest for frontend, pytest for backend, integration tests for APIs', '2024-03-15T10:00:00Z', '2024-07-30T16:45:00Z'),
('dir-004', '{project_id}', 'ARCH-001', 'API Versioning Strategy', 'All public APIs must include version numbers and maintain backward compatibility', 'architecture', 'medium', 'All public API endpoints', 'Prevent breaking changes for API consumers', 'v1/users, v2/orders, deprecation headers for sunset endpoints', '2024-04-01T08:00:00Z', '2024-07-01T12:30:00Z'),
('dir-005', '{project_id}', 'PERF-001', 'Performance Budgets', 'Frontend bundle size under 1MB, API response times under 200ms', 'performance', 'medium', 'All frontend builds and API endpoints', 'Maintain optimal user experience', 'Webpack bundle analyzer, New Relic monitoring, Lighthouse CI', '2024-05-01T11:00:00Z', '2024-07-20T09:45:00Z'),
('dir-006', '{project_id}', 'DOC-001', 'API Documentation Required', 'All API endpoints must have OpenAPI documentation with examples', 'documentation', 'medium', 'All API endpoints', 'Facilitate API integration and maintenance', 'Swagger UI, Redoc, automated doc generation from code annotations', '2024-04-15T14:00:00Z', '2024-06-30T10:20:00Z');
-- Insert milestones (using correct column names: no code column, no validation_evidence, achievement_summary, feature_count, task_count)
INSERT INTO milestones (id, project_id, title, description, target_date, achieved_date, success_criteria, status, completion_percentage, created_at, updated_at) VALUES
('milestone-001', '{project_id}', 'MVP Launch', 'Minimum viable product with core authentication and payment features', '2024-06-30T23:59:59Z', '2024-07-15T16:30:00Z', 'User registration, login, payment processing, basic dashboard functional', 'achieved', 100.0, '2024-03-01T09:00:00Z', '2024-07-15T17:00:00Z'),
('milestone-002', '{project_id}', 'Beta Release', 'Feature-complete beta with advanced analytics and mobile support', '2024-08-31T23:59:59Z', NULL, 'Mobile responsive design, advanced analytics, performance optimizations complete', 'in_progress', 65.0, '2024-06-01T10:00:00Z', '2024-08-01T14:20:00Z'),
('milestone-003', '{project_id}', 'Production Scaling', 'Production-ready system handling 10,000 concurrent users', '2024-10-31T23:59:59Z', NULL, 'Load testing passed, auto-scaling configured, monitoring comprehensive', 'planned', 0.0, '2024-07-01T11:00:00Z', '2024-07-15T16:00:00Z'),
('milestone-004', '{project_id}', 'Q4 Feature Expansion', 'Advanced features including AI recommendations and multi-tenant support', '2024-12-31T23:59:59Z', NULL, 'AI models deployed, multi-tenancy implemented, enterprise features complete', 'planned', 0.0, '2024-08-01T09:00:00Z', '2024-08-01T09:00:00Z');
-- Insert note links (using correct column names: target_id and target_type instead of target_entity_id and target_entity_type)
INSERT INTO note_links (id, project_id, source_note_id, target_id, target_type, link_type, created_at) VALUES
('link-001', '{project_id}', 'note-001', 'feat-007', 'feature', 'reference', '2024-06-10T15:30:00Z'),
('link-002', '{project_id}', 'note-002', 'task-004', 'task', 'related', '2024-06-10T15:45:00Z'),
('link-003', '{project_id}', 'note-003', 'feat-008', 'feature', 'blocks', '2024-07-25T10:30:00Z'),
('link-004', '{project_id}', 'note-004', 'note-002', 'note', 'response_to', '2024-07-28T14:45:00Z'),
('link-005', '{project_id}', 'note-005', 'milestone-002', 'milestone', 'depends_on', '2024-07-01T12:15:00Z'),
('link-006', '{project_id}', 'note-006', 'milestone-001', 'milestone', 'reference', '2024-03-15T16:15:00Z'),
('link-007', '{project_id}', 'note-007', 'dir-005', 'directive', 'reference', '2024-07-08T14:45:00Z'),
('link-008', '{project_id}', 'note-008', 'feat-006', 'feature', 'reference', '2024-05-01T09:45:00Z');
-- Insert audit trail records for recent activity
INSERT INTO entity_audit_trails (id, entity_id, entity_type, project_id, operation_type, field_changed, old_value, new_value, change_reason, triggered_by, session_id, timestamp, metadata) VALUES
('audit-001', 'feat-001', 'feature', '{project_id}', 'create', NULL, NULL, NULL, 'Initial feature creation during project setup', 'sample-generator', 'session-001', '2024-03-15T14:00:00Z', NULL),
('audit-002', 'feat-002', 'feature', '{project_id}', 'create', NULL, NULL, NULL, 'Dashboard feature added', 'sample-generator', 'session-001', '2024-03-15T14:05:00Z', NULL),
('audit-003', 'feat-001', 'feature', '{project_id}', 'update', 'state', 'planned', 'implemented', 'Feature implementation completed', 'development-team', 'session-001', '2024-03-15T16:30:00Z', '{{"completion_percentage": 100}}'),
('audit-004', 'task-001', 'task', '{project_id}', 'create', NULL, NULL, NULL, 'Infrastructure setup task created', 'sample-generator', 'session-001', '2024-03-10T09:00:00Z', NULL),
('audit-005', 'task-001', 'task', '{project_id}', 'update', 'status', 'pending', 'in_progress', 'Started infrastructure work', 'ops-team', 'session-001', '2024-03-10T10:00:00Z', NULL),
('audit-006', 'task-001', 'task', '{project_id}', 'update', 'status', 'in_progress', 'completed', 'Infrastructure deployment finished', 'ops-team', 'session-001', '2024-03-15T15:30:00Z', NULL),
('audit-007', 'feat-007', 'feature', '{project_id}', 'create', NULL, NULL, NULL, 'Payment system feature created', 'sample-generator', 'session-002', '2024-06-10T10:00:00Z', NULL),
('audit-008', 'feat-007', 'feature', '{project_id}', 'update', 'state', 'planned', 'implemented', 'Payment integration completed', 'backend-team', 'session-002', '2024-06-10T16:00:00Z', '{{"provider": "stripe"}}'),
('audit-009', 'task-004', 'task', '{project_id}', 'create', NULL, NULL, NULL, 'Payment API integration task', 'sample-generator', 'session-002', '2024-06-01T09:00:00Z', NULL),
('audit-010', 'task-004', 'task', '{project_id}', 'state_change', 'status', 'pending', 'blocked', 'Blocked by external API issues', 'integration-team', 'session-004', '2024-07-28T14:00:00Z', '{{"blocking_reason": "API rate limits"}}'),
('audit-011', 'feat-008', 'feature', '{project_id}', 'create', NULL, NULL, NULL, 'Performance monitoring feature added', 'sample-generator', 'session-003', '2024-04-01T09:30:00Z', NULL),
('audit-012', 'session-004', 'session', '{project_id}', 'create', NULL, NULL, NULL, 'New development session started', 'project-manager', NULL, '2024-07-15T09:00:00Z', '{{"sprint": "Sprint 4"}}'),
('audit-013', 'note-001', 'note', '{project_id}', 'create', NULL, NULL, NULL, 'Architecture decision documented', 'architect', 'session-001', '2024-03-15T14:30:00Z', '{{"category": "architecture"}}'),
('audit-014', 'feat-004', 'feature', '{project_id}', 'update', 'priority', 'medium', 'high', 'Increased priority for PWA features', 'product-manager', 'session-004', '2024-07-20T11:00:00Z', NULL),
('audit-015', 'milestone-001', 'milestone', '{project_id}', 'update', 'status', 'in_progress', 'achieved', 'Q4 Feature Expansion milestone completed', 'project-manager', 'session-004', '2024-07-30T17:00:00Z', '{{"completion_date": "2024-07-30"}}');
"#,
project_id = project_id
);
let db_path = abs_path.join(".wsb/wsb.db");
let db_path_str = db_path.to_string_lossy();
let mut child = std::process::Command::new("sqlite3")
.arg(&*db_path_str)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()?;
if let Some(stdin) = child.stdin.as_mut() {
use std::io::Write;
stdin.write_all(test_data_sql.as_bytes())?;
}
let output = child.wait_with_output()?;
if !output.status.success() {
let error = String::from_utf8_lossy(&output.stderr);
println!(
"{} Warning: Some SQL statements failed: {}",
"⚠️".yellow(),
error
);
}
let project = db.project_repo.list(None, None).await?.into_iter().next()
.ok_or_else(|| anyhow::anyhow!("No active project"))?;
let features = db.feature_repo.list(None, None).await?.into_iter()
.filter(|f| f.project_id == project.id).collect::<Vec<_>>();
let tasks = db.task_repo.list(None, None).await?.into_iter()
.filter(|t| t.project_id == project.id).collect::<Vec<_>>();
println!(" {} {} features created", "📋".cyan(), features.len());
println!(" {} {} tasks created", "✅".cyan(), tasks.len());
println!(" {} Comprehensive sample data loaded", "✅".green());
Ok(())
}
fn link_entities(
from_entity: String,
from_type: String,
to_entity: String,
to_type: String,
relationship_type: String,
description: Option<String>,
ctx: &AppContext,
) -> Result<()> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let db = &ctx.db;
let _project = db.project_repo.list(None, None).await?.into_iter().next();
let from_entity_type = parse_entity_type(&from_type)?;
let to_entity_type = parse_entity_type(&to_type)?;
println!("Dependency creation not implemented in new schema");
Ok(())
})
}
fn list_entity_relationships(
entity_id: String,
entity_type: String,
_relationship_type: Option<String>,
_include_resolved: bool,
ctx: &AppContext,
) -> Result<()> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
println!("Relationship listing not implemented in new schema");
println!(
"{} Relationships for {} {}",
"🔗".cyan(),
entity_type,
entity_id
);
Ok(())
})
}
fn unlink_entities(dependency_id: String, force: bool, ctx: &AppContext) -> Result<()> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let service = ctx.db_service.clone();
if !force {
print!("Remove relationship {}? [y/N]: ", dependency_id);
use std::io::Write;
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
if !input.trim().to_lowercase().starts_with('y') {
println!("Cancelled");
return Ok(());
}
}
let conn = service.get_connection()?;
conn.execute("DELETE FROM dependencies WHERE id = ?", rusqlite::params![&dependency_id])?;
println!("{} Removed relationship {}", "✅".green(), dependency_id);
Ok(())
})
}
fn resolve_entity_relationship(dependency_id: String, description: Option<String>, ctx: &AppContext) -> Result<()> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
println!("Dependency resolution not implemented in new schema");
println!("{} Resolved relationship {}", "✅".green(), dependency_id);
if let Some(desc) = description {
println!(" Resolution: {}", desc);
}
Ok(())
})
}
fn show_relationship_stats(detailed: bool, format: String, ctx: &AppContext) -> Result<()> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let db = &ctx.db;
let project = db.project_repo.list(None, None).await?.into_iter().next()
.ok_or_else(|| anyhow::anyhow!("No active project"))?;
println!("Project dependencies listing not implemented in new schema");
let dependencies: Vec<String> = vec![];
if format == "json" {
let stats = serde_json::json!({
"total_relationships": dependencies.len(),
"active_relationships": dependencies.len(), "resolved_relationships": 0, });
println!("{}", serde_json::to_string_pretty(&stats)?);
} else {
println!(
"{} Relationship Statistics for {}",
"📊".cyan(),
project.name
);
println!(" Total relationships: {}", dependencies.len());
println!(" Active relationships: {}", dependencies.len()); println!(" Resolved relationships: {}", 0);
if detailed {
println!(" Breakdown by type: Not yet implemented");
}
}
Ok(())
})
}
fn parse_entity_type(type_str: &str) -> Result<wsb::entities::EntityType> {
match type_str.to_lowercase().as_str() {
"project" => Ok(wsb::entities::EntityType::Project),
"feature" => Ok(wsb::entities::EntityType::Feature),
"task" => Ok(wsb::entities::EntityType::Task),
"session" => Ok(wsb::entities::EntityType::Session),
"directive" => Ok(wsb::entities::EntityType::Directive),
_ => Err(anyhow::anyhow!("Unknown entity type: {}", type_str)),
}
}
fn run_note_command(action: NoteAction, ctx: &AppContext) -> Result<()> {
match action {
NoteAction::Add {
entity_type,
entity_id,
title,
content,
note_type,
tags,
} => {
add_entity_note(entity_type, entity_id, title, content, note_type, tags, ctx)?;
}
NoteAction::AddProject {
title,
content,
note_type,
tags,
} => {
add_project_note(title, content, note_type, tags, ctx)?;
}
NoteAction::List {
entity_type,
entity_id,
note_type,
project_wide,
pinned,
} => {
list_notes(entity_type, entity_id, note_type, project_wide, pinned, ctx)?;
}
NoteAction::Search {
query,
note_type,
format,
} => {
search_notes(query, note_type, format, ctx)?;
}
NoteAction::Update {
note_id,
title,
content,
tags,
} => {
update_note(note_id, title, content, tags, ctx)?;
}
NoteAction::Delete { note_id, force } => {
delete_note(note_id, force, ctx)?;
}
NoteAction::Pin { note_id } => {
toggle_note_pin(note_id, ctx)?;
}
NoteAction::Link {
source_note_id,
target_id,
target_type,
entity_type,
link_type,
} => {
link_note_to_target(
source_note_id,
target_id,
target_type,
entity_type,
link_type,
ctx,
)?;
}
NoteAction::Unlink { link_id, force } => {
unlink_note(link_id, force, ctx)?;
}
NoteAction::ListLinks {
id,
incoming,
outgoing,
format,
} => {
list_note_links(id, incoming, outgoing, format, ctx)?;
}
}
Ok(())
}
fn add_entity_note(
entity_type: String,
entity_id: String,
title: String,
content: String,
note_type: String,
_tags: Option<String>,
ctx: &AppContext,
) -> Result<()> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let entity_type_enum = parse_entity_type(&entity_type)?;
let note_type_enum = parse_note_type(¬e_type)?;
println!("Note creation not implemented in new schema");
Ok(())
})
}
fn add_project_note(
title: String,
content: String,
note_type: String,
_tags: Option<String>,
ctx: &AppContext,
) -> Result<()> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let db = &ctx.db;
let _project = db.project_repo.list(None, None).await?.into_iter().next();
println!("Project note creation not implemented in new schema");
Ok(())
})
}
fn list_notes(
_entity_type: Option<String>,
entity_id: Option<String>,
_note_type: Option<String>,
project_wide: bool,
_pinned: bool,
ctx: &AppContext,
) -> Result<()> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let db = &ctx.db;
let _project = db.project_repo.list(None, None).await?.into_iter().next();
println!("Note listing not implemented in new schema");
let notes: Vec<String> = vec![];
if notes.is_empty() {
println!("{} No notes found", "ℹ️".blue());
return Ok(());
}
println!("{} Found {} notes", "📝".cyan(), notes.len());
for note in notes {
println!(" Note: {}", note);
}
Ok(())
})
}
fn search_notes(query: String, note_type: Option<String>, format: String, ctx: &AppContext) -> Result<()> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let db = &ctx.db;
let _project = db.project_repo.list(None, None).await?.into_iter().next();
let notes: Vec<String> = Vec::new();
let filtered_notes = notes;
if format == "json" {
println!("{}", serde_json::to_string_pretty(&filtered_notes)?);
} else {
if filtered_notes.is_empty() {
println!("{} No notes found matching '{}'", "ℹ️".blue(), query);
return Ok(());
}
println!(
"{} Found {} notes matching '{}'",
"🔍".cyan(),
filtered_notes.len(),
query
);
for note in filtered_notes {
println!(" Note: {}", note);
}
}
Ok(())
})
}
fn update_note(
note_id: String,
title: Option<String>,
content: Option<String>,
tags: Option<String>,
ctx: &AppContext,
) -> Result<()> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let tags_vec: Option<Vec<String>> =
tags.map(|t| t.split(',').map(|s| s.trim().to_string()).collect());
println!("{} Note {} updated", "✅".green(), note_id);
Ok(())
})
}
fn delete_note(note_id: String, force: bool, ctx: &AppContext) -> Result<()> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
if !force {
print!("Delete note {}? (y/N): ", note_id);
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
if !input.trim().to_lowercase().starts_with('y') {
println!("Cancelled");
return Ok(());
}
}
println!("{} Note {} deleted", "✅".green(), note_id);
Ok(())
})
}
fn toggle_note_pin(note_id: String, ctx: &AppContext) -> Result<()> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let is_pinned = false;
let status = if is_pinned { "pinned" } else { "unpinned" };
println!("{} Note {} {}", "✅".green(), note_id, status);
Ok(())
})
}
fn link_note_to_target(
source_note_id: String,
target_id: String,
target_type: String,
entity_type: Option<String>,
link_type: String,
ctx: &AppContext,
) -> Result<()> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let db = &ctx.db;
let _project = db.project_repo.list(None, None).await?.into_iter().next();
anyhow::bail!("Note links are not yet implemented in the new schema");
})
}
fn run_database_command(action: DatabaseAction, ctx: &AppContext) -> Result<()> {
match action {
DatabaseAction::Backup {
backup_dir,
compress,
max_backups,
} => {
create_database_backup(backup_dir, compress, max_backups, ctx)?;
}
DatabaseAction::List { backup_dir, format } => {
list_database_backups(backup_dir, format)?;
}
DatabaseAction::Restore {
backup_id,
target,
force,
} => {
restore_database_backup(backup_id, target, force, ctx)?;
}
DatabaseAction::Cleanup {
backup_dir,
max_backups,
dry_run,
} => {
cleanup_database_backups(backup_dir, max_backups, dry_run)?;
}
DatabaseAction::Health { performance } => {
check_database_health(performance, ctx)?;
}
}
Ok(())
}
fn create_database_backup(
backup_dir: Option<String>,
compress: bool,
max_backups: usize,
ctx: &AppContext,
) -> Result<()> {
use colored::*;
use wsb::persist::local::database::{create_backup, BackupConfig};
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let db_path = ctx.workspace_root.join(".wsb/wsb.db");
if !db_path.exists() {
println!(
"{} No project database found at {}",
"❌".red(),
db_path.display()
);
return Ok(());
}
let service = ctx.db_service.clone();
let mut config = BackupConfig::default();
if let Some(dir) = backup_dir {
config.backup_directory = PathBuf::from(dir);
}
config.compression_enabled = compress;
config.max_backups = max_backups;
println!("{} Creating database backup...", "⏳".yellow());
let metadata = create_backup(&service, &db_path, &config).await?;
println!("{} Database backup created successfully", "✅".green());
println!(" Backup ID: {}", metadata.backup_id);
println!(" Location: {}", metadata.backup_path.display());
println!(" Size: {} bytes", metadata.size_bytes);
println!(
" Compressed: {}",
if metadata.compression { "Yes" } else { "No" }
);
println!(" Checksum: {}", &metadata.checksum[..16]);
Ok(())
})
}
fn list_database_backups(backup_dir: Option<String>, format: String) -> Result<()> {
use colored::*;
use wsb::persist::local::database::{list_backups, BackupConfig};
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let mut config = BackupConfig::default();
if let Some(dir) = backup_dir {
config.backup_directory = PathBuf::from(dir);
}
let backups = list_backups(&config).await?;
if backups.is_empty() {
println!(
"{} No database backups found in {}",
"ℹ️".blue(),
config.backup_directory.display()
);
return Ok(());
}
match format.as_str() {
"json" => {
println!("{}", serde_json::to_string_pretty(&backups)?);
}
_ => {
println!("{} Database Backups ({} found)", "📦".blue(), backups.len());
println!();
for backup in &backups {
let size_mb = backup.size_bytes as f64 / 1024.0 / 1024.0;
let compression_info = if backup.compression {
" (compressed)"
} else {
""
};
println!(" {} {}", "ID:".bold(), backup.backup_id);
println!(
" {} {}",
"Date:".bold(),
backup.timestamp.format("%Y-%m-%d %H:%M:%S UTC")
);
println!(
" {} {}",
"Size:".bold(),
format!("{:.2} MB{}", size_mb, compression_info)
);
println!(" {} {}", "Path:".bold(), backup.backup_path.display());
println!(" {} {}", "Checksum:".bold(), &backup.checksum[..16]);
println!();
}
}
}
Ok(())
})
}
fn restore_database_backup(backup_id: String, target: Option<String>, force: bool, ctx: &AppContext) -> Result<()> {
use colored::*;
use std::io::{self, Write};
use wsb::persist::local::database::{list_backups, restore_backup, BackupConfig};
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let config = BackupConfig::default();
let backups = list_backups(&config).await?;
let backup_metadata = if backup_id.contains('/') || backup_id.contains('\\') {
backups
.iter()
.find(|b| b.backup_path.to_string_lossy().contains(&backup_id))
} else {
backups.iter().find(|b| b.backup_id == backup_id)
};
let backup_metadata =
backup_metadata.ok_or_else(|| anyhow::anyhow!("Backup not found: {}", backup_id))?;
let target_path = if let Some(target) = target {
PathBuf::from(target)
} else {
ctx.workspace_root.join(".wsb/wsb.db")
};
if target_path.exists() && !force {
print!(
"{} Database exists at {}. Overwrite? (y/N): ",
"⚠️".yellow(),
target_path.display()
);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
if !input.trim().to_lowercase().starts_with('y') {
println!("{} Restore cancelled", "❌".red());
return Ok(());
}
}
println!("{} Restoring database from backup...", "⏳".yellow());
println!(
" Backup: {} ({})",
backup_metadata.backup_id,
backup_metadata.timestamp.format("%Y-%m-%d %H:%M:%S")
);
println!(" Target: {}", target_path.display());
restore_backup(backup_metadata, &target_path).await?;
println!("{} Database restored successfully", "✅".green());
Ok(())
})
}
fn cleanup_database_backups(
backup_dir: Option<String>,
max_backups: usize,
dry_run: bool,
) -> Result<()> {
use colored::*;
use wsb::persist::local::database::{cleanup_old_backups, list_backups, BackupConfig};
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let mut config = BackupConfig::default();
if let Some(dir) = backup_dir {
config.backup_directory = PathBuf::from(dir);
}
config.max_backups = max_backups;
let backups_before = list_backups(&config).await?;
if backups_before.len() <= max_backups {
println!(
"{} No cleanup needed. {} backups found, {} allowed",
"ℹ️".blue(),
backups_before.len(),
max_backups
);
return Ok(());
}
let to_remove = backups_before.len() - max_backups;
if dry_run {
println!("{} Would remove {} old backups:", "🔍".blue(), to_remove);
for backup in backups_before.iter().skip(max_backups) {
println!(
" - {} ({})",
backup.backup_id,
backup.timestamp.format("%Y-%m-%d %H:%M:%S")
);
}
} else {
println!("{} Cleaning up {} old backups...", "🧹".yellow(), to_remove);
cleanup_old_backups(&config).await?;
println!("{} Cleanup completed", "✅".green());
}
Ok(())
})
}
fn check_database_health(performance: bool, ctx: &AppContext) -> Result<()> {
use colored::*;
use wsb::persist::local::database::{health_check, optimize_database};
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let db_path = ctx.workspace_root.join(".wsb/wsb.db");
if !db_path.exists() {
println!(
"{} No project database found at {}",
"❌".red(),
db_path.display()
);
return Ok(());
}
let service = ctx.db_service.clone();
println!("{} Checking database health...", "⏳".yellow());
let health = health_check(&service).await?;
println!("{} Database Health Report", "🏥".blue());
println!(
" {} {}",
"Connection:".bold(),
if health.connected {
"✅ OK".green()
} else {
"❌ Failed".red()
}
);
println!(
" {} {} ms",
"Response time:".bold(),
health.response_time_ms
);
println!(" {} {}", "Schema version:".bold(), health.schema_version);
println!(" {} {}", "Projects:".bold(), health.project_count);
println!(" {} {}", "Features:".bold(), health.feature_count);
println!(" {} {}", "Tasks:".bold(), health.task_count);
println!(" {} {}", "Sessions:".bold(), health.session_count);
println!(" {} {}", "Notes:".bold(), health.note_count);
println!(
" {} {}",
"FK violations:".bold(),
if health.foreign_key_violations == 0 {
format!("{} ✅", health.foreign_key_violations).green()
} else {
format!("{} ⚠️", health.foreign_key_violations).yellow()
}
);
if performance {
println!();
println!("{} Running performance optimization...", "⚡".yellow());
optimize_database(&service).await?;
println!("{} Performance optimization completed", "✅".green());
}
Ok(())
})
}
fn run_continuity_command(action: ContinuityAction, ctx: &AppContext) -> Result<()> {
match action {
ContinuityAction::Save {
session_id,
focus,
notes,
} => {
save_session_continuity_state(session_id, focus, notes, ctx)?;
}
ContinuityAction::Load { session_id, format } => {
load_session_continuity_state(session_id, format, ctx)?;
}
ContinuityAction::Transfer {
from_session,
to_session,
force,
} => {
transfer_session_continuity(from_session, to_session, force, ctx)?;
}
ContinuityAction::List { project, format } => {
list_session_continuity_states(project, format, ctx)?;
}
ContinuityAction::Snapshot { project, format } => {
create_project_context_snapshot(project, format, ctx)?;
}
}
Ok(())
}
fn save_session_continuity_state(
session_id: String,
focus: String,
notes: Option<String>,
ctx: &AppContext,
) -> Result<()> {
use colored::*;
use wsb::persist::local::database::{
create_context_snapshot, save_session_continuity, SessionContinuityState,
};
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let db_path = ctx.workspace_root.join(".wsb/wsb.db");
if !db_path.exists() {
println!(
"{} No project database found at {}",
"❌".red(),
db_path.display()
);
return Ok(());
}
let service = ctx.db_service.clone();
let db = &ctx.db;
let project = db.project_repo.list(None, None).await?.into_iter().next()
.ok_or_else(|| anyhow::anyhow!("No active project found"))?;
println!("{} Creating session continuity state...", "⏳".yellow());
let context_snapshot = create_context_snapshot(&service, &project.id).await?;
let active_features = db.feature_repo.list(None, None).await?
.into_iter()
.filter(|f| f.project_id == project.id && matches!(
f.state.as_str(),
"implemented_no_tests" | "implemented_failing_tests"
))
.map(|f| f.id)
.collect();
let in_progress_tasks = db.task_repo.list(None, None).await?
.into_iter()
.filter(|t| t.project_id == project.id && t.status == "in_progress")
.map(|t| t.id)
.collect();
let state = SessionContinuityState {
session_id: session_id.clone(),
project_id: project.id.clone(),
context_snapshot,
active_features,
in_progress_tasks,
session_focus: focus.clone(),
conversation_context: notes.unwrap_or_else(|| "Session context saved".to_string()),
working_directory: std::env::current_dir()?.to_string_lossy().to_string(),
environment_state: HashMap::new(),
timestamp: chrono::Utc::now(),
};
save_session_continuity(&service, &state).await?;
println!("{} Session continuity state saved", "✅".green());
println!(" Session ID: {}", session_id);
println!(" Project: {}", project.name);
println!(" Focus: {}", focus);
println!(" Active features: {}", state.active_features.len());
println!(" In-progress tasks: {}", state.in_progress_tasks.len());
println!(
" Recent achievements: {}",
state.context_snapshot.recent_achievements.len()
);
Ok(())
})
}
fn load_session_continuity_state(session_id: String, format: String, ctx: &AppContext) -> Result<()> {
use colored::*;
use wsb::persist::local::database::load_session_continuity;
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let db_path = ctx.workspace_root.join(".wsb/wsb.db");
if !db_path.exists() {
println!(
"{} No project database found at {}",
"❌".red(),
db_path.display()
);
return Ok(());
}
let service = ctx.db_service.clone();
let state = load_session_continuity(&service, &session_id).await?;
match state {
Some(state) => match format.as_str() {
"json" => {
println!("{}", serde_json::to_string_pretty(&state)?);
}
"detailed" => {
println!("{} Session Continuity State (Detailed)", "🔄".blue());
println!(" {} {}", "Session ID:".bold(), state.session_id);
println!(" {} {}", "Project ID:".bold(), state.project_id);
println!(" {} {}", "Focus:".bold(), state.session_focus);
println!(
" {} {}",
"Timestamp:".bold(),
state.timestamp.format("%Y-%m-%d %H:%M:%S UTC")
);
println!(" {} {}", "Working Dir:".bold(), state.working_directory);
println!();
println!("{} Context Snapshot:", "📸".blue());
println!(
" {} {}",
"Phase:".bold(),
state.context_snapshot.current_phase
);
println!(
" {} {}",
"Active features:".bold(),
state.active_features.len()
);
println!(
" {} {}",
"In-progress tasks:".bold(),
state.in_progress_tasks.len()
);
if !state.context_snapshot.recent_achievements.is_empty() {
println!();
println!(" {} Recent Achievements:", "🏆".green());
for achievement in &state.context_snapshot.recent_achievements {
println!(" - {}", achievement);
}
}
if !state.context_snapshot.active_issues.is_empty() {
println!();
println!(" {} Active Issues:", "⚠️".yellow());
for issue in &state.context_snapshot.active_issues {
println!(" - {}", issue);
}
}
if !state.context_snapshot.next_priorities.is_empty() {
println!();
println!(" {} Next Priorities:", "🎯".blue());
for priority in &state.context_snapshot.next_priorities {
println!(" - {}", priority);
}
}
}
_ => {
println!("{} Session Continuity State (Summary)", "🔄".blue());
println!(" {} {}", "Session:".bold(), state.session_id);
println!(" {} {}", "Focus:".bold(), state.session_focus);
println!(
" {} {}",
"Date:".bold(),
state.timestamp.format("%Y-%m-%d %H:%M:%S")
);
println!(
" {} {} features, {} tasks",
"Active:".bold(),
state.active_features.len(),
state.in_progress_tasks.len()
);
println!(
" {} {} achievements, {} issues, {} priorities",
"Context:".bold(),
state.context_snapshot.recent_achievements.len(),
state.context_snapshot.active_issues.len(),
state.context_snapshot.next_priorities.len()
);
}
},
None => {
println!(
"{} No continuity state found for session {}",
"ℹ️".blue(),
session_id
);
}
}
Ok(())
})
}
fn transfer_session_continuity(
from_session: String,
to_session: String,
force: bool,
ctx: &AppContext,
) -> Result<()> {
use colored::*;
use std::io::{self, Write};
use wsb::persist::local::database::transfer_session_knowledge;
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let db_path = ctx.workspace_root.join(".wsb/wsb.db");
if !db_path.exists() {
println!(
"{} No project database found at {}",
"❌".red(),
db_path.display()
);
return Ok(());
}
let service = ctx.db_service.clone();
if !force {
print!(
"{} Transfer knowledge from session {} to {}? (y/N): ",
"⚠️".yellow(),
from_session,
to_session
);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
if !input.trim().to_lowercase().starts_with('y') {
println!("{} Transfer cancelled", "❌".red());
return Ok(());
}
}
println!("{} Transferring session knowledge...", "⏳".yellow());
transfer_session_knowledge(&service, &from_session, &to_session).await?;
println!("{} Knowledge transferred successfully", "✅".green());
println!(" From: {}", from_session);
println!(" To: {}", to_session);
Ok(())
})
}
fn list_session_continuity_states(project: Option<String>, format: String, ctx: &AppContext) -> Result<()> {
use colored::*;
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let db_path = ctx.workspace_root.join(".wsb/wsb.db");
if !db_path.exists() {
println!(
"{} No project database found at {}",
"❌".red(),
db_path.display()
);
return Ok(());
}
let service = ctx.db_service.clone();
let conn = service.get_connection()?;
let sql = if project.is_some() {
"SELECT session_id, project_id, state_data, timestamp FROM session_continuity_states WHERE project_id = ? ORDER BY timestamp DESC"
} else {
"SELECT session_id, project_id, state_data, timestamp FROM session_continuity_states ORDER BY timestamp DESC"
};
let mut stmt = conn.prepare(sql)?;
let rows: Vec<(String, String, String, String)> = if let Some(ref project_id) = project {
stmt.query_map(rusqlite::params![project_id], |row| {
Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?))
})?.filter_map(|r| r.ok()).collect()
} else {
stmt.query_map([], |row| {
Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?))
})?.filter_map(|r| r.ok()).collect()
};
if rows.is_empty() {
println!("{} No session continuity states found", "ℹ️".blue());
return Ok(());
}
match format.as_str() {
"json" => {
let states: Vec<serde_json::Value> = rows
.iter()
.map(|(_, _, state_data, _)| {
serde_json::from_str(state_data)
.unwrap_or_else(|_| serde_json::Value::Null)
})
.collect();
println!("{}", serde_json::to_string_pretty(&states)?);
}
_ => {
println!(
"{} Session Continuity States ({} found)",
"🔄".blue(),
rows.len()
);
println!();
for (session_id, project_id, state_data, timestamp) in &rows {
if let Ok(state) = serde_json::from_str::<serde_json::Value>(state_data) {
let focus = state
.get("session_focus")
.and_then(|v| v.as_str())
.unwrap_or("No focus specified");
let active_features = state
.get("active_features")
.and_then(|v| v.as_array())
.map(|arr| arr.len())
.unwrap_or(0);
let in_progress_tasks = state
.get("in_progress_tasks")
.and_then(|v| v.as_array())
.map(|arr| arr.len())
.unwrap_or(0);
println!(" {} {}", "Session ID:".bold(), session_id);
println!(" {} {}", "Project:".bold(), project_id);
println!(" {} {}", "Focus:".bold(), focus);
println!(" {} {}", "Date:".bold(), timestamp);
println!(
" {} {} features, {} tasks",
"Active:".bold(),
active_features,
in_progress_tasks
);
println!();
}
}
}
}
Ok(())
})
}
fn create_project_context_snapshot(project: Option<String>, format: String, ctx: &AppContext) -> Result<()> {
use colored::*;
use wsb::persist::local::database::create_context_snapshot;
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let db_path = ctx.workspace_root.join(".wsb/wsb.db");
if !db_path.exists() {
println!(
"{} No project database found at {}",
"❌".red(),
db_path.display()
);
return Ok(());
}
let service = ctx.db_service.clone();
let db = &ctx.db;
let project_id = if let Some(pid) = project {
pid
} else {
let current_project = db.project_repo.list(None, None).await?.into_iter().next()
.ok_or_else(|| anyhow::anyhow!("No active project found"))?;
current_project.id
};
println!("{} Creating context snapshot...", "⏳".yellow());
let snapshot = create_context_snapshot(&service, &project_id).await?;
match format.as_str() {
"json" => {
println!("{}", serde_json::to_string_pretty(&snapshot)?);
}
_ => {
println!("{} Project Context Snapshot", "📸".blue());
println!(" {} {}", "Project:".bold(), project_id);
println!(" {} {}", "Phase:".bold(), snapshot.current_phase);
println!();
if !snapshot.recent_achievements.is_empty() {
println!(
" {} Recent Achievements ({}):",
"🏆".green(),
snapshot.recent_achievements.len()
);
for achievement in &snapshot.recent_achievements {
println!(" - {}", achievement);
}
println!();
}
if !snapshot.active_issues.is_empty() {
println!(
" {} Active Issues ({}):",
"⚠️".yellow(),
snapshot.active_issues.len()
);
for issue in &snapshot.active_issues {
println!(" - {}", issue);
}
println!();
}
if !snapshot.next_priorities.is_empty() {
println!(
" {} Next Priorities ({}):",
"🎯".blue(),
snapshot.next_priorities.len()
);
for priority in &snapshot.next_priorities {
println!(" - {}", priority);
}
println!();
}
if snapshot.recent_achievements.is_empty()
&& snapshot.active_issues.is_empty()
&& snapshot.next_priorities.is_empty()
{
println!(
" {} No significant context found - project may be in initial state",
"ℹ️".blue()
);
}
}
}
Ok(())
})
}
fn unlink_note(link_id: String, force: bool, ctx: &AppContext) -> Result<()> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let _service = ctx.db_service.clone();
if !force {
print!("Remove link {}? (y/N): ", link_id);
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
if !input.trim().to_lowercase().starts_with('y') {
println!("Cancelled");
return Ok(());
}
}
anyhow::bail!("Note links are not yet implemented in the new schema");
})
}
fn list_note_links(id: String, incoming: bool, outgoing: bool, format: String, ctx: &AppContext) -> Result<()> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let _service = ctx.db_service.clone();
let show_incoming = incoming || (!incoming && !outgoing);
let show_outgoing = outgoing || (!incoming && !outgoing);
let (outgoing_links, incoming_links) = if show_incoming || show_outgoing {
anyhow::bail!("Note links are not yet implemented in the new schema");
} else {
(Vec::<String>::new(), Vec::<String>::new())
};
if format == "json" {
let response = serde_json::json!({
"entity_id": id,
"outgoing_links": if show_outgoing { outgoing_links } else { Vec::new() },
"incoming_links": if show_incoming { incoming_links } else { Vec::new() }
});
println!("{}", serde_json::to_string_pretty(&response)?);
} else {
println!("{} Links for {}", "🔗".blue(), id);
if show_outgoing && !outgoing_links.is_empty() {
println!("\n{} Outgoing Links:", "→".blue());
for link in &outgoing_links {
println!(" Link: {}", link);
}
}
if show_incoming && !incoming_links.is_empty() {
println!("\n{} Incoming Links:", "←".blue());
for link in &incoming_links {
println!(" Link: {}", link);
}
}
if (show_outgoing && outgoing_links.is_empty())
&& (show_incoming && incoming_links.is_empty())
{
println!(" No links found for {}", id);
}
}
Ok(())
})
}
fn parse_note_type(type_str: &str) -> Result<String> {
Ok(type_str.to_string())
}
async fn handle_list_artifacts(
artifact_type: Option<String>,
session: Option<String>,
recent: Option<u32>,
verbose: bool,
) -> Result<()> {
println!("{}", "=== Session Artifacts ===".bold().blue());
let workspace_path = std::env::current_dir()?;
let mut artifacts = Vec::new();
let artifact_paths = [(".wsb", "workspace state")];
for (path_str, category) in artifact_paths {
let path = workspace_path.join(path_str);
if path.exists() {
collect_artifacts_recursive(&path, category, &mut artifacts)?;
}
}
if let Some(ref filter_type) = artifact_type {
artifacts.retain(|a| a.category.contains(filter_type));
}
if let Some(ref session_id) = session {
artifacts.retain(|a| {
a.session_id
.as_ref()
.map_or(false, |s| s.contains(session_id))
});
}
if let Some(days) = recent {
use std::time::SystemTime;
let cutoff = SystemTime::now() - std::time::Duration::from_secs(days as u64 * 24 * 3600);
artifacts.retain(|a| a.modified_time > cutoff);
}
if artifacts.is_empty() {
println!("No artifacts found matching criteria.");
return Ok(());
}
for artifact in &artifacts {
use chrono::{DateTime, Utc};
let modified: DateTime<Utc> = artifact.modified_time.into();
let size = format_file_size(artifact.size);
if verbose {
println!("\n{}: {}", "Artifact".bold(), artifact.path);
println!(" Category: {}", artifact.category);
println!(" Size: {}", size);
println!(" Modified: {}", modified.format("%Y-%m-%d %H:%M:%S UTC"));
if let Some(session) = &artifact.session_id {
println!(" Session: {}", session);
}
} else {
let session_info = artifact
.session_id
.as_ref()
.map(|s| format!(" [{}]", s))
.unwrap_or_default();
println!(
"{} {} {} {} {}",
modified.format("%m-%d %H:%M"),
size.cyan(),
artifact.category.yellow(),
artifact.path,
session_info.dimmed()
);
}
}
println!("\n{} artifacts found.", artifacts.len());
Ok(())
}
async fn handle_organize_artifacts(
categorize: bool,
manifest: bool,
tag: bool,
) -> anyhow::Result<()> {
use chrono::{DateTime, Utc};
use std::fs;
let workspace_path = std::env::current_dir()?;
if categorize {
let categories = ["logs", "generated", "diagrams", "exports", "archives"];
let artifacts_dir = workspace_path.join(".wsb/artifacts");
for category in categories {
let category_path = artifacts_dir.join(category);
fs::create_dir_all(&category_path)?;
}
}
if manifest {
let mut artifacts = Vec::new();
collect_artifacts_recursive(&workspace_path.join(".wsb"), "workspace", &mut artifacts)?;
if workspace_path.join("generated").exists() {
collect_artifacts_recursive(
&workspace_path.join("generated"),
"generated",
&mut artifacts,
)?;
}
let manifest_path = workspace_path.join(".wsb/artifact_manifest.json");
let manifest_data = serde_json::json!({
"generated_at": chrono::Utc::now().to_rfc3339(),
"total_artifacts": artifacts.len(),
"artifacts": artifacts.iter().map(|a| serde_json::json!({
"path": a.path,
"category": a.category,
"size": a.size,
"modified": Into::<DateTime<Utc>>::into(a.modified_time).to_rfc3339(),
"session_id": a.session_id
})).collect::<Vec<_>>()
});
fs::write(
&manifest_path,
serde_json::to_string_pretty(&manifest_data)?,
)?;
}
if tag {}
Ok(())
}
async fn handle_search_artifacts(
query: &str,
content: bool,
names: bool,
limit: u32,
) -> anyhow::Result<()> {
let workspace_path = std::env::current_dir()?;
let mut results = Vec::new();
let search_paths = [
".wsb",
"generated",
"internal/diagrams/generated",
"internal/archive",
];
for path_str in search_paths {
let path = workspace_path.join(path_str);
if path.exists() {
search_artifacts_recursive(&path, query, content, names, &mut results)?;
}
}
results.truncate(limit as usize);
if results.is_empty() {
return Ok(());
}
for result in results {}
Ok(())
}
async fn handle_show_artifact(
artifact_path: &str,
content: bool,
metadata: bool,
) -> anyhow::Result<()> {
use chrono::{DateTime, Utc};
use std::fs;
let path = PathBuf::from(artifact_path);
if !path.exists() {
return Ok(());
}
if metadata {
let meta = fs::metadata(&path)?;
let modified: DateTime<Utc> = meta.modified()?.into();
if meta.is_file() {
if let Some(extension) = path.extension() {}
}
}
if content && path.is_file() {
println!("{}", "=== Content ===".bold());
match fs::read_to_string(&path) {
Ok(file_content) => {
if file_content.len() > 10000 {
println!(
"{}\n[Content truncated - {} total characters]",
&file_content[..10000],
file_content.len()
);
} else {
println!("{}", file_content);
}
}
Err(e) => match fs::read(&path) {
Ok(binary_content) => {
println!("[Binary file - {} bytes]", binary_content.len());
}
Err(_) => {
println!("Error reading file: {}", e);
}
},
}
}
Ok(())
}
async fn handle_export_artifacts(
sessions: &[String],
format: &str,
output: Option<String>,
include_content: bool,
) -> anyhow::Result<()> {
Ok(())
}
async fn handle_archive_artifacts(
session_id: &str,
format: &str,
output: Option<String>,
remove_originals: bool,
) -> anyhow::Result<()> {
Ok(())
}
fn handle_code_command(action: CodeAction) -> Result<()> {
use wsb::code_analysis::{
search::{AstSearchEngine, SearchOptions},
transform::{AstTransformEngine, CommonTransforms, TransformOptions, TransformRule},
SupportedLanguage,
};
match action {
CodeAction::Tree {
depth,
hidden,
sizes,
extensions,
no_ignore,
} => {
show_codebase_tree(depth, hidden, sizes, extensions, no_ignore)?;
}
CodeAction::Search {
pattern,
files,
language,
context,
max_matches,
format,
} => {
let lang = language.and_then(|l| match l.as_str() {
"rust" => Some(SupportedLanguage::Rust),
"javascript" | "js" => Some(SupportedLanguage::JavaScript),
"typescript" | "ts" => Some(SupportedLanguage::TypeScript),
"python" | "py" => Some(SupportedLanguage::Python),
"go" => Some(SupportedLanguage::Go),
"java" => Some(SupportedLanguage::Java),
"c" => Some(SupportedLanguage::C),
"cpp" | "c++" => Some(SupportedLanguage::Cpp),
_ => None,
});
let options = SearchOptions {
pattern,
language: lang,
include_context: context > 0,
context_lines: context,
max_matches: Some(max_matches),
..Default::default()
};
let engine = AstSearchEngine::new(options);
let results = engine.search_files(&files)?;
match format.as_str() {
"json" => {
println!("{}", serde_json::to_string_pretty(&results)?);
}
_ => {
for (file_path, matches) in results {
println!("\n{}:", file_path.display().to_string().bright_blue());
for search_match in matches {
println!(
" {}:{} - {}",
search_match.line.to_string().yellow(),
search_match.column.to_string().yellow(),
search_match.matched_text.trim()
);
if !search_match.context_before.is_empty() {
for line in search_match.context_before.lines() {
println!(" {}", line.dimmed());
}
}
if !search_match.context_after.is_empty() {
for line in search_match.context_after.lines() {
println!(" {}", line.dimmed());
}
}
}
}
}
}
}
CodeAction::Transform {
pattern,
replacement,
files,
language,
dry_run,
no_backup,
max_changes,
} => {
let lang = language
.and_then(|l| match l.as_str() {
"rust" => Some(SupportedLanguage::Rust),
"javascript" | "js" => Some(SupportedLanguage::JavaScript),
"typescript" | "ts" => Some(SupportedLanguage::TypeScript),
"python" | "py" => Some(SupportedLanguage::Python),
"go" => Some(SupportedLanguage::Go),
"java" => Some(SupportedLanguage::Java),
"c" => Some(SupportedLanguage::C),
"cpp" | "c++" => Some(SupportedLanguage::Cpp),
_ => None,
})
.unwrap_or(SupportedLanguage::Rust);
let options = TransformOptions {
dry_run,
backup_files: !no_backup,
max_changes_per_file: Some(max_changes),
..Default::default()
};
let rule = TransformRule {
name: "user_transform".to_string(),
pattern,
replacement,
language: lang,
};
let engine = AstTransformEngine::new(options);
let results = engine.transform_files(&files, &rule)?;
for result in results {
if result.successful {
println!(
"{}: {} changes applied",
result.file_path.display().to_string().green(),
result.changes_made.to_string().yellow()
);
if dry_run {
println!(" (dry run - no files modified)");
}
} else {
println!(
"{}: failed - {}",
result.file_path.display().to_string().red(),
result.error_message.unwrap_or_default()
);
}
}
}
CodeAction::Patterns { language, category } => {
let lang = match language.as_str() {
"rust" => SupportedLanguage::Rust,
"javascript" | "js" => SupportedLanguage::JavaScript,
"typescript" | "ts" => SupportedLanguage::TypeScript,
"python" | "py" => SupportedLanguage::Python,
"go" => SupportedLanguage::Go,
"java" => SupportedLanguage::Java,
"c" => SupportedLanguage::C,
"cpp" | "c++" => SupportedLanguage::Cpp,
_ => {
eprintln!("Unsupported language: {}", language);
return Ok(());
}
};
println!("Common {} patterns for {}:", category, language);
if category == "transform" {
let transforms = CommonTransforms::for_language(lang);
for transform in transforms {
println!(
" {}: {} -> {}",
transform.name.bright_blue(),
transform.pattern.yellow(),
transform.replacement.green()
);
}
} else {
println!("Search patterns will be available in full implementation");
}
}
CodeAction::Analyze {
files,
language: _language,
analysis_type,
format,
} => {
println!(
"Code Analysis ({}): analyzing {} files",
analysis_type,
files.len()
);
for file in files {
if let Ok(content) = std::fs::read_to_string(&file) {
let lines = content.lines().count();
let chars = content.len();
match format.as_str() {
"json" => {
println!(
"{{\"file\": \"{}\", \"lines\": {}, \"chars\": {}}}",
file.display(),
lines,
chars
);
}
_ => {
println!(
"{}: {} lines, {} characters",
file.display().to_string().bright_blue(),
lines.to_string().yellow(),
chars.to_string().yellow()
);
}
}
} else {
println!("{}: could not read file", file.display().to_string().red());
}
}
}
}
Ok(())
}
fn show_codebase_tree(
depth: usize,
show_hidden: bool,
show_sizes: bool,
extensions_filter: Option<String>,
no_ignore: bool,
) -> Result<()> {
use colored::Colorize;
use ignore::gitignore::GitignoreBuilder;
let current_dir = std::env::current_dir()?;
let project_root = find_project_root(¤t_dir);
println!("{}", "📁 Codebase Structure".bright_blue().bold());
println!(
"{} {}",
"Project Root:".bright_green(),
project_root.display()
);
println!(
"{} {}",
"Current Location:".bright_yellow(),
current_dir.display()
);
if current_dir != project_root {
let relative_path = current_dir
.strip_prefix(&project_root)
.unwrap_or(¤t_dir);
println!(
"{} {}",
"Relative Path:".bright_cyan(),
relative_path.display()
);
}
println!();
let extensions: Option<Vec<String>> = extensions_filter.map(|ext_str| {
ext_str
.split(',')
.map(|s| s.trim().to_lowercase())
.collect()
});
let gitignore = if no_ignore {
None
} else {
let mut builder = GitignoreBuilder::new(&project_root);
let _ = builder.add(&project_root.join(".gitignore"));
builder.build().ok()
};
display_tree(
&project_root,
"",
depth,
0,
show_hidden,
show_sizes,
&extensions,
&gitignore,
)?;
Ok(())
}
fn show_interactive_codebase_tree(
depth: usize,
show_hidden: bool,
show_sizes: bool,
extensions_filter: Option<String>,
no_ignore: bool,
) -> Result<()> {
use wsb::interactive_tree::InteractiveTree;
let current_dir = std::env::current_dir()?;
let project_root = find_project_root(¤t_dir);
println!(
"{}",
"🌳 Interactive Codebase Navigator".bright_blue().bold()
);
println!(
"{} {}",
"Project Root:".bright_green(),
project_root.display()
);
println!();
println!(
"{}",
"Loading interactive tree... Press 'q' to exit when ready.".dimmed()
);
std::thread::sleep(std::time::Duration::from_millis(1500));
let max_depth = if depth > 0 { Some(depth) } else { None };
let mut tree = InteractiveTree::new(&project_root, max_depth, show_hidden)?;
tree.set_callback(|selected_paths| {
if !selected_paths.is_empty() {
println!("\n🎯 Selected items:");
for path in selected_paths {
println!(" • {}", path.display().to_string().bright_cyan());
}
println!(
"\n{}",
"✓ Callback executed! Press any key to continue...".bright_green()
);
use std::io::Read;
let mut buffer = [0; 1];
let _ = std::io::stdin().read(&mut buffer);
} else {
println!("\n{}", "No items selected.".yellow());
}
Ok(())
});
tree.run()?;
println!("\n{}", "Interactive navigation completed.".bright_green());
Ok(())
}
fn find_project_root(current: &Path) -> std::path::PathBuf {
let mut path = current.to_path_buf();
let project_markers = [
"Cargo.toml",
"package.json",
"pyproject.toml",
"setup.py",
"composer.json",
"pom.xml",
"build.gradle",
"CMakeLists.txt",
".git",
".svn",
".hg",
"Makefile",
"go.mod",
];
loop {
for marker in &project_markers {
if path.join(marker).exists() {
return path;
}
}
if !path.pop() {
break;
}
}
current.to_path_buf()
}
fn display_tree(
dir: &Path,
prefix: &str,
max_depth: usize,
current_depth: usize,
show_hidden: bool,
show_sizes: bool,
extensions: &Option<Vec<String>>,
gitignore: &Option<ignore::gitignore::Gitignore>,
) -> Result<()> {
if current_depth >= max_depth {
return Ok(());
}
let entries = fs::read_dir(dir)?;
let mut entries: Vec<_> = entries.collect::<Result<Vec<_>, _>>()?;
if let Some(ref gi) = gitignore {
entries.retain(|entry| {
let path = entry.path();
let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
let matched = gi.matched(&path, is_dir);
!matched.is_ignore()
});
}
entries.sort_by_key(|entry| {
let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
(!is_dir, entry.file_name())
});
let total_entries = entries.len();
for (index, entry) in entries.iter().enumerate() {
let file_name = entry.file_name();
let file_name_str = file_name.to_string_lossy();
let path = entry.path();
let is_last = index == total_entries - 1;
if !show_hidden && file_name_str.starts_with('.') {
continue;
}
let is_dir = entry.file_type()?.is_dir();
let connector = if is_last { "└── " } else { "├── " };
let new_prefix = if is_last { " " } else { "│ " };
if !is_dir {
if let Some(ref exts) = extensions {
if let Some(ext) = path.extension() {
let ext_str = ext.to_string_lossy().to_lowercase();
if !exts.contains(&ext_str) {
continue;
}
} else if !exts.is_empty() {
continue;
}
}
}
print!("{}{}", prefix, connector);
if is_dir {
print!("{}", file_name_str.bright_blue().bold());
} else {
let colored_name = match path.extension().and_then(|s| s.to_str()) {
Some("rs") => file_name_str.bright_red(),
Some("py") => file_name_str.bright_yellow(),
Some("js" | "ts" | "jsx" | "tsx") => file_name_str.bright_green(),
Some("json" | "yaml" | "yml" | "toml") => file_name_str.bright_cyan(),
Some("md" | "txt" | "doc") => file_name_str.white(),
Some("sh" | "bash" | "zsh") => file_name_str.bright_magenta(),
_ => file_name_str.normal(),
};
print!("{}", colored_name);
}
if show_sizes && !is_dir {
if let Ok(metadata) = entry.metadata() {
let size = format_file_size(metadata.len());
print!(" {}", size.dimmed());
}
}
println!();
if is_dir && current_depth + 1 < max_depth {
let next_prefix = format!("{}{}", prefix, new_prefix);
display_tree(
&path,
&next_prefix,
max_depth,
current_depth + 1,
show_hidden,
show_sizes,
extensions,
gitignore,
)?;
}
}
Ok(())
}
#[derive(Debug)]
struct ArtifactInfo {
path: String,
category: String,
size: u64,
modified_time: std::time::SystemTime,
session_id: Option<String>,
description: Option<String>,
}
#[derive(Debug)]
struct SearchResult {
path: String,
match_info: String,
}
fn collect_artifacts_recursive(
dir: &Path,
category: &str,
artifacts: &mut Vec<ArtifactInfo>,
) -> anyhow::Result<()> {
use std::fs;
if !dir.exists() || !dir.is_dir() {
return Ok(());
}
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
let metadata = entry.metadata()?;
if path.is_file() {
let session_id = extract_session_id_from_path(&path);
artifacts.push(ArtifactInfo {
path: path.display().to_string(),
category: category.to_string(),
size: metadata.len(),
modified_time: metadata.modified()?,
session_id,
description: None,
});
} else if path.is_dir() {
collect_artifacts_recursive(&path, category, artifacts)?;
}
}
Ok(())
}
fn format_file_size(bytes: u64) -> String {
const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
let mut size = bytes as f64;
let mut unit_index = 0;
while size >= 1024.0 && unit_index < UNITS.len() - 1 {
size /= 1024.0;
unit_index += 1;
}
if unit_index == 0 {
format!("{} {}", bytes, UNITS[unit_index])
} else {
format!("{:.1} {}", size, UNITS[unit_index])
}
}
fn extract_session_id_from_path(_path: &Path) -> Option<String> {
None
}
fn find_old_files(
dir: &Path,
cutoff: std::time::SystemTime,
files: &mut Vec<PathBuf>,
) -> anyhow::Result<()> {
use std::fs;
if !dir.exists() || !dir.is_dir() {
return Ok(());
}
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
let metadata = entry.metadata()?;
if metadata.modified()? < cutoff {
files.push(path);
}
} else if path.is_dir() {
find_old_files(&path, cutoff, files)?;
}
}
Ok(())
}
fn search_artifacts_recursive(
dir: &Path,
query: &str,
content: bool,
names: bool,
results: &mut Vec<SearchResult>,
) -> anyhow::Result<()> {
use std::fs;
if !dir.exists() || !dir.is_dir() {
return Ok(());
}
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
let path_str = path.display().to_string();
let mut matches = Vec::new();
if names || !content {
if path_str.contains(query) {
matches.push("filename match".to_string());
}
}
if content {
if let Ok(file_content) = fs::read_to_string(&path) {
if file_content.contains(query) {
matches.push("content match".to_string());
}
}
}
if !matches.is_empty() {
results.push(SearchResult {
path: path_str,
match_info: matches.join(", "),
});
}
} else if path.is_dir() {
search_artifacts_recursive(&path, query, content, names, results)?;
}
}
Ok(())
}
fn run_version_command(action: VersionAction, ctx: &AppContext) -> Result<()> {
match action {
VersionAction::Show { verbose, format } => handle_version_show(verbose, format, ctx),
VersionAction::Major { version } => handle_version_major(version, ctx),
VersionAction::Tag { prefix, message } => handle_version_tag(prefix, message, ctx),
VersionAction::Info { include_history } => handle_version_info(include_history, ctx),
}
}
fn handle_version_show(verbose: bool, format: String, ctx: &AppContext) -> Result<()> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let db = &ctx.db;
let service = ctx.db_service.clone();
let project = db.project_repo.list(None, None).await?.into_iter().next();
let major_version = get_project_major_version(&service)?;
let version_info = wsb::state::VersionInfo::calculate_with_major(major_version)?;
match format.as_str() {
"json" => {
let json_output = if verbose {
let calc_info = wsb::state::VersionInfo::get_calculation_info(major_version)?;
serde_json::json!({
"version": version_info.full_version,
"major": major_version,
"minor": version_info.minor_version,
"patch": version_info.patch_version,
"project": project.map(|p| p.name.clone()).unwrap_or_else(|| "Unknown".to_string()),
"calculation": {
"total_commits": calc_info.total_commits,
"changes_since_release": calc_info.changes_since_release,
"last_release_tag": calc_info.last_release_tag,
"method": calc_info.calculation_method
}
})
} else {
serde_json::json!({
"version": version_info.full_version,
"major": major_version,
"minor": version_info.minor_version,
"patch": version_info.patch_version,
"project": project.map(|p| p.name.clone()).unwrap_or_else(|| "Unknown".to_string())
})
};
println!("{}", serde_json::to_string_pretty(&json_output)?);
}
_ => {
println!("{} {}", "Version:".blue().bold(), version_info.full_version.green().bold());
let project_name = project.map(|p| p.name.clone()).unwrap_or_else(|| "Unknown".to_string());
println!("{} {}", "Project:".blue(), project_name);
if verbose {
let calc_info = wsb::state::VersionInfo::get_calculation_info(major_version)?;
println!("\n{}", "Calculation Breakdown:".blue().bold());
println!(" {} {}", "Major (DB):".blue(), major_version.to_string().yellow());
println!(" {} {}", "Minor (commits):".blue(), version_info.minor_version.to_string().yellow());
println!(" {} {}", "Patch (changes):".blue(), version_info.patch_version.to_string().yellow());
if let Some(tag) = calc_info.last_release_tag {
println!(" {} {}", "Last release tag:".blue(), tag.green());
} else {
println!(" {} {}", "Last release tag:".blue(), "none".yellow());
}
println!(" {} {}", "Method:".blue(), calc_info.calculation_method.cyan());
}
}
}
anyhow::Ok(())
})
}
fn handle_version_major(version: u32, ctx: &AppContext) -> Result<()> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let service = ctx.db_service.clone();
let conn = service.get_connection()?;
conn.execute(
"UPDATE projects SET major_version = ?, updated_at = datetime('now') WHERE id = (SELECT id FROM projects LIMIT 1)",
rusqlite::params![version as i64],
)?;
let version_info = wsb::state::VersionInfo::calculate_with_major(version)?;
info!("Major version set to {}, new version: {}", version, version_info.full_version);
println!("{} Major version set to {}", "✅".green(), version.to_string().green().bold());
println!("{} New version: {}", "🔢".blue(), version_info.full_version.green().bold());
info!("Version set successfully - minor/patch calculated from git state");
println!("\n{} {}", "Note:".blue(), "Minor/patch are calculated from git state");
println!("{} Use 'wsb version tag' to create a release tag", "💡".yellow());
anyhow::Ok(())
})
}
fn handle_version_tag(prefix: String, message: Option<String>, ctx: &AppContext) -> Result<()> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let service = ctx.db_service.clone();
let major_version = get_project_major_version(&service)?;
let version_info = wsb::state::VersionInfo::calculate_with_major(major_version)?;
let tag_name = format!("{}{}", prefix, version_info.full_version);
let tag_message =
message.unwrap_or_else(|| format!("Release version {}", version_info.full_version));
let cwd = std::env::current_dir().context("Failed to determine current directory")?;
let repo = wsb::git_backend::discover_repo(&cwd)?;
wsb::git_backend::create_tag(&repo, &tag_name, &tag_message)?;
info!("Created git tag: {}", tag_name);
println!(
"{} Created git tag: {}",
"✅".green(),
tag_name.green().bold()
);
println!("{} Message: {}", "📝".blue(), tag_message);
println!(
"\n{} To push tag to remote: git push origin {}",
"💡".yellow(),
tag_name
);
anyhow::Ok(())
})
}
fn handle_version_info(include_history: bool, ctx: &AppContext) -> Result<()> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let service = ctx.db_service.clone();
let major_version = get_project_major_version(&service)?;
let calc_info = wsb::state::VersionInfo::get_calculation_info(major_version)?;
let version_info = wsb::state::VersionInfo::calculate_with_major(major_version)?;
println!("{}", "Version Calculation Information".blue().bold());
println!("=====================================");
println!();
println!("{}", "Current Version Breakdown:".green().bold());
println!(
" {} {}.{}.{}",
"Version:".blue(),
major_version.to_string().green(),
version_info.minor_version.to_string().yellow(),
version_info.patch_version.to_string().cyan()
);
println!(
" {} {} (stored in database)",
"Major:".blue(),
major_version.to_string().green()
);
println!(
" {} {} (total git commits)",
"Minor:".blue(),
version_info.minor_version.to_string().yellow()
);
println!(
" {} {} (changes since last v{}.* tag)",
"Patch:".blue(),
version_info.patch_version.to_string().cyan(),
major_version
);
println!();
println!("{}", "Git Integration:".green().bold());
if let Some(ref git_root) = calc_info.git_root {
println!(
" {} {}",
"Git Root:".blue(),
git_root.display().to_string().cyan()
);
}
if let Some(ref tag) = calc_info.last_release_tag {
println!(" {} {}", "Last Release Tag:".blue(), tag.green());
} else {
println!(" {} {}", "Last Release Tag:".blue(), "none found".yellow());
}
println!();
println!("{}", "Commands:".green().bold());
println!(
" {} Set major version",
format!("wsb version major <number>").cyan()
);
println!(" {} Create release tag", "wsb version tag".cyan());
println!(" {} Show current version", "wsb version show".cyan());
println!();
if include_history {
println!("{}", "Git History Analysis:".green().bold());
let cwd = std::env::current_dir().context("Failed to determine current directory")?;
let repo = wsb::git_backend::discover_repo(&cwd)?;
let tags = wsb::git_backend::list_tags(&repo)?;
if tags.is_empty() {
println!(" {} {}", "Recent Tags:".blue(), "none found".yellow());
} else {
println!(" {}:", "Recent Tags".blue());
for (i, tag) in tags.iter().take(5).enumerate() {
println!(" {}. {}", i + 1, tag.green());
}
}
println!();
}
anyhow::Ok(())
})
}
fn get_project_major_version(service: &wsb::persist::global::DatabaseService) -> Result<u32> {
let conn = service.get_connection()?;
let result: Option<i64> = conn
.query_row("SELECT major_version FROM projects LIMIT 1", [], |row| row.get(0))
.ok();
if let Some(version) = result {
Ok(version as u32)
} else {
Ok(0)
}
}
fn handle_test_command(dry_run: bool, install: bool, args: Vec<String>, project_root: &Path) -> Result<()> {
use wsb::state::detect_project_files;
let project_files = detect_project_files(&project_root)?;
if project_files.is_empty() {
println!(
"{} No project files detected in current directory",
"⚠️".yellow()
);
println!("{} Supported project types: Rust (Cargo.toml), Node.js (package.json), Python (pyproject.toml), PHP (composer.json), Dart (pubspec.yaml), Java (pom.xml, build.gradle), C++ (CMakeLists.txt)", "💡".blue());
return Ok(());
}
let (test_cmd, test_args, description) = determine_test_command(&project_files)?;
println!("{} Detected project: {}", "🔍".blue(), description);
println!(
"{} Test command: {} {}",
"🚀".green(),
test_cmd,
test_args.join(" ")
);
if !args.is_empty() {
println!("{} Additional args: {}", "📋".blue(), args.join(" "));
}
if dry_run {
println!("{} Dry run - would execute:", "👀".cyan());
let mut full_args = test_args.clone();
full_args.extend(args);
println!(" {} {}", test_cmd, full_args.join(" "));
return Ok(());
}
if install {
ensure_test_runner_available(&test_cmd, &project_files)?;
}
let mut full_args = test_args.clone();
full_args.extend(args);
println!("{} Running tests...", "🧪".green());
let mut command = Command::new(&test_cmd);
command.args(&full_args);
let status = command.status()?;
if status.success() {
println!("{} Tests completed successfully!", "✅".green());
} else {
println!(
"{} Tests failed with exit code: {}",
"❌".red(),
status.code().unwrap_or(-1)
);
std::process::exit(status.code().unwrap_or(1));
}
Ok(())
}
fn determine_test_command(
project_files: &[wsb::state::ProjectFile],
) -> Result<(String, Vec<String>, String)> {
use wsb::state::ProjectFileType;
for project_file in project_files {
match project_file.file_type {
ProjectFileType::CargoToml => {
return Ok((
"cargo".to_string(),
vec![
"nextest".to_string(),
"run".to_string(),
"--no-fail-fast".to_string(),
],
"Rust (Cargo.toml)".to_string(),
));
}
ProjectFileType::PackageJson => {
return Ok((
"npm".to_string(),
vec!["test".to_string()],
"Node.js (package.json)".to_string(),
));
}
ProjectFileType::PyprojectToml => {
return Ok((
"pytest".to_string(),
vec![],
"Python (pyproject.toml)".to_string(),
));
}
ProjectFileType::SetupPy => {
return Ok((
"python".to_string(),
vec!["-m".to_string(), "pytest".to_string()],
"Python (setup.py)".to_string(),
));
}
ProjectFileType::ComposerJson => {
return Ok((
"vendor/bin/phpunit".to_string(),
vec![],
"PHP (composer.json)".to_string(),
));
}
ProjectFileType::PubspecYaml => {
return Ok((
"flutter".to_string(),
vec!["test".to_string()],
"Dart/Flutter (pubspec.yaml)".to_string(),
));
}
ProjectFileType::PomXml => {
return Ok((
"mvn".to_string(),
vec!["test".to_string()],
"Java Maven (pom.xml)".to_string(),
));
}
ProjectFileType::BuildGradle => {
return Ok((
"./gradlew".to_string(),
vec!["test".to_string()],
"Java Gradle (build.gradle)".to_string(),
));
}
ProjectFileType::BuildGradleKts => {
return Ok((
"./gradlew".to_string(),
vec!["test".to_string()],
"Kotlin Gradle (build.gradle.kts)".to_string(),
));
}
ProjectFileType::CMakeLists => {
return Ok((
"ctest".to_string(),
vec!["--output-on-failure".to_string()],
"C/C++ (CMakeLists.txt)".to_string(),
));
}
ProjectFileType::PackageSwift => {
return Ok((
"swift".to_string(),
vec!["test".to_string()],
"Swift (Package.swift)".to_string(),
));
}
ProjectFileType::Gemspec => {
return Ok((
"bundle".to_string(),
vec!["exec".to_string(), "rspec".to_string()],
"Ruby (gemspec)".to_string(),
));
}
ProjectFileType::Csproj => {
return Ok((
"dotnet".to_string(),
vec!["test".to_string()],
".NET (csproj)".to_string(),
));
}
ProjectFileType::GoMod => {
return Ok((
"go".to_string(),
vec!["test".to_string(), "./...".to_string()],
"Go (go.mod)".to_string(),
));
}
ProjectFileType::MixExs => {
return Ok((
"mix".to_string(),
vec!["test".to_string()],
"Elixir (mix.exs)".to_string(),
));
}
ProjectFileType::BuildSbt => {
return Ok((
"sbt".to_string(),
vec!["test".to_string()],
"Scala (build.sbt)".to_string(),
));
}
ProjectFileType::ShardYml => {
return Ok((
"crystal".to_string(),
vec!["spec".to_string()],
"Crystal (shard.yml)".to_string(),
));
}
ProjectFileType::JuliaProject => {
return Ok((
"julia".to_string(),
vec![
"--project=.".to_string(),
"-e".to_string(),
"using Pkg; Pkg.test()".to_string(),
],
"Julia (Project.toml)".to_string(),
));
}
}
}
anyhow::bail!("No supported test runner found for detected project files")
}
fn ensure_test_runner_available(
test_cmd: &str,
project_files: &[wsb::state::ProjectFile],
) -> Result<()> {
use wsb::state::ProjectFileType;
let check_status = Command::new("which").arg(test_cmd).output();
if let Ok(output) = check_status {
if output.status.success() {
println!(
"{} Test runner {} is already available",
"✅".green(),
test_cmd
);
return Ok(());
}
}
for project_file in project_files {
match project_file.file_type {
ProjectFileType::CargoToml if test_cmd.contains("nextest") => {
println!("{} Installing cargo-nextest...", "📦".blue());
let status = Command::new("cargo")
.args(["install", "cargo-nextest", "--locked"])
.status()?;
if status.success() {
println!("{} Successfully installed cargo-nextest", "✅".green());
} else {
println!(
"{} Failed to install cargo-nextest, falling back to standard cargo test",
"⚠️".yellow()
);
}
return Ok(());
}
ProjectFileType::PyprojectToml | ProjectFileType::SetupPy
if test_cmd.contains("pytest") =>
{
println!("{} Installing pytest...", "📦".blue());
let status = Command::new("pip").args(["install", "pytest"]).status()?;
if status.success() {
println!("{} Successfully installed pytest", "✅".green());
} else {
println!("{} Failed to install pytest", "❌".red());
anyhow::bail!("Could not install pytest");
}
return Ok(());
}
_ => {}
}
}
println!(
"{} Test runner {} not found, please install it manually",
"⚠️".yellow(),
test_cmd
);
Ok(())
}