1mod budget;
2mod compatibility;
3mod context;
4mod daemon;
5mod dashboard;
6mod demo;
7mod duration;
8mod engine;
9mod guided;
10mod importing;
11mod installation;
12mod issue;
13mod launch;
14mod native;
15mod observability;
16mod operational;
17mod parallel;
18mod planning;
19mod project;
20mod publication;
21mod review;
22mod service;
23mod snapshots;
24mod templates;
25mod terminal_chat;
26mod validation_cache;
27mod workbench;
28mod workbench_http;
29use anyhow::Result;
30use clap::{CommandFactory, FromArgMatches, Parser, Subcommand};
31use forge_adapters::process;
32use serde_json::{Value, json};
33use std::path::{Path, PathBuf};
34use tokio::sync::watch;
35
36#[derive(Parser)]
37#[command(
38 name = "illumitech-forge",
39 version,
40 about = "Releases com evidências verificáveis / Evidence-backed releases"
41)]
42struct Cli {
43 #[arg(long, global = true)]
44 json: bool,
45 #[arg(long, global = true, hide = true)]
46 service_logging: bool,
47 #[arg(long, global = true, default_value = ".forge-state")]
48 state_dir: PathBuf,
49 #[command(subcommand)]
50 command: Command,
51}
52#[derive(Subcommand)]
53enum Command {
54 Contracts,
56 Installation {
58 #[command(subcommand)]
59 command: installation::Command,
60 },
61 Demo {
63 #[command(subcommand)]
64 command: demo::Command,
65 },
66 State {
68 #[command(subcommand)]
69 command: snapshots::Command,
70 },
71 Issue {
73 #[command(subcommand)]
74 command: issue::Command,
75 },
76 Review {
78 #[command(subcommand)]
79 command: ReviewCommand,
80 },
81 Templates {
82 #[command(subcommand)]
83 command: templates::Command,
84 },
85 Budget {
86 #[command(subcommand)]
87 command: BudgetCommand,
88 },
89 Workbench {
91 #[arg(long)]
92 root: PathBuf,
93 #[arg(long, default_value_t = 0)]
94 port: u16,
95 #[arg(long)]
96 app_server: Option<PathBuf>,
97 #[arg(long)]
98 manifest: Option<PathBuf>,
99 },
100 Chat {
102 #[arg(long, default_value = ".")]
103 root: PathBuf,
104 #[arg(long)]
105 app_server: Option<PathBuf>,
106 },
107 Feature {
108 #[command(subcommand)]
109 command: FeatureCommand,
110 },
111 Plan {
112 #[command(subcommand)]
113 command: PlanCommand,
114 },
115 #[command(hide = true)]
117 ServiceHost {
118 #[arg(long)]
119 releases_dir: PathBuf,
120 },
121 Service(service::Options),
123 Dashboard {
125 #[arg(long, default_value_t = 0)]
126 port: u16,
127 },
128 Daemon {
129 #[arg(long)]
130 releases_dir: PathBuf,
131 #[arg(long)]
132 once: bool,
133 #[arg(long, default_value_t = 60)]
134 interval_seconds: u64,
135 #[arg(long)]
136 dry_run: bool,
137 },
138 DaemonControl {
139 #[command(subcommand)]
140 command: DaemonControl,
141 },
142 Init {
143 #[arg(default_value = ".")]
144 path: PathBuf,
145 #[arg(long)]
147 memory_workspace: Option<String>,
148 },
149 Doctor,
150 Spec {
151 #[command(subcommand)]
152 command: SpecCommand,
153 },
154 Release {
155 #[command(subcommand)]
156 command: ReleaseCommand,
157 },
158 Status {
159 manifest: PathBuf,
160 },
161 Report {
162 manifest: PathBuf,
163 #[arg(long, default_value_t = 100)]
164 limit: u32,
165 #[arg(long, default_value_t = 0)]
166 offset: u64,
167 },
168 Context {
169 #[command(subcommand)]
170 command: ContextCommand,
171 },
172 Pause {
173 manifest: PathBuf,
174 },
175 Cancel {
176 manifest: PathBuf,
177 },
178 Resume {
179 manifest: PathBuf,
180 #[arg(long)]
181 reconcile: Option<String>,
182 },
183 Memory {
184 #[command(subcommand)]
185 command: MemoryCommand,
186 },
187 Tools {
188 #[command(subcommand)]
189 command: ToolsCommand,
190 },
191 Eval {
192 #[command(subcommand)]
193 command: EvalCommand,
194 },
195}
196#[derive(Subcommand)]
197enum ReviewCommand {
198 Plan {
199 manifest: PathBuf,
200 },
201 Run {
202 manifest: PathBuf,
203 #[arg(long)]
204 app_server: Option<PathBuf>,
205 },
206}
207#[derive(Subcommand)]
208enum BudgetCommand {
209 Inspect { manifest: PathBuf },
210}
211#[derive(Subcommand)]
212enum FeatureCommand {
213 New {
214 #[arg(long)]
215 root: PathBuf,
216 #[arg(long)]
217 id: String,
218 #[arg(long)]
219 title: String,
220 #[arg(long)]
221 ops_root: PathBuf,
222 #[arg(long, conflicts_with = "apply")]
223 dry_run: bool,
224 #[arg(long)]
225 apply: bool,
226 },
227}
228#[derive(Subcommand)]
229enum PlanCommand {
230 Propose {
231 #[arg(long)]
232 root: PathBuf,
233 #[arg(long)]
234 request: String,
235 #[arg(long)]
236 ops_root: PathBuf,
237 #[arg(long)]
238 output: PathBuf,
239 #[arg(long)]
240 dry_run: bool,
241 #[arg(long)]
242 app_server: Option<PathBuf>,
243 #[arg(long)]
244 previous: Option<PathBuf>,
245 #[arg(long)]
247 manifest: Option<PathBuf>,
248 },
249 Validate {
250 path: PathBuf,
251 #[arg(long)]
252 root: PathBuf,
253 },
254}
255#[derive(Subcommand)]
256enum ContextCommand {
257 Plan {
259 manifest: PathBuf,
260 #[arg(long)]
261 task: String,
262 #[arg(long)]
263 budget_tokens: u64,
264 },
265}
266#[derive(Subcommand)]
267enum SpecCommand {
268 Init {
269 id: String,
270 title: String,
271 #[arg(long, default_value = ".")]
272 root: PathBuf,
273 },
274 Check {
275 #[arg(default_value = ".")]
276 root: PathBuf,
277 },
278}
279#[derive(Subcommand)]
280enum ReleaseCommand {
281 ImportSpec {
282 #[arg(long)]
283 root: PathBuf,
284 #[arg(long, required = true)]
285 spec: Vec<String>,
286 #[arg(long)]
287 release_id: String,
288 #[arg(long)]
289 output: Option<PathBuf>,
290 },
291 DraftPr {
292 manifest: PathBuf,
293 #[arg(long)]
294 dry_run: bool,
295 },
296 Inspect {
297 manifest: PathBuf,
298 },
299 Run {
300 manifest: PathBuf,
301 #[arg(long)]
302 dry_run: bool,
303 },
304 Approve {
305 manifest: PathBuf,
306 #[arg(long)]
307 task: String,
308 #[arg(long)]
309 gate: String,
310 #[arg(long)]
311 reviewer: String,
312 #[arg(long)]
313 fingerprint: String,
314 },
315 ImportLoop {
316 input: PathBuf,
317 #[arg(long)]
318 output: Option<PathBuf>,
319 },
320 Publish {
321 manifest: PathBuf,
322 #[arg(long, value_enum, default_value = "PRIVATE")]
324 expected_visibility: publication::ExpectedVisibility,
325 #[arg(long)]
326 repository: String,
327 #[arg(long)]
328 tag: String,
329 #[arg(long)]
330 assets: PathBuf,
331 #[arg(long)]
332 notes: PathBuf,
333 #[arg(long)]
334 dry_run: bool,
335 },
336}
337#[derive(Subcommand)]
338enum DaemonControl {
339 Status,
340 Pause {
341 #[arg(long)]
342 reason: Option<String>,
343 },
344 Resume,
345 Retry {
346 #[arg(long)]
347 release: String,
348 #[arg(long)]
349 reason: String,
350 },
351}
352#[derive(Subcommand)]
353enum MemoryCommand {
354 Doctor { manifest: PathBuf },
355 Query { manifest: PathBuf, query: String },
356}
357#[derive(Subcommand)]
358enum ToolsCommand {
359 List,
360 Doctor,
361 Plan {
362 #[arg(long, default_value = ".")]
363 root: PathBuf,
364 #[arg(required = true)]
365 recipes: Vec<String>,
366 },
367 Apply {
368 #[arg(long, default_value = ".")]
369 root: PathBuf,
370 #[arg(required = true)]
371 recipes: Vec<String>,
372 },
373}
374#[derive(Subcommand)]
375enum EvalCommand {
376 Run {
377 manifest: PathBuf,
378 #[arg(long)]
379 dry_run: bool,
380 },
381}
382
383#[tokio::main]
384pub async fn run() {
385 let (program, argv) = launch::arguments(std::env::args_os().collect());
386 let mut command = Cli::command();
387 if program == "forge" {
388 command = command.name("forge").after_help("Sem subcomando: abre o chat no projeto atual. Use forge --root PATH para escolher outra pasta.");
389 }
390 let arguments = command.get_matches_from(argv);
391 let mut cli = Cli::from_arg_matches(&arguments).unwrap_or_else(|e| e.exit());
392 let explicit_state =
393 arguments.value_source("state_dir") == Some(clap::parser::ValueSource::CommandLine);
394 let explicit_root = arguments
395 .subcommand_matches("chat")
396 .is_some_and(|a| a.value_source("root") == Some(clap::parser::ValueSource::CommandLine));
397 let prepared = launch::prepare(&mut cli, explicit_state, explicit_root);
398 let logs = if cli.service_logging {
399 match service::Logs::new(&cli.state_dir) {
400 Ok(logs) => Some(logs),
401 Err(e) => {
402 eprintln!("service logging refused: {e}");
403 std::process::exit(2);
404 }
405 }
406 } else {
407 None
408 };
409 let writer = tracing_subscriber::fmt::writer::BoxMakeWriter::new(std::io::stderr);
410 let writer = logs
411 .clone()
412 .map(tracing_subscriber::fmt::writer::BoxMakeWriter::new)
413 .unwrap_or(writer);
414 tracing_subscriber::fmt()
415 .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
416 .with_ansi(!cli.service_logging)
417 .with_writer(writer)
418 .init();
419 let result = if let Err(error) = prepared {
420 Err(error)
421 } else if matches!(
422 &cli.command,
423 Command::State {
424 command: snapshots::Command::Snapshot { .. }
425 }
426 ) && !explicit_state
427 {
428 Err(anyhow::anyhow!(
429 "state snapshot requires explicit --state-dir"
430 ))
431 } else {
432 dispatch(&cli).await
433 };
434 let (value, code) = match result {
435 Ok(v) => {
436 let status = v["effective_status"]
437 .as_str()
438 .or_else(|| v["run"]["status"].as_str())
439 .or_else(|| v["status"].as_str());
440 let code = if matches!(
441 status,
442 Some("failed" | "blocked" | "awaiting_approval" | "cancelled" | "degraded")
443 ) {
444 2
445 } else {
446 0
447 };
448 (v, code)
449 }
450 Err(e) => (
451 json!({"schema_version":1,"status":"blocked","error_code":"operation_blocked","error":process::redact(&format!("{e:#}"))}),
452 2,
453 ),
454 };
455 let value = compatibility::envelope(value);
456 if let Some(logs) = logs {
457 if logs
458 .write("stdout", serde_json::to_string(&value).unwrap().as_bytes())
459 .is_err()
460 {
461 std::process::exit(2);
462 }
463 } else if cli.json {
464 println!("{}", serde_json::to_string(&value).unwrap());
465 } else {
466 if let Some(summary) = duration::human(&value) {
467 println!("{summary}");
468 }
469 println!("{}", serde_json::to_string_pretty(&value).unwrap());
470 }
471 std::process::exit(code);
472}
473async fn dispatch(cli: &Cli) -> Result<Value> {
474 let ctx = |p: &Path| engine::Context::load(p, &cli.state_dir);
475 match &cli.command {
476 Command::Contracts => Ok(compatibility::contracts()),
477 Command::Installation { command } => installation::dispatch(command).await,
478 Command::Demo { command } => demo::dispatch(command).await,
479 Command::State { command } => snapshots::dispatch(command, &cli.state_dir),
480 Command::Issue { command } => issue::dispatch(command).await,
481 Command::Review { command } => match command {
482 ReviewCommand::Plan { manifest } => review::plan(&ctx(manifest)?),
483 ReviewCommand::Run {
484 manifest,
485 app_server,
486 } => review::run(&ctx(manifest)?, app_server.as_deref()).await,
487 },
488 Command::Templates { command } => templates::dispatch(command),
489 Command::Budget {
490 command: BudgetCommand::Inspect { manifest },
491 } => budget::inspect(&ctx(manifest)?),
492 Command::Workbench {
493 root,
494 port,
495 app_server,
496 manifest,
497 } => {
498 workbench::serve(
499 root,
500 &cli.state_dir,
501 *port,
502 app_server.clone(),
503 manifest.clone(),
504 )
505 .await
506 }
507 Command::Chat { root, app_server } => {
508 anyhow::ensure!(!cli.json, "chat is interactive; omit --json");
509 terminal_chat::run(root, &cli.state_dir, app_server.clone()).await
510 }
511 Command::Feature { command } => match command {
512 FeatureCommand::New {
513 root,
514 id,
515 title,
516 ops_root,
517 apply,
518 ..
519 } => guided::feature(root, id, title, ops_root, *apply),
520 },
521 Command::Plan { command } => match command {
522 PlanCommand::Validate { path, root } => planning::validate_file(path, root),
523 PlanCommand::Propose {
524 root,
525 request,
526 ops_root,
527 output,
528 dry_run,
529 app_server,
530 previous,
531 manifest,
532 } => {
533 let reservation = if let Some(path) = manifest.as_ref().filter(|_| !*dry_run) {
534 let c = ctx(path)?;
535 let root = guided::root(root)?;
536 anyhow::ensure!(
537 c.roots.values().all(|r| r == &root),
538 "planning manifest project mismatch"
539 );
540 let mut nonce = [0u8; 16];
541 getrandom::fill(&mut nonce)
542 .map_err(|e| anyhow::anyhow!("secure operation ID: {e}"))?;
543 Some(budget::Reservation::begin(
544 &c,
545 &format!("planning:{}", forge_domain::digest(&nonce)),
546 "planning",
547 )?)
548 } else {
549 None
550 };
551 planning::propose_reserved(
552 root,
553 request,
554 ops_root,
555 output,
556 *dry_run,
557 app_server.as_deref(),
558 previous.as_deref(),
559 reservation,
560 )
561 .await
562 }
563 },
564 Command::ServiceHost { releases_dir } => service::host(releases_dir, &cli.state_dir).await,
565 Command::Service(options) => service::execute(options, &cli.state_dir),
566 Command::Dashboard { port } => dashboard::serve(&cli.state_dir, *port).await,
567 Command::Daemon {
568 releases_dir,
569 once,
570 interval_seconds,
571 dry_run,
572 } => {
573 daemon::run(
574 releases_dir,
575 &cli.state_dir,
576 *once,
577 *interval_seconds,
578 *dry_run,
579 )
580 .await
581 }
582 Command::DaemonControl { command } => match command {
583 DaemonControl::Status => daemon::control(&cli.state_dir, "status", None),
584 DaemonControl::Pause { reason } => {
585 daemon::control(&cli.state_dir, "pause", reason.as_deref())
586 }
587 DaemonControl::Resume => daemon::control(&cli.state_dir, "resume", None),
588 DaemonControl::Retry { release, reason } => {
589 daemon::retry(&cli.state_dir, release, reason)
590 }
591 },
592 Command::Init {
593 path,
594 memory_workspace,
595 } => project::init(path, memory_workspace.as_deref()),
596 Command::Doctor => doctor().await,
597 Command::Spec { command } => match command {
598 SpecCommand::Init { id, title, root } => project::spec_init(root, id, title),
599 SpecCommand::Check { root } => project::spec_check(root),
600 },
601 Command::Release { command } => match command {
602 ReleaseCommand::ImportSpec {
603 root,
604 spec,
605 release_id,
606 output,
607 } => importing::import_spec(root, spec, release_id, output.as_deref()),
608 ReleaseCommand::DraftPr { manifest, dry_run } => {
609 publication::draft_pr(&ctx(manifest)?, *dry_run).await
610 }
611 ReleaseCommand::Inspect { manifest } => ctx(manifest)?.inspect(),
612 ReleaseCommand::Run { manifest, dry_run } => {
613 let c = ctx(manifest)?;
614 if *dry_run {
615 c.inspect()
616 } else {
617 engine::execute(c, false, None).await
618 }
619 }
620 ReleaseCommand::Approve {
621 manifest,
622 task,
623 gate,
624 reviewer,
625 fingerprint,
626 } => engine::approve(&ctx(manifest)?, task, gate, reviewer, fingerprint),
627 ReleaseCommand::ImportLoop { input, output } => {
628 importing::import_loop(input, output.as_deref())
629 }
630 ReleaseCommand::Publish {
631 manifest,
632 expected_visibility,
633 repository,
634 tag,
635 assets,
636 notes,
637 dry_run,
638 } => {
639 publication::release(
640 &ctx(manifest)?,
641 repository,
642 tag,
643 assets,
644 notes,
645 *dry_run,
646 *expected_visibility,
647 )
648 .await
649 }
650 },
651 Command::Status { manifest } => engine::status(&ctx(manifest)?, false),
652 Command::Report {
653 manifest,
654 limit,
655 offset,
656 } => engine::status_window(&ctx(manifest)?, true, *limit, *offset),
657 Command::Context {
658 command:
659 ContextCommand::Plan {
660 manifest,
661 task,
662 budget_tokens,
663 },
664 } => context::plan(&ctx(manifest)?, task, *budget_tokens),
665 Command::Pause { manifest } => engine::control(&ctx(manifest)?, "pause"),
666 Command::Cancel { manifest } => engine::control(&ctx(manifest)?, "cancel"),
667 Command::Resume {
668 manifest,
669 reconcile,
670 } => engine::execute(ctx(manifest)?, true, reconcile.clone()).await,
671 Command::Memory { command } => match command {
672 MemoryCommand::Doctor { manifest } => engine::memory_check(&ctx(manifest)?, None).await,
673 MemoryCommand::Query { manifest, query } => {
674 engine::memory_check(&ctx(manifest)?, Some(query)).await
675 }
676 },
677 Command::Tools { command } => match command {
678 ToolsCommand::List => {
679 Ok(json!({"schema_version":1,"recipes":forge_catalog::recipes()?}))
680 }
681 ToolsCommand::Plan { root, recipes } => {
682 Ok(json!({"schema_version":1,"changes":forge_catalog::plan(root,recipes)?}))
683 }
684 ToolsCommand::Apply { root, recipes } => {
685 Ok(json!({"schema_version":1,"changes":forge_catalog::apply(root,recipes)?}))
686 }
687 ToolsCommand::Doctor => {
688 let mut results = Vec::new();
689 for r in forge_catalog::recipes()? {
690 let discovered = r.executable.as_deref().and_then(find_executable);
691 results.push(json!({"id":r.id,"available":true,"installed_recipe":std::env::current_dir()?.join(format!(".forge-toolkit/{}.md",r.id)).is_file(),"discovered":discovered.is_some(),"tested":false,"upstream_revision":r.revision,"last_verified":r.last_verified}));
692 }
693 Ok(json!({"schema_version":1,"tools":results}))
694 }
695 },
696 Command::Eval {
697 command: EvalCommand::Run { manifest, dry_run },
698 } => {
699 let c = ctx(manifest)?;
700 if *dry_run {
701 c.inspect()
702 } else {
703 engine::execute(c, false, None).await
704 }
705 }
706 }
707}
708fn find_executable(name: &str) -> Option<PathBuf> {
709 forge_adapters::clients::find_executable(name)
710}
711async fn doctor() -> Result<Value> {
712 let mut checks = Vec::new();
713 for (name, args, features) in [
714 ("git", vec!["--version"], vec![]),
715 ("codex", vec!["exec", "--help"], vec!["--json", "--sandbox"]),
716 (
717 "claude",
718 vec!["--help"],
719 vec!["--print", "--output-format", "--permission-mode"],
720 ),
721 ] {
722 let mut argv = vec![name.to_string()];
723 argv.extend(args.iter().map(|s| s.to_string()));
724 let (_tx, rx) = watch::channel(false);
725 let result = process::run(&argv, &std::env::current_dir()?, 15, rx).await;
726 let passed = result
727 .as_ref()
728 .is_ok_and(|r| r.passed() && features.iter().all(|f| r.stdout.contains(f)));
729 checks.push(json!({"name":name,"discovered":find_executable(name).is_some(),"capabilities_verified":passed,"authentication_tested":false}));
730 }
731 Ok(
732 json!({"schema_version":1,"platform":std::env::consts::OS,"operational_support":cfg!(unix),"windows_candidate":cfg!(windows),"authenticated_windows_acceptance":false,"checks":checks}),
733 )
734}