66
77use std:: {
88 fs:: create_dir_all,
9+ io:: IsTerminal ,
910 path:: { Path , PathBuf } ,
1011 sync:: Arc ,
1112} ;
@@ -103,12 +104,25 @@ enum OciCommand {
103104 } ,
104105 /// List all tagged OCI images in the repository
105106 #[ clap( name = "images" ) ]
106- ListImages ,
107+ ListImages {
108+ /// Output as JSON array
109+ #[ clap( long) ]
110+ json : bool ,
111+ } ,
107112 /// Show information about an OCI image
113+ ///
114+ /// By default, outputs JSON with manifest, config, and referrers.
115+ /// Use --manifest or --config to output just that raw JSON.
108116 #[ clap( name = "inspect" ) ]
109117 Inspect {
110118 /// Image reference (tag name or manifest digest)
111119 image : String ,
120+ /// Output only the raw manifest JSON (as originally stored)
121+ #[ clap( long, conflicts_with = "config" ) ]
122+ manifest : bool ,
123+ /// Output only the raw config JSON (as originally stored)
124+ #[ clap( long, conflicts_with = "manifest" ) ]
125+ config : bool ,
112126 } ,
113127 /// Tag an image with a new name
114128 Tag {
@@ -122,6 +136,21 @@ enum OciCommand {
122136 /// Tag name to remove
123137 name : String ,
124138 } ,
139+ /// Inspect a stored layer
140+ ///
141+ /// By default, outputs the raw tar stream to stdout.
142+ /// Use --dumpfile for composefs dumpfile format, or --json for metadata.
143+ #[ clap( name = "layer" ) ]
144+ LayerInspect {
145+ /// Layer diff_id (sha256:...)
146+ layer : String ,
147+ /// Output as composefs dumpfile format (one entry per line)
148+ #[ clap( long, conflicts_with = "json" ) ]
149+ dumpfile : bool ,
150+ /// Output layer metadata as JSON
151+ #[ clap( long, conflicts_with = "dumpfile" ) ]
152+ json : bool ,
153+ } ,
125154 /// Compute the composefs image object id of the rootfs of a stored OCI image
126155 ComputeId {
127156 #[ clap( flatten) ]
@@ -390,15 +419,17 @@ where
390419 println ! ( "verity {}" , result. manifest_verity. to_hex( ) ) ;
391420 println ! ( "tagged {tag_name}" ) ;
392421 }
393- OciCommand :: ListImages => {
422+ OciCommand :: ListImages { json } => {
394423 let images = composefs_oci:: oci_image:: list_images ( & repo) ?;
395424
396- if images. is_empty ( ) {
425+ if json {
426+ println ! ( "{}" , serde_json:: to_string_pretty( & images) ?) ;
427+ } else if images. is_empty ( ) {
397428 println ! ( "No images found" ) ;
398429 } else {
399430 println ! (
400- "{:<30} {:<12} {:<10} {:<8} {:<6}" ,
401- "NAME" , "DIGEST" , "ARCH" , "SEALED" , "LAYERS"
431+ "{:<30} {:<12} {:<10} {:<8} {:<6} {:<5} " ,
432+ "NAME" , "DIGEST" , "ARCH" , "SEALED" , "LAYERS" , "REFS"
402433 ) ;
403434 for img in images {
404435 let digest_short = img
@@ -411,7 +442,7 @@ where
411442 digest_short
412443 } ;
413444 println ! (
414- "{:<30} {:<12} {:<10} {:<8} {:<6}" ,
445+ "{:<30} {:<12} {:<10} {:<8} {:<6} {:<5} " ,
415446 img. name,
416447 digest_display,
417448 if img. architecture. is_empty( ) {
@@ -420,58 +451,37 @@ where
420451 & img. architecture
421452 } ,
422453 if img. sealed { "yes" } else { "no" } ,
423- img. layer_count
454+ img. layer_count,
455+ img. referrer_count
424456 ) ;
425457 }
426458 }
427459 }
428- OciCommand :: Inspect { ref image } => {
460+ OciCommand :: Inspect {
461+ ref image,
462+ manifest,
463+ config,
464+ } => {
429465 let img = if image. starts_with ( "sha256:" ) {
430466 composefs_oci:: oci_image:: OciImage :: open ( & repo, image, None ) ?
431467 } else {
432468 composefs_oci:: oci_image:: OciImage :: open_ref ( & repo, image) ?
433469 } ;
434470
435- println ! ( "Manifest: {}" , img. manifest_digest( ) ) ;
436- println ! ( "Config: {}" , img. config_digest( ) ) ;
437- println ! (
438- "Type: {}" ,
439- if img. is_container_image( ) {
440- "container"
441- } else {
442- "artifact"
443- }
444- ) ;
445-
446- if img. is_container_image ( ) {
447- println ! ( "Architecture: {}" , img. architecture( ) ) ;
448- println ! ( "OS: {}" , img. os( ) ) ;
449- }
450-
451- if let Some ( created) = img. created ( ) {
452- println ! ( "Created: {created}" ) ;
453- }
454-
455- println ! (
456- "Sealed: {}" ,
457- if img. is_sealed( ) { "yes" } else { "no" }
458- ) ;
459- if let Some ( seal) = img. seal_digest ( ) {
460- println ! ( "Seal digest: {seal}" ) ;
461- }
462-
463- println ! ( "Layers: {}" , img. layer_descriptors( ) . len( ) ) ;
464- for ( i, layer) in img. layer_descriptors ( ) . iter ( ) . enumerate ( ) {
465- println ! ( " [{i}] {} ({} bytes)" , layer. digest( ) , layer. size( ) ) ;
466- }
467-
468- if let Some ( labels) = img. labels ( ) {
469- if !labels. is_empty ( ) {
470- println ! ( "Labels:" ) ;
471- for ( k, v) in labels {
472- println ! ( " {k}: {v}" ) ;
473- }
474- }
471+ if manifest {
472+ // Output raw manifest JSON exactly as stored
473+ let manifest_json = img. read_manifest_json ( & repo) ?;
474+ std:: io:: Write :: write_all ( & mut std:: io:: stdout ( ) , & manifest_json) ?;
475+ println ! ( ) ;
476+ } else if config {
477+ // Output raw config JSON exactly as stored
478+ let config_json = img. read_config_json ( & repo) ?;
479+ std:: io:: Write :: write_all ( & mut std:: io:: stdout ( ) , & config_json) ?;
480+ println ! ( ) ;
481+ } else {
482+ // Default: output combined JSON with manifest, config, and referrers
483+ let output = img. inspect_json ( & repo) ?;
484+ println ! ( "{}" , serde_json:: to_string_pretty( & output) ?) ;
475485 }
476486 }
477487 OciCommand :: Tag {
@@ -485,6 +495,28 @@ where
485495 composefs_oci:: oci_image:: untag_image ( & repo, name) ?;
486496 println ! ( "Removed tag {name}" ) ;
487497 }
498+ OciCommand :: LayerInspect {
499+ ref layer,
500+ dumpfile,
501+ json,
502+ } => {
503+ if json {
504+ let info = composefs_oci:: layer_info ( & repo, layer) ?;
505+ println ! ( "{}" , serde_json:: to_string_pretty( & info) ?) ;
506+ } else if dumpfile {
507+ composefs_oci:: layer_dumpfile ( & repo, layer, & mut std:: io:: stdout ( ) ) ?;
508+ } else {
509+ // Default: output raw tar, but not to a tty
510+ let mut out = std:: io:: stdout ( ) . lock ( ) ;
511+ if out. is_terminal ( ) {
512+ anyhow:: bail!(
513+ "Refusing to write tar data to terminal. \
514+ Redirect to a file, pipe to tar, or use --json for metadata."
515+ ) ;
516+ }
517+ composefs_oci:: layer_tar ( & repo, layer, & mut out) ?;
518+ }
519+ }
488520 OciCommand :: Seal {
489521 config_opts :
490522 OCIConfigOptions {
0 commit comments