@@ -269,6 +269,9 @@ enum Command {
269269 #[ command( flatten) ]
270270 ctx : MsgCtx ,
271271 } ,
272+ /// EXPERIMENTAL, READ-ONLY: parse, validate, and inspect versioned catalog plans.
273+ #[ command( subcommand) ]
274+ Plan ( PlanCmd ) ,
272275 /// Print a shell completion script for `st2` to stdout (`st2 completions <bash|zsh|fish|…>`).
273276 /// Generated from the live command tree, so it never drifts from the actual flags.
274277 Completions {
@@ -339,6 +342,48 @@ enum HooksCmd {
339342 VerifyOwn ,
340343}
341344
345+ #[ derive( Subcommand ) ]
346+ enum PlanCmd {
347+ /// Validate every external and inline plan without executing or writing anything.
348+ Validate {
349+ /// Catalog folder or KDL file. Prefer --catalog; defaults to the selected catalog.
350+ #[ arg( conflicts_with = "catalog_path" ) ]
351+ root : Option < PathBuf > ,
352+ /// Emit a machine-readable validation receipt.
353+ #[ arg( long) ]
354+ json : bool ,
355+ } ,
356+ /// List normalized plan identity, owner, and derived frontier.
357+ List {
358+ /// Catalog folder or KDL file. Prefer --catalog; defaults to the selected catalog.
359+ #[ arg( conflicts_with = "catalog_path" ) ]
360+ root : Option < PathBuf > ,
361+ /// Emit a machine-readable array.
362+ #[ arg( long) ]
363+ json : bool ,
364+ } ,
365+ /// Show normalized intent for one explicit plan identity.
366+ Show {
367+ identity : String ,
368+ /// Catalog folder or KDL file. Prefer --catalog; defaults to the selected catalog.
369+ #[ arg( conflicts_with = "catalog_path" ) ]
370+ root : Option < PathBuf > ,
371+ /// Emit machine-readable normalized intent.
372+ #[ arg( long) ]
373+ json : bool ,
374+ } ,
375+ /// Inspect one plan with source provenance, resolved file paths, and agent references.
376+ Inspect {
377+ identity : String ,
378+ /// Catalog folder or KDL file. Prefer --catalog; defaults to the selected catalog.
379+ #[ arg( conflicts_with = "catalog_path" ) ]
380+ root : Option < PathBuf > ,
381+ /// Emit the complete machine-readable inspection record.
382+ #[ arg( long) ]
383+ json : bool ,
384+ } ,
385+ }
386+
342387#[ derive( Subcommand ) ]
343388enum ResourceCmd {
344389 /// Link a resource (a URL you produced or reference) into your resource list.
@@ -571,6 +616,7 @@ fn main() -> Result<()> {
571616 enrich,
572617 ctx,
573618 } => agents_cmd ( catalog, status, json, enrich, ctx) ,
619+ Command :: Plan ( command) => plan_cmd ( command) ,
574620 Command :: CompileAgent {
575621 catalog,
576622 identity,
@@ -638,6 +684,159 @@ fn main() -> Result<()> {
638684 }
639685}
640686
687+ fn plan_cmd ( command : PlanCmd ) -> Result < ( ) > {
688+ match command {
689+ PlanCmd :: Validate { root, json } => {
690+ let root = catalog_arg ( root) ?;
691+ match st2:: plans:: load ( & root) {
692+ Ok ( catalog) => {
693+ if json {
694+ println ! (
695+ "{}" ,
696+ serde_json:: to_string_pretty( & serde_json:: json!( {
697+ "result" : "valid" ,
698+ "plans" : catalog. plans. len( ) ,
699+ "errors" : 0 ,
700+ } ) ) ?
701+ ) ;
702+ } else {
703+ println ! (
704+ "valid: {} plan{}; read-only (no execution or writes)" ,
705+ catalog. plans. len( ) ,
706+ plural( catalog. plans. len( ) )
707+ ) ;
708+ }
709+ Ok ( ( ) )
710+ }
711+ Err ( error) => {
712+ if json {
713+ println ! (
714+ "{}" ,
715+ serde_json:: to_string_pretty( & serde_json:: json!( {
716+ "result" : "invalid" ,
717+ "code" : error. code( ) ,
718+ "path" : error. path( ) ,
719+ "error" : error. to_string( ) ,
720+ } ) ) ?
721+ ) ;
722+ }
723+ Err ( error. into ( ) )
724+ }
725+ }
726+ }
727+ PlanCmd :: List { root, json } => {
728+ let catalog = st2:: plans:: load ( & catalog_arg ( root) ?) ?;
729+ if json {
730+ let rows = catalog
731+ . plans
732+ . iter ( )
733+ . map ( |plan| {
734+ serde_json:: json!( {
735+ "identity" : plan. identity,
736+ "owner" : plan. owner,
737+ "frontier" : plan. frontier,
738+ } )
739+ } )
740+ . collect :: < Vec < _ > > ( ) ;
741+ println ! ( "{}" , serde_json:: to_string_pretty( & rows) ?) ;
742+ } else {
743+ for plan in catalog. plans {
744+ println ! (
745+ "{}\t owner={}\t frontier={}" ,
746+ plan. identity,
747+ plan. owner,
748+ plan. frontier. join( "," )
749+ ) ;
750+ }
751+ }
752+ Ok ( ( ) )
753+ }
754+ PlanCmd :: Show {
755+ identity,
756+ root,
757+ json,
758+ } => {
759+ let catalog = st2:: plans:: load ( & catalog_arg ( root) ?) ?;
760+ let plan = exact_plan ( & catalog, & identity) ?;
761+ let intent = serde_json:: json!( {
762+ "identity" : plan. identity,
763+ "owner" : plan. owner,
764+ "versions" : plan. versions. iter( ) . map( |version| serde_json:: json!( {
765+ "identity" : version. identity,
766+ "parents" : version. parents,
767+ "why" : version. why,
768+ "resource" : version. resource,
769+ } ) ) . collect:: <Vec <_>>( ) ,
770+ "frontier" : plan. frontier,
771+ } ) ;
772+ if json {
773+ println ! ( "{}" , serde_json:: to_string_pretty( & intent) ?) ;
774+ } else {
775+ println ! ( "plan {} owner={}" , plan. identity, plan. owner) ;
776+ for version in & plan. versions {
777+ let marker = if plan. frontier . contains ( & version. identity ) {
778+ " [frontier]"
779+ } else {
780+ ""
781+ } ;
782+ println ! (
783+ " version {}{marker} resource={}" ,
784+ version. identity, version. resource
785+ ) ;
786+ if !version. parents . is_empty ( ) {
787+ println ! ( " parents: {}" , version. parents. join( ", " ) ) ;
788+ }
789+ if let Some ( why) = & version. why {
790+ println ! ( " why: {why}" ) ;
791+ }
792+ }
793+ }
794+ Ok ( ( ) )
795+ }
796+ PlanCmd :: Inspect {
797+ identity,
798+ root,
799+ json,
800+ } => {
801+ let catalog = st2:: plans:: load ( & catalog_arg ( root) ?) ?;
802+ let plan = exact_plan ( & catalog, & identity) ?;
803+ if json {
804+ println ! ( "{}" , serde_json:: to_string_pretty( plan) ?) ;
805+ } else {
806+ println ! (
807+ "plan {} owner={} kind={:?}\n source: {}\n referenced-by: {}\n frontier: {}" ,
808+ plan. identity,
809+ plan. owner,
810+ plan. source_kind,
811+ plan. source. display( ) ,
812+ plan. referenced_by. join( "," ) ,
813+ plan. frontier. join( "," )
814+ ) ;
815+ for version in & plan. versions {
816+ println ! (
817+ " {}: {} -> {}" ,
818+ version. identity,
819+ version. resource,
820+ version. resolved_resource. display( )
821+ ) ;
822+ }
823+ }
824+ Ok ( ( ) )
825+ }
826+ }
827+ }
828+
829+ fn exact_plan < ' a > (
830+ catalog : & ' a st2:: plans:: PlanCatalog ,
831+ identity : & str ,
832+ ) -> Result < & ' a st2:: plans:: Plan > {
833+ catalog
834+ . plans
835+ . iter ( )
836+ . find ( |plan| plan. identity == identity)
837+ . with_context ( || format ! ( "no plan '{identity}' found" ) )
838+ }
839+
641840#[ allow( clippy:: too_many_arguments) ]
642841fn compile_agent_cmd (
643842 catalog : & Path ,
0 commit comments