#!/usr/bin/perl

# $Id: oi_manage,v 1.61 2001/02/02 04:49:41 cwinters Exp $

use strict;
use Cwd                   qw( cwd );
use Data::Dumper          qw( Dumper );
use ExtUtils::Manifest;
use File::Copy            qw( cp );
use Getopt::Long          qw( GetOptions );
use OpenInteract::Package ();
use OpenInteract::Startup;
use OpenInteract::DBI;
use OpenInteract::SQLInstall;
use Pod::Usage            qw( pod2usage );
use SPOPS::Secure         qw( :level :scope );
use SPOPS::HashFile       ();
use Text::Wrap            qw( wrap );

$Text::Wrap::columns = 60; 

use constant DEBUG    => 0;

# Legitimate commands; anything not listed here will be kicked out and
# the 'usage' stuff displayed

my %COMMANDS          = map { $_ => 1 } 
                        qw( initial_packages list_packages list_actions
                            install create_skeleton export_package check_package
                            create_website install_sql test_db
                            install_package apply_package remove_package upgrade_package 
                            install_template dump_template remove_template );

# These are aliases people might type by accident instead of the
# proper command; add as necessary

my %ALIASES           = (
 input_template  => 'install_template',
 install_website => 'create_website',
 test_database   => 'test_db',
 install_oi      => 'install',
 action_list     => 'list_actions',
);

# Subdirectories used when creating a website

my @WEBSITE_SUBDIR    = qw( conf data doc email error html html/images logs pkg test );

# Packages shipped with OI -- the 'base' packages necessary to run the system

my @WEBSITE_BASE_PKG  = qw( 
   base  base_box  base_component  base_error  base_group  base_security  
   base_template  base_theme  base_user  static_page  system_doc 
);

# Extra packages shipped with OI -- small applications that show how things work

my @WEBSITE_EXTRA_PKG = qw( full_text classified news );

# Files that are copied from the directory where you untar/gz the main
# OpenInteract distribution into the base installation, when you run
# the 'install' command

my @INITIAL_FILES     = qw( 
   README INSTALL INSTALL.website COPYING 
);

# Separator used in output

my $SEP               = '=========================';

# Data used for getting templates into and out of the database

my $TMPL_EXT          = 'tmpl';
my $META_EXT          = 'meta';
my $TMPL_DIR          = 'template';
my $SCRIPT_SEPARATE   = '<!-- TEMPLATE SCRIPT -->';

{
 # Process command-line options

 my ( $OPT_help, $OPT_man, $OPT_initial_pkg );
 my ( $OPT_base_dir, $OPT_website_dir, $OPT_website_name, $OPT_dump_dir );
 my ( $OPT_package_file, $OPT_package_dir, $OPT_package_list_file );
 my ( @OPT_package );
 GetOptions( 'help|?' => \$OPT_help, 'man' => \$OPT_man, 
             'base_dir=s'          => \$OPT_base_dir,
             'website_dir=s'       => \$OPT_website_dir, 
             'website_name=s'      => \$OPT_website_name,
             'package_file=s'      => \$OPT_package_file,
             'package_list_file=s' => \$OPT_package_list_file,
             'package_dir=s'       => \$OPT_package_dir,
             'package=s'           => \@OPT_package,
             'dump_dir=s'          => \$OPT_dump_dir,
           );

 # Give them the man page if they want
 # (note that 'pod2usage' exits the script after displaying the page)

 pod2usage( -exitstatus => 0, -verbose => 2 )  if ( $OPT_man );

 # Grab the command -- if it's not there or doesn't exist in the list
 # of valid commands, then print the basic help page

 my $command = lc shift @ARGV;

 # If the command is one of the aliases, alias it and let them know so
 # the user can change his/her behavior.

 if ( $ALIASES{ $command } ) {
   print "[oi_manage]: Command ($command) is an alias for $ALIASES{ $command }.\n",
         "Using alias and continuing...\n";
   $command = $ALIASES{ $command };
 }

 # Now, see if the user wants help. If no command was given, then they
 # obviously need some help :)

 $OPT_help++ unless ( $COMMANDS{ $command } );
 if ( $OPT_help ) {
   my $msg = '';
   if ( $command ) {
     my $valid_commands = undef;
     my $count = 1;
     foreach my $command ( sort keys %COMMANDS ) {
       $valid_commands .= "$command | ";
       $valid_commands .= "\n" if ( $count % 4 == 0 );
       $count++;
     }
     $valid_commands =~ s/ \| $//;
     $msg = "[oi_manage]: $command is not a valid command.\n\nValid commands:\n\n$valid_commands\n";
   }
   pod2usage( -exitstatus => 0, -verbose => 1, -msg => $msg ) ;
 }

 # Easiest to process, so do first

 if ( $command eq 'initial_packages' ) {
   print "\n[oi_manage]: initial_packages\n\n",
         "Packages installed when a new website is created:\n$SEP\n",
         join( "\n", @WEBSITE_BASE_PKG ), "\n";
   exit(0);
 }

 # Set any defaults and do initializations

 OpenInteract::Package->class_initialize;

 if ( ! $OPT_base_dir and $ENV{OPENINTERACT} ) {
   print "[oi_manage]: Using ($ENV{OPENINTERACT}) for 'base_dir'.\n";
   $OPT_base_dir = $ENV{OPENINTERACT};
 }
 if ( ! $OPT_website_dir and $ENV{OIWEBSITE} ) {
   print "[oi_manage]: Using ($ENV{OIWEBSITE}) for 'website_dir'.\n";
   $OPT_website_dir = $ENV{OIWEBSITE};
 }

 if ( $OPT_package_list_file ) {
   unless ( -f $OPT_package_list_file ) {
     die "[oi_manage]: The parameter 'package_list_file' must point to a valid file.\n";
   }
   open( PKGLIST, $OPT_package_list_file ) 
       || die "[oi_manage]: Cannot open the file ($OPT_package_list_file). Error: $!\n";
   while( <PKGLIST> ) {
     chomp;
     next if ( /^\#/ );
     next if ( /^\s*$/ );
     push @OPT_package, $_;
   }
   close( PKGLIST );
 }

 if ( scalar @OPT_package ) {

   # allows --package=x,y --package=z to be combined

   @OPT_package  = split( /,/, join( ',', @OPT_package ) );

   # Allow a special keyword for users to specify all the initial
   # (base) packages. This allows something like:
   #
   #   oi_manage --package=INITIAL ...
   #   oi_manage --package=INITIAL,mypkg,theirpkg ...
   #
   # and the keyword 'INITIAL' will be replaced by all the initial
   # packages, which can be found by doing 'oi_manage
   # initial_packages'

   my %pkg_names = map { $_ => 1 } @OPT_package;
   if ( exists $pkg_names{INITIAL} ) {
     map { $pkg_names{ $_ } = 1 } @WEBSITE_BASE_PKG;
     delete $pkg_names{INITIAL};
     @OPT_package = sort keys %pkg_names;
   }
 }

 # Lop off any trailing '/' characters in directories passed in

 $OPT_base_dir    =~ s/[\\|\/]$//;
 $OPT_website_dir =~ s/[\\|\/]$//;
 $OPT_package_dir =~ s/[\\|\/]$//;
 $OPT_dump_dir    =~ s/[\\|\/]$//;

 # If we've made it down here, we can be certain that $command
 # actually contains a valid command. Each command section can still
 # call 'pod2usage' as necessary if there are arguments necessary for
 # them to run that were not specified.

 # 
 # CREATE SKELETON
 # 
 my ( @error_msg );
 if ( $command eq 'create_skeleton' ) {
   unless ( scalar @OPT_package ) {
     push @error_msg, "Command 'create_skeleton' requires you to specify at least one package name\n";
   }
   unless ( -d $OPT_base_dir ) {
     push @error_msg, "Command 'create_skeleton' requires the parameter " .
                      "'base_dir' to be defined and exist as a directory\n";
   }
   notify_error( @error_msg )  if ( @error_msg );
   print "Creating package skeleton in current directory.\n$SEP\n\n";
   foreach my $package_name ( @OPT_package ) {
     eval { OpenInteract::Package->create_package_skeleton( $package_name, $OPT_base_dir ) };
     my $status = $@ || 'ok';
     printf( "Package %-15s: %s\n", $package_name, $status );
   }
   print "\n$SEP\nFinished with create_skeleton!\n";
   exit(0);
 }

 #
 # INSTALL
 #
 if ( $command eq 'install' ) {
   unless ( $OPT_base_dir ) {
     push @error_msg, "Command 'install' requires the parameter " .
                      "'base_dir' to be set, although the directory specified should not exist yet.\n";
   }
   if ( -d $OPT_base_dir ) {
     push @error_msg, "Command 'install' requires that the parameter " .
                      "'base_dir' refers to a directory name that does not exist! " .
                      "Please remove or rename the specified directory, or specify a new one.\n";
   }
   unless ( -f 'VERSION' and -d 'pkg' ) {
     push @error_msg, "Command 'install' must be run from the OpenInteract source " .
                      "directory. Please go there and run this command again.\n";
   }
   notify_error( @error_msg )  if ( @error_msg );
   print "Running install...\n$SEP\n\n";
   my $status = install_oi( $OPT_base_dir );
   print "Packages installed:\n",
         join( "\n", map { print_status_line( $_ ) } @{ $status } ), "\n",
         "\n$SEP\nInstallation of OpenInteract complete!\n";
   exit(0);
 }

 # 
 # CREATE WEBSITE
 #
 if ( $command eq 'create_website' ) {
   unless ( $OPT_website_name and $OPT_website_dir and -d $OPT_base_dir ) {
     push @error_msg, "Command 'create_website' requires the parameters " .
                      "'website_name' and 'website_dir' to be defined and 'base_dir' to " .
                      "be defined and exist as a directory\n";
   }

   # Ensure that the website directory doesn't already exist
   if ( -d $OPT_website_dir ) {
     push @error_msg, "Cannot complete command: the directory specified in " .
                      "'website_directory' ($OPT_website_dir) already exists. " .
                      "Please rename or remove it and then re-run this program.\n\n" ;
   }
   notify_error( @error_msg )  if ( @error_msg );
   print "Running create_website...\n$SEP\n\n";
   my $status = create_website( { website_name => $OPT_website_name, 
                                  website_dir  => $OPT_website_dir,
                                  base_dir     => $OPT_base_dir } );
   my $pkg_status = join( "\n", map { print_status_line( $_ ) } @{ $status } ), "\n";
   print <<CREATEMSG;
Status of the packages installed:

$pkg_status

Please see the file:

 $OPT_website_dir/INSTALL.website

It has information on the next steps needed to get your website up and
running. You have already accomplished the first two step so you can
start at step (2) -- 'Configure your website'. Good luck!

$SEP
Finished creating new website!
CREATEMSG
   exit(0);
 }

 #
 # LIST PACKAGES
 #
 if ( $command eq 'list_packages' ) {
   unless ( -d $OPT_website_dir or -d $OPT_base_dir ) {
     push @error_msg, "Command 'list_packages' requires you to specify either " .
                      "'website_dir' or 'base_dir' as a valid directory\n";
   }
   notify_error( @error_msg )  if ( @error_msg );
   print "Running list_packages...\n$SEP\n\n";
   list_packages( { base_dir    => $OPT_base_dir,
                    website_dir => $OPT_website_dir } );
   print "\n$SEP\nFinished listing packages!\n";
   exit(0);
 }

 #
 # LIST ACTIONS
 #
 if ( $command eq 'list_actions' ) {
   unless ( -d $OPT_website_dir ) {
     push @error_msg, "Command 'list_actions' requires that the parameter 'website_dir' " .
                      "refer to a valid directory.";
   }
   notify_error( @error_msg )  if ( @error_msg );
   print "Running list_actions...\n$SEP\n\n";
   my $action_info = list_actions( $OPT_website_dir );
   foreach my $info ( @{ $action_info } ) {
     print "ACTION: $info->{action}\n",
           "Package:  $info->{package_name} ($info->{package_version})\n";
     if ( $info->{template} ) {
       print "Template: $info->{template}\n\n";
     }
     else {
       print "Class:    $info->{class}\n", 
             "Method:   $info->{method}\n\n";
     }
   }
   print "\n$SEP\nFinished listing actions!\n";
   exit(0);       
 }

 #
 # EXPORT PACKAGE
 #
 if ( $command eq 'export_package' ) {
   print "Running export_package...\n$SEP\n\n";
   if ( $OPT_package_dir ) {
     unless ( -d $OPT_package_dir ) {
       push @error_msg, "Command 'export_package' requires the 'package_dir' parameter " .
                        "to point to an existing directory\n";
     }
     unless ( scalar @OPT_package ) {
       push @error_msg, "Command 'export_package' requires one or more packages to " .
                        "be specified if you use the 'package_dir' parameter. You may also simply 'cd' " .
                        "to the directory with the package and run this command without any parameters.\n";
     }
   }
   notify_error( @error_msg )  if ( @error_msg );     
   my $status = export_package( { package_dir => $OPT_package_dir,
                                  package     => \@OPT_package } );
   print "Status of the packages you requested to be exported:\n", 
         join( "\n", map { print_status_line( $_ ) } @{ $status } ), "\n",
         "\n$SEP\nFinished export_package!\n";
   exit(0);
 }

 #
 # CHECK PACKAGE
 #
 if ( $command eq 'check_package' ) {
   my $use_dir = 0;
   if ( $OPT_package_dir and ! $OPT_package_dir ) {
     push @error_msg, "Command 'check_package' requires the parameter " .
                      "'package_dir' exist as a directory if you choose to specify it.\n";
     $use_dir++;
   }
   if ( $OPT_website_dir and ! -d $OPT_website_dir ) {
     push @error_msg, "Command 'check_package' requires the parameter " .
                      "'website_dir' exist as a directory if you choose to specify it.\n";
     $use_dir++;
   }
   if ( $OPT_base_dir and ! -d $OPT_base_dir ) {
     push @error_msg, "Command 'check_package' requires the parameter " .
                      "'base_dir' exist as a directory if you choose to specify it.\n";
     $use_dir++;
   }
   if ( $use_dir ) {
     unless ( scalar @OPT_package ) {
       push @error_msg, "Command 'check_package' requires that you specify one or more packages " .
                        "if you specify one of the parameters 'package_dir', 'base_dir', 'website_dir'\n";
     }
   }
   notify_error( @error_msg )  if ( @error_msg );
   print "Running check_package...\n$SEP\n\n";
   my $status = [];
   if ( $OPT_package_dir ) {
     if ( scalar @OPT_package ) {
       foreach my $package_name ( @OPT_package ) {         
         push @{ $status }, check_package( { package_dir => "$OPT_package_dir/$package_name" } );
       }
     }
     else {
       push @{ $status }, check_package( { package_dir => $OPT_package_dir } );
     }
   }
   elsif ( $use_dir ) {
     $status = check_installed_package( { package     => \@OPT_package,
                                          website_dir => $OPT_website_dir,
                                          base_dir    => $OPT_base_dir } );
   }
   else {
     push @{ $status }, check_package( { package_dir => cwd } );
   }
   print "Status of the packages you requested to be checked:\n", 
         join( "\n", map { print_status_line( $_, { same_line => 1 } ) } @{ $status } ), "\n",
         "\n$SEP\nFinished with check_package!\n";
   exit(0);
 }

 #
 # INSTALL PACKAGE
 #
 if ( $command eq 'install_package' ) {
   unless ( -d $OPT_base_dir ) {
     push @error_msg, "Command 'install_package' requires the parameter 'base_dir' " .
                   "to be set to an existing directory\n";
   }
   unless ( -f $OPT_package_file ) {
     push @error_msg, "Command 'install_package' requires the parameter 'package_file' " .
                      "to be set to an existing package distribution \n";
   }
   notify_error( @error_msg )  if ( @error_msg );
   print "Running install_package...\n$SEP\n\n";   
   my $pkg = OpenInteract::Package->install_package( { base_dir => $OPT_base_dir,
                                                       package_file => $OPT_package_file } );
   if ( $pkg ) {
     print "Installed package: $pkg->{name}-$pkg->{version}\n";
   }
   else {
     print "Error installating package.\n";
   }
   print "\n$SEP\nFinished installing package!\n";
   exit(0);
 }

 #
 # APPLY PACKAGE
 #
 if ( $command eq 'apply_package' ) {
   unless ( scalar @OPT_package ) {
     push @error_msg, "Command 'apply_package' requires one or more package names to install.";
   }
   unless ( -d $OPT_website_dir ) {
     push @error_msg, "Command 'apply_package' requires the parameter " .
                      "'website_dir' be set to an existing directory\n";
   }
   if ( $OPT_base_dir and ! -d $OPT_base_dir ) {
     push @error_msg, "Command 'apply_package' requires the parameter " .
                      "'base_dir' be either not set or set to an existing directory\n";     
   }
   notify_error( @error_msg )  if ( @error_msg );
   print "Running apply_package...\n$SEP\n\n";
   my $status = apply_package( { package     => \@OPT_package, 
                                 base_dir    => $OPT_base_dir,
                                 website_dir => $OPT_website_dir } );
   print "Status of the packages you requested to be applied:\n", 
         join( "\n", map { print_status_line( $_ ) } @{ $status } ), "\n",
         "\n$SEP\nFinished applying package!\n";
   exit(0);
 }

 #
 # REMOVE PACKAGE
 # 
 if ( $command eq 'remove_package' ) {
   unless ( scalar @OPT_package ) {
     push @error_msg, "Command 'remove_package' requires one or more package names to remove.";
   }
   unless ( -d $OPT_website_dir ) {
     push @error_msg, "Command 'remove_package' requires the parameter " .
                      "to be set to existing directories\n";
   }
   notify_error( @error_msg )  if ( @error_msg );
   print "Running remove_package...\n$SEP\n\n";
   my $status = remove_package( { package     => \@OPT_package, 
                                  website_dir => $OPT_website_dir } );
   my $removal_status = join( "\n", map { print_status_line( $_ ) } @{ $status } );
   print <<RMVMSG;
Status of the packages you requested for removal:

$removal_status

General Removal Notes:

We removed the information from the package database for the above
package(s), but we did not remove the files. The package files still
exist under the directory tree -- if you would like to remove this
package forever you can simply remove the directories.
$SEP
Finished remove_package!
RMVMSG
   exit(0);
 }

 #
 # UPGRADE PACKAGE
 # 
 if ( $command eq 'upgrade_package' ) {
   unless ( scalar @OPT_package ) {
     push @error_msg, "Command 'upgrade_package' requires one or more package names to remove.";
   }
   unless ( -d $OPT_website_dir ) {
     push @error_msg, "Command 'upgrade_package' requires the parameter 'website_dir' " .
                      "to be set to an existing directory\n";
   }
   notify_error( @error_msg )  if ( @error_msg );
   print "Running upgrade_package...\n$SEP\n\n";
   my $status = upgrade_package( { package     => \@OPT_package, 
                                   website_dir => $OPT_website_dir } );
   my $pkg_status = join( "\n", map { print_status_line( $_ ) } @{ $status } );
   print <<UPGRADEMSG;
Status of the packages you requested for upgrade:

$pkg_status

General Upgrade Notes:

We removed the information from the package database for the old
package(s), we did not remove its files -- any modified templates,
handlers, configuration files, etc. still exist under the old
directory name. If you made such modifications, you will need to bring
them to the new version by hand

$SEP
Finished upgrade_package!
UPGRADEMSG
   exit(0);
 }

 #
 # INSTALL TEMPLATE
 #
 if ( $command eq 'install_template' ) {
   unless ( scalar @OPT_package ) {
     push @error_msg, "Command 'install_template' requires one or more package names " .
                      "for which it should install templates.";
   }
   unless ( -d $OPT_website_dir ) {
     push @error_msg, "Command 'install_template' requires the parameter 'website_dir' " .
                      "to be set to an existing directory\n";
   }
   
   notify_error( @error_msg )  if ( @error_msg );
   print "Running install_template...\n$SEP\n\n";
   my $status = install_template( { package     => \@OPT_package, 
                                  website_dir => $OPT_website_dir } );
   my $pkg_status = join( "\n", map { print_status_line( $_ ) } @{ $status } );
   print <<INSTTMPLMSG;
Status of the templates you requested for installation:

$pkg_status

$SEP
Finished install_template!
INSTTMPLMSG
   exit(0);
 }

 #
 # REMOVE TEMPLATE
 #
 if ( $command eq 'remove_template' ) {
   unless ( scalar @OPT_package ) {
     push @error_msg, "Command 'remove_template' requires one or more package names " .
                      "for which it should remove templates.";
   }
   unless ( -d $OPT_website_dir ) {
     push @error_msg, "Command 'remove_template' requires the parameter 'website_dir' " .
                      "to be set to an existing directory\n";
   }
   notify_error( @error_msg )  if ( @error_msg );
   print "Running remove_template...\n$SEP\n\n";
   my $status = remove_template( { package     => \@OPT_package, 
                                   website_dir => $OPT_website_dir } );
   my $pkg_status = join( "\n", map { print_status_line( $_ ) } @{ $status } );
   print <<RMVTMPLMSG;
Status of the templates you requested for remove:

$pkg_status

$SEP
Finished remove_template!
RMVTMPLMSG
   exit(0);
 }

 #
 # DUMP TEMPLATE
 #
 if ( $command eq 'dump_template' ) {
   if ( $OPT_dump_dir and ! -d $OPT_dump_dir ) {
     push @error_msg, "Command 'dump_template' can use the parameter 'dump_dir' " .
                      "but if you specify the parameter it must exist as a directory.";
   }
   unless ( scalar @OPT_package ) {
     push @error_msg, "Command 'dump_template' requires one or more package names " .
                      "for which it should remove templates.";
   }
   unless ( -d $OPT_website_dir ) {
     push @error_msg, "Command 'dump_template' requires the parameter 'website_dir' " .
                      "to be set to an existing directory\n";
   }
   notify_error( @error_msg )  if ( @error_msg );
   print "Running dump_template...\n$SEP\n\n";
   my $status = dump_template( { package     => \@OPT_package, 
                                 dump_dir    => $OPT_dump_dir,
                                 website_dir => $OPT_website_dir } );
   my $pkg_status = join( "\n", map { print_status_line( $_ ) } @{ $status } );
   print <<DUMPTMPLMSG;
Status of the templates you requested to dump:

$pkg_status

$SEP
Finished dump_template!
DUMPTMPLMSG
   exit(0);
 }

 #
 # SQL INSTALL
 #
 if ( $command eq 'install_sql' ) {
   unless ( scalar @OPT_package ) {
     push @error_msg, "Command 'install_sql' requires one or more package names to install.";
   }
   unless ( -d $OPT_website_dir ) {
     push @error_msg, "Command 'install_sql' requires the parameter " .
                      "'website_dir' to be set to an existing directory\n";
   }
   notify_error( @error_msg )  if ( @error_msg );
   print "Running install_sql...\n$SEP\n\n";
   my $status = install_sql( { package     => \@OPT_package,
                               website_dir => $OPT_website_dir } );
   my $pkg_status = join( "\n$SEP\n", map { print_status_line( $_ ) } @{ $status } );
   my @notes = grep { $_->{note} } @{ $status };
   my $note_status = '';
   foreach my $status ( @notes )  {
     $note_status .= "Package $status->{name}-$status->{version}\n" .
                     "$status->{note}\n";
   }
   $note_status ||= 'none';
   print <<INSTSQLMSG;
Status of the packages requested for SQL install:

$pkg_status

Important Notes
-------------------------

$note_status

$SEP
Finished install_sql!
INSTSQLMSG
   exit(0);
 }

 #
 # TEST DB
 #
 if ( $command eq 'test_db' ) {
   unless ( -d $OPT_website_dir ) {
     push @error_msg, "Command 'test_db' requires the parameter " .
                      "'website_dir' to be set to an existing directory\n";
   }
   notify_error( @error_msg )  if ( @error_msg );
   print "Running test_db...\n$SEP\n\n";
   my $status = test_db_connection( $OPT_website_dir );
   my $status_info = print_status_line( $status );
   print <<DBMSG;
Status of the database test:

$status_info

$SEP
Finished test_db!
DBMSG
   exit(0);
 }

 die "Command '$command' not scripted yet!\n";
}



sub notify_error {
 my @msgs = @_;
 my $error_msg = "[oi_manage]\n";
 foreach ( @msgs ) {
   $error_msg .= " -- " . wrap( undef, undef, $_) . "\n\n";
 }
 pod2usage( -exitstatus => 0, -verbose => 1, 
            -msg => $error_msg );
}



# 
# list_packages
# 
sub list_packages {
 my $p = shift;
 my $use_dir = $p->{website_dir} || $p->{base_dir};
 my $pkg_list = OpenInteract::Package->fetch_group( { directory => $use_dir } ); 
 my $package_location = ( $p->{website_dir} ) ? 'website directory' : 'installation directory';
 print "Available packages in the $package_location:\n";
 foreach my $pkg ( sort { $a->{name} cmp $b->{name} } @{ $pkg_list } ) {
   my $base_dir = $pkg->{website_dir} || $pkg->{base_dir};
   print "Package: $pkg->{name}-$pkg->{version}\n",
         "Installed: $pkg->{installed_on}\n",
         "Directory: $base_dir/$pkg->{package_dir}\n\n";
 } 
}



#
# list_actions
#

sub list_actions {
 my $website_dir = shift;
 my $bc = open_base_config_file( $website_dir );
 push @INC, $bc->{website_dir};
 my ( $init, $C ) = OpenInteract::Startup->main_initialize( { base_config => $bc,
                                                              alias_init => 1,
                                                              spops_init => 1 } );
 my @actions = ();
 foreach my $action ( sort keys %{ $C->{action} } ) {
   push @actions, { action => $action, 
                    package_name => $C->{action}->{ $action }->{package_name},
                    package_version => $C->{action}->{ $action }->{package_version},
                    class => $C->{action}->{ $action }->{class},
                    method => $C->{action}->{ $action }->{method},
                    template => $C->{action}->{ $action }->{tmpl_name} };   
 }
 return \@actions;
}



#
# install_oi
#
# requires parameter base_dir be defined but not exist yet.
sub install_oi {
 my $base_dir = shift;
 my $pwd = cwd;

 # First match up the package names in our initial and extra list with
 # the filename of the package distributed

 opendir( PKG, "pkg/" ) || die "Cannot open package directory for reading! Error: $!";
 my @package_files = grep { -f "pkg/$_" } grep /\.tar\.gz$/, readdir( PKG );
 closedir( PKG );
 my ( %package_match );
 foreach my $package_file ( @package_files ) {
   my ( $package_name ) = $package_file =~ /^(.*)\-\d+\.\d+\.tar\.gz$/;
   $package_match{ $package_name } = $package_file;
 }
 warn " --Packages matched: ", Dumper( \%package_match ), "\n"             if ( DEBUG );

 # Create the new install dir

 if ( -d $base_dir ) {
   die "[oi_manage]: Installation directory ($base_dir) already exists!\n",
       "Please remove it before continuing.\n";
 }
 mkdir( $base_dir, 0775 ) || die "Cannot create installation directory! Error: $!";
 
 # Make our subdirectories

 my @sub_dirs = qw( conf doc pkg );
 foreach my $sub_dir ( @sub_dirs ) {
   mkdir( "$base_dir/$sub_dir", 0775 ) || die "Cannot create subdirectory ($base_dir/$sub_dir). Error: $!";
 }

 # Copy some files that are specified above

 foreach my $file_frag ( @INITIAL_FILES ) {
   cp( $file_frag, "$base_dir/$file_frag" ) 
     || warn "Could not copy file ($file_frag) to new directory! Error: $!";
 }

 # Now copy over everything in the 'conf/' and 'doc/' directories

COPYDIR:
 foreach my $dir_name ( qw( conf doc ) ) {
   eval { opendir( CPDIR, $dir_name ) || die $! };
   if ( $@ ) { 
     warn "Cannot open copy directory $dir_name for reading! ($@) Continuing...\n"; 
     next COPYDIR;
   }
   my @file_list = grep { -f "$dir_name/$_" } readdir( CPDIR );
   closedir( CPDIR );
   foreach my $file_name ( @file_list ) {
     cp( "$dir_name/$file_name", "$base_dir/$dir_name/$file_name" ) 
         || warn "Could not copy file ($dir_name/$file_name) to new directory! Error: $!";
   }
 }

 # Install all the packages and return the status of installing them

 my @package_status = ();

PKG:
 foreach my $package_name ( @WEBSITE_BASE_PKG, @WEBSITE_EXTRA_PKG ) {
   warn " (install_oi): Trying to install package $package_name\n"         if ( DEBUG );
   my $status = { ok => 0, name => $package_name };
   unless ( $package_match{ $package_name } ) {
     $status->{msg} = "Cannot match $package_name to file in distribution! Not installed!";
     push @package_status, $status;
   }
   
   my $pkg = eval { OpenInteract::Package->install_package({ 
                              base_dir => $base_dir,
                              package_file => "$pwd/pkg/$package_match{ $package_name }" 
                    }) };
   if ( $@ ) {
     warn " (install_oi): Error installing: $@\n"                          if ( DEBUG );
     $status->{msg} = $@;
   }
   else {
     $status->{ok} = 1;
     $status->{version} = $pkg->{version};
   }
   push @package_status, $status;
 }
 return \@package_status;
}



#
# check_package
# 
# What we check for:
#   package.conf      -- has name, version and author defined
#   conf/*.perl       -- pass an 'eval' test (through SPOPS::HashFile)
#   OpenInteract/*.pm -- pass a 'require' test
#   MyApp/*.pm        -- pass a 'require' test
#
# Parameters:
#   package_dir
#   package_name
#   website_name (optional)

sub check_package {
 my $p = shift;
 my $status = { ok => 0 };
 my $pwd = cwd;
 chdir( $p->{package_dir} );
 
 # First ensure all the directories and the config exist
 $status->{msg} .= "\n-- Config directory (conf/) does not exist in package!"         unless ( -d "conf/" );
 $status->{msg} .= "\n-- Module directory (OpenInteract/) does not exist in package!" unless ( -d "OpenInteract/" );
 $status->{msg} .= "\n-- Package config (package.conf) does not exist in package!"    unless ( -f "package.conf" );
 if ( $p->{website_name} and ! -d "$p->{website_name}/" ) {
   $status->{msg} .= "\n-- Website directory ($p->{website_name}/) does not exist in package!";
 }
 return $status if ( $status->{msg} );

 # Set this after we do the initial sanity checks
 $status->{ok}++;

 # This is just a warning
 if ( -f 'Changes' ) {
   $status->{msg} .= "\n++ File (Changes) to show package Changelog: ok" ;
 }   
 else {
   $status->{msg} .= "\n-- File (Changes) to show package Changelog: NOT EXISTING" ;
 }

 my $pkg_files = ExtUtils::Manifest::maniread;

 # Now, first go through the config perl files
 my @perl_files = grep /^conf.*\.perl$/, keys %{ $pkg_files };
 foreach my $perl_file ( @perl_files ) {
   my $filestatus = 'ok';
   my $obj = eval { SPOPS::HashFile->new( { filename => $perl_file } ) };
   my $sig = '++';
   if ( $@ ) {
     $status->{ok} = 0;
     $filestatus = "cannot be read in. $@";
     $sig = '--';
   }
   $status->{msg} .= "\n$sig File ($perl_file) $filestatus";
 }

 # Next all the .pm files

 {
   # Suppress warnings within this block
   local $SIG{__WARN__} = sub { return undef };

   my @pm_files = grep /\.pm$/, keys %{ $pkg_files };
   foreach my $pm_file ( @pm_files ) {
     my $filestatus = 'ok';
     my $sig = '++';
     eval { require "$pm_file" };     
     if ( $@ ) {
       $status->{ok} = 0;
       $filestatus = "cannot be require'd. $@";
       $sig = '--';
     }
     $status->{msg} .= "\n$sig File ($pm_file) $filestatus";
   }
 }

 # Now open up the package.conf and check to see that name, version
 # and author exist
 my $config = OpenInteract::Package->read_package_config( "package.conf" );
 $status->{name} = $config->{name};
 my $conf_msg = '';
 unless ( $config->{name} )    { $conf_msg .= "\n-- package.conf: required field 'name' is not defined." }
 unless ( $config->{version} ) { $conf_msg .= "\n-- package.conf: required field 'version' is not defined." }
 unless ( $config->{author} )  { $conf_msg .= "\n-- package.conf: required field 'author' is not defined." }
 if ( $conf_msg ) {
   $status->{msg} .= $conf_msg;
   $status->{ok}   = 0;
 }
 else {
   $status->{msg} .= "\n++ package.conf: ok";
 }

 # Now do the check to ensure that all files in the MANIFEST exist --
 # just get feedback from the manifest module, don't let it print out
 # results of its findings (Quiet)
 $ExtUtils::Manifest::Quiet = 1;
 my @missing = ExtUtils::Manifest::manicheck();
 if ( scalar @missing ) {
   $status->{msg} .= "\n-- MANIFEST files not all in package. Following not found: \n     " .
                     join( "\n     ", @missing );

 }
 else {
   $status->{msg} .= "\n++ MANIFEST files all exist in package: ok";
 }

 # Now do the check to see if any extra files exist than are in the MANIFEST
 my @extra = ExtUtils::Manifest::filecheck();
 if ( scalar @extra ) {
   $status->{msg} .= "\n-- Files in package not in MANIFEST:\n     " .
                     join( "\n     ", @extra );
 }
 else {
   $status->{msg} .= "\n++ All files in package also in MANIFEST: ok";
 }

 $status->{msg} .= "\n";

 chdir( $pwd );
 return $status;
}



#
# check_installed_package
#
# wrapper around check_package for installed packages
# 
# Parameters:
#   package (\@)
#   website_dir | base_dir
sub check_installed_package {
 my $p = shift;
 my $app_name = undef;
 my $use_dir  = $p->{base_dir};
 if ( $p->{website_dir} ) {
   $use_dir = $p->{website_dir};
   my $bc = open_base_config_file( $p->{website_dir} );
   $app_name = $bc->{website_name};
 }
 my @status = ();
PKG:
 foreach my $package_name ( @{ $p->{package} } ) {
   my $pkg = OpenInteract::Package->fetch_by_name({ 
                    name => $package_name,
                    directory => $use_dir
             });
   unless ( $pkg ) {
     push @status, { name => $package_name, ok => 0,
                     msg => 'Package not found! Cannot check package.' };
     next PKG;
   }
   my $package_dir = join( '/', $use_dir, $pkg->{package_dir} );
   push @status, check_package( { package_dir  => $package_dir,
                                  website_name => $app_name,
                                  package_name => $package_name } );
 }
 return \@status;
}



#
# export_package
#
sub export_package {
 my $p = shift;
 my @status = ();
 my @dir_list = ();
 my $pwd = cwd;
 if ( ref $p->{package} eq 'ARRAY' and scalar @{ $p->{package} } ) {

   # Each package specified: try to just open up the
   # $pkg_dir/$pkg_name directory -- this normally works, because
   # when you're doing batches of packages at once you're probably
   # (hopefully) doing it from a devel directory where the packages
   # have no version numbers; if that directory isn't found, we try
   # to find a directory name that matches the package name plus a
   # dash and the beginning of a version number. For instance:
   #
   # $package_name = 'base';
   # @dirs = qw( 'base_config', 'base_doodad', 'base-1.02' );
   #  -- even through the first and second *begin* with 'base' it is
   #  not followed by a '-' and then a number, so it will only match
   #  the third.

   opendir( PKGDIR, $p->{package_dir} ) || die "Cannot read $p->{package_dir}: $!";
   my %package_dirs = map { $_ => 1 } grep ! /^\./, grep { -d "$p->{package_dir}/$_" } readdir( PKGDIR );
   closedir( PKGDIR );
   warn " (export): Found package directories: ", Dumper( \%package_dirs ), "\n" if ( DEBUG );

PKG:   
   foreach my $package_name ( @{ $p->{package} } ) {
     warn " (export): Trying $package_name...\n"                           if ( DEBUG );
     my $target_dir = "$p->{package_dir}/$package_name";
     warn " (export): Testing target $target_dir\n"                        if ( DEBUG );
     unless ( -d $target_dir ) {
       $target_dir = undef;
       while ( my ( $check_dir, $v ) = each %package_dirs ) {
         if ( $check_dir =~ /^$package_name\-\d/ ) {
           $target_dir = "$p->{package_dir}/$check_dir";
           last;
         }         
       }
       unless ( $target_dir ) {
         push @status, { ok => 0, name => $package_name,
                         msg => "Could not find matching directory. No distribution created." };
         next PKG;
       }
     }
     warn " (export): Found target directory $package_name: $target_dir\n" if ( DEBUG );
     push @dir_list, $target_dir;
   }
 }

 # If no package_dir specified, just use the current directory, which
 # is what most people will probably use.

 else {
   push @dir_list, $pwd;
 }
 foreach my $package_dir ( @dir_list ) {
   warn " (export): Exporting directory: $package_dir\n"                   if ( DEBUG );
   chdir( $package_dir );
   my $status_info = OpenInteract::Package->export_package();
   chdir( $pwd );   
   if ( scalar @dir_list > 1 and $status_info->{file} ) {
     my ( $base_file ) = $status_info->{file} =~ m|^.*/(.*)$|;
     rename( $status_info->{file}, "$pwd/$base_file" );
     $status_info->{file} = "$pwd/$base_file";
   }
   push @status, { ok => 1, name => $status_info->{name},
                   msg => "Version $status_info->{version} distribution to $status_info->{file}" };
 }
 return \@status;
}



#
# create_website
#
sub create_website {
 my $p = shift;

 # Create the main directory and subdirectories

 mkdir( $p->{website_dir}, 0775 ) 
      || die "[oi_manage]: Cannot complete comamnd: cannot create website directory ($p->{website_dir}): $!";
 foreach my $sub_dir ( @WEBSITE_SUBDIR, $p->{website_name} ) {
   mkdir ( "$p->{website_dir}/$sub_dir", 0775 ) 
         || die "[oi_manage]: Cannot complete command: failed creating subdir ($p->{website_dir}/$sub_dir): $!";
 }
 
 my $pc = 'OpenInteract::Package'; # shortcut

 # Now do the Stash

 $pc->replace_and_copy( { from_file =>  "$p->{base_dir}/conf/sample-Stash.pm",
                          to_file   => "$p->{website_dir}/$p->{website_name}/Stash.pm",
                          from_text => [ 'OpenInteract::SampleStash' ],
                          to_text   => [ "$p->{website_name}\:\:Stash" ] } );
 
 # Now do the conf and other files

 cp( "$p->{base_dir}/conf/sample-apache.dat", "$p->{website_dir}/conf/apache.dat" ) 
   || die "[oi_manage]: Failure! Cannot copy apache module listing: $!";

 # Copy over all OI system documentation.

 my $html_doc_root = "$p->{website_dir}/html/oi_docs";
 eval { mkdir( $html_doc_root, 0775 ) || die $! };
 if ( $@ ) {
   warn "Cannot create directory 'html/oi_docs' in website directory, \n",
        "so I cannot copy OpenInteract documentation to viewable location. Continuing...\n";
 }
 else {
   my $oi_doc_root = "$p->{base_dir}/doc";
   eval { opendir( DOC, $oi_doc_root ) || die $! };
   if ( $@ ) {
     warn "Cannot read system documentation from ($oi_doc_root). Please ask\n",
          "your system administrator to install it there..\n",
          "Continuing without system documentation available.\n";
   }
   else {
     my @file_list = grep { -f "$oi_doc_root/$_" } readdir( DOC );
     foreach my $file_name ( @file_list ) {
       cp( "$oi_doc_root/$file_name", "$html_doc_root/$file_name" )
           || warn "Could not copy ($file_name) from system documentation directory ($!).\n";
     }
   }
 }

 my $replace_keys = [ '%%INTERACT_DIR%%', '%%WEBSITE_DIR%%', 
                      '%%WEBSITE_NAME%%', '%%STASH_CLASS%%' ];
 my $replace_vals = [ $p->{base_dir}, $p->{website_dir}, 
                      $p->{website_name}, "$p->{website_name}\:\:Stash" ];
 $pc->replace_and_copy( { from_file => "$p->{base_dir}/conf/sample-httpd_modperl.conf",
                          to_file   => "$p->{website_dir}/conf/httpd_modperl.conf",
                          from_text => $replace_keys,
                          to_text   => $replace_vals } );

 $pc->replace_and_copy( { from_file => "$p->{base_dir}/conf/sample-httpd_static.conf",
                          to_file   => "$p->{website_dir}/conf/httpd_static.conf",
                          from_text => $replace_keys,
                          to_text   => $replace_vals } );
 
 $pc->replace_and_copy( { from_file => "$p->{base_dir}/conf/sample-base.conf",
                          to_file   => "$p->{website_dir}/conf/base.conf",
                          from_text => $replace_keys,
                          to_text   => $replace_vals  } );

 $pc->replace_and_copy( { from_file => "$p->{base_dir}/conf/sample-server.perl",
                          to_file   => "$p->{website_dir}/conf/server.perl",
                          from_text => $replace_keys,
                          to_text   => $replace_vals } );
 
 $pc->replace_and_copy( { from_file => "$p->{base_dir}/conf/sample-startup.pl",
                          to_file   => "$p->{website_dir}/conf/startup.pl",
                          from_text => $replace_keys,
                          to_text   => $replace_vals } );

 $pc->replace_and_copy( { from_file => "$p->{base_dir}/INSTALL.website",
                          to_file   => "$p->{website_dir}/INSTALL.website",
                          from_text => $replace_keys,
                          to_text   => $replace_vals } );

 # Now install the packages 

 return apply_package( { package      => \@WEBSITE_BASE_PKG, 
                         base_dir     => $p->{base_dir},
                         website_name => $p->{website_name},
                         website_dir  => $p->{website_dir} } ); 
}



#
# apply_package
#
# requires: base_dir, website_dir

sub apply_package {
 my $p = shift;

 # Grab the base configuration information to find the
 # website_name

 unless ( $p->{base_dir} and $p->{website_name} ) {
   my $bc = open_base_config_file( $p->{website_dir} );
   $p->{website_name} ||= $bc->{website_name};
   $p->{base_dir}     ||= $bc->{base_dir};
 }
 warn " (apply): Incoming info (after base): ", Dumper( $p ), "\n"         if ( DEBUG );

 # Ensure that the packages passed in are actual packages by fetching
 # the ones named

 my $pkg_exist = OpenInteract::Package->verify_packages( 
                       { directory => $p->{base_dir} },
                       @{ $p->{package} } 
                 );

 # We create a hash of all the packages passed in and for each
 # existing one remove it -- anything left after processing all
 # packages didn't get processed and is therefore not a verified
 # (good) package to begin with

 my %pkg_track = map { lc $_ => 1 } @{ $p->{package} };

 # Track status

 my @status = ();

 # Cycle through the installed packages -- first see if the package
 # already exists in the website; if not, change a couple of
 # parameters and run the 'install_to_website' method of the
 # package, then save the package to our website db

PACKAGE:
 foreach my $pkg ( @{ $pkg_exist } ) {
   delete $pkg_track{ lc $pkg->{name} };
   warn " (apply): Try to apply package $pkg->{name}-$pkg->{version}\n"    if ( DEBUG );
   my $this_status = { ok => 0, name => $pkg->{name} };
   my $app_pkg = OpenInteract::Package->fetch_by_name({
                     name => $pkg->{name}, 
                     directory => $p->{website_dir} 
                 });
   if ( $app_pkg ) {
     $this_status->{msg} = "This package (Version: $app_pkg->{version}) already exists in " .
                           "the website. Please run 'upgrade_package' to upgrade it.";
     push @status, $this_status;
     next PACKAGE;
   }
   $this_status->{version} = $pkg->{version};
   $pkg->{website_dir}  = $p->{website_dir};
   $pkg->{website_name} = $p->{website_name};
   $pkg->{installed_on} = $pkg->now;
   eval { $pkg->install_to_website() };
   if ( $@ ) {
     $this_status->{msg} = "Cannot install package to website. Error: $@\n";
   }
   else {

     # Do the switcheroo! Even though the object was retrieved from
     # the interact package db, we can save it to the website package
     # db by passing a new directory

     eval { $pkg->save( { directory => $p->{website_dir} } ) };
     if ( $@ ) {
       $this_status->{msg} = "Website directories installed, but entry not in website database. Error: $@";
     }

     # If the status is ok, open up the 'INSTALL' file from the base
     # installation directory (if it exists) and put it into the
     # {notes} field of the status. This is used for upgrade/other
     # notes and displayed in 'print_status_line' below.

     else {
       $this_status->{ok}++;
       my $notes_file = join( '/', $pkg->{base_dir}, $pkg->{package_dir}, 'INSTALL' );
       if ( -f $notes_file ) {
         eval { open( NOTES, $notes_file ) || die $! };
         if ( $@ ) {
           $this_status->{notes} = "Failed to open package installation notes file. Error: $@";
         }
         else {
           local $/ = undef;
           $this_status->{notes} = <NOTES>;
           close( NOTES );
         }
       }
     }
   }
   push @status, $this_status;
 }

 # Now report status of the packages not verified
 foreach my $pkg_name ( sort keys %pkg_track ) {

   push @status, { ok => 0, msg => "Package could not be verified -- either it " .
                                   "doesn't exist in base install or there's " .
                                   "a problem with the base install package." };
 }
 return \@status;
}

#
# remove_package
#
# Required: website_dir, package list

sub remove_package {
 my $p  = shift;
 my @status = ();
PKG:
 foreach my $package_name ( @{ $p->{package} } ) {
   my $this_status = { ok => 0, name => $package_name };
   my $pkg = OpenInteract::Package->fetch_by_name({ 
                 name => $package_name, 
                 directory => $p->{website_dir} 
             });
   unless ( $pkg ) {
     $this_status->{msg} = 'Package does not exist in this website!';
     push @status, $this_status;
     next PKG;
   }
   $this_status->{version} = $pkg->{version};
   eval { $pkg->remove( { directory => $p->{website_dir} } ) };
   if ( $@ ) {
     $this_status->{msg} = "Failed to remove. Error: $@";
   }
   else {
     $this_status->{ok}++;
   }
   push @status, $this_status;
 }
 return \@status;
}

#
# upgrade_package
#
# Accomplish this by removing the old package definition and then
# applying the new package

sub upgrade_package {
 my $p = shift;
 unless ( $p->{base_dir} ) {
   my $bc = open_base_config_file( $p->{website_dir} );
   $p->{base_dir} ||= $bc->{base_dir};
 }

 my @status = ();

PKG:
 foreach my $package_name ( @{ $p->{package} } ) {
   my $this_status = { ok => 0, name => $package_name };

   # First, ensure that the package exists in the local app

   my $app_pkg = OpenInteract::Package->fetch_by_name({ 
                     directory => $p->{website_dir},
                     name => $package_name 
                 });
   unless ( $app_pkg ) {
     $this_status->{msg} = "Package does not exist in website -- run 'apply_package' to " .
                           "install from OpenInteract installation to this website. No action taken.";
     push @status, $this_status;
     next PKG;
   }
   
   # Next, ensure that the package exists in the main app
   my $base_pkg = OpenInteract::Package->fetch_by_name({
                      directory => $p->{base_dir},
                      name => $package_name 
                  });
   unless ( $base_pkg ) {
     $this_status->{msg} = "Package does not exist in base install -- cannot upgrade.";
     push @status, $this_status;
     next PKG;
   }

   # Ensure that the versions are different

   unless ( $base_pkg->{version} > $app_pkg->{version} ) {
     $this_status->{msg} = "Installed package does not have version higher than package in website. " .
                           "No action taken.";
     push @status, $this_status;
     next PKG;
   }
      
   # Looks good; let's do it. First remove the website package

   my $app_status = [ { ok => 0, msg => 'Did not run since remove failed' } ];
   my $upg_status = { ok => 0 };
   my $rmv_status = remove_package( { website_dir => $p->{website_dir},
                                      package     => [ $package_name ] } );

   # If removal went ok, apply the package from the install directory
   if ( $rmv_status->[0]->{ok} ) {
     $app_status = apply_package( { base_dir    => $p->{base_dir},
                                    website_dir => $p->{website_dir},
                                    package     => [ $package_name ] } );
     if ( $app_status->[0]->{ok} ) {
       $upg_status->{ok}++;
       $upg_status->{msg} = "upgraded to version $base_pkg->{version}";
     }
   }
   $this_status->{ok}++;
   $this_status->{msg} = "  Removal: " . print_status_line( $rmv_status->[0], { same_line => 1 } ) . "\n" .
                         "  Apply:   " . print_status_line( $app_status->[0], { same_line => 1 }  ) . "\n" .
                         "  Upgrade: " . print_status_line( $upg_status );
   push @status, $this_status;
 }
 return \@status;
}



#
# install_sql
#
sub install_sql {
  my $p = shift;
  my $R = initialize_db_actions( $p->{website_dir}, 'install SQL' );
  my $pkg_exist = OpenInteract::Package->verify_packages( 
                       { directory => $p->{website_dir} },
                       @{ $p->{package} } );
  my @status = ();
  my %pkg_status = ();

  # Go through and do the structures first...
  
  foreach my $pkg ( @{ $pkg_exist } ) {
    warn " -- Starting structure install for $pkg->{name}-$pkg->{version}\n" if ( DEBUG );
    $pkg_status{ $pkg->{name} }->{version}   = $pkg->{version};
    $pkg_status{ $pkg->{name} }->{ok} = 1;
    my $installer_class = OpenInteract::SQLInstall->require_package_installer( $pkg );
    if ( $installer_class ) {
      my %args = ( package => $pkg, config => $R->CONFIG, 
                   db => $R->db, status => 'raw' );
      my $struct_status = eval { $installer_class->apply({ 
                                            %args, action => 'create_structure' }) };
      if ( $@ ) { 
        $pkg_status{ $pkg->{name} }->{ok} = 0;
      }
      else {
        $pkg_status{ $pkg->{name} }->{installer} = $installer_class;
        $pkg_status{ $pkg->{name} }->{structure} = join( "\n  * ", "Structure:", map { $_->{msg} } @{ $struct_status } );
        my @notes = grep { defined $_ } map { $_->{note} } @{ $struct_status };
        if ( @notes ) { $pkg_status{ $pkg->{name} }->{note} = join( "\n", @notes ) }
      }
    }
  }

  # Then the data and security; this is so we don't try to create data
  # in (as yet) non-existent structures. We also check to see the 'ok'
  # status before each entry so that we don't enter data and/or
  # security when the table creation failed, etc.

  foreach my $pkg ( @{ $pkg_exist } ) {
    my $installer_class = $pkg_status{ $pkg->{name} }->{installer};
    my $name = $pkg->{name};
    if ( $installer_class ) {
      my %args = ( package => $pkg, config => $R->CONFIG, 
                   db => $R->db, status => 'raw' );
      if ( $pkg_status{ $name }->{ok} ) {
        my $data_status = eval { $installer_class->apply({ 
                                              %args, action => 'install_data' }) };
        if ( $@ ) { 
          $pkg_status{ $pkg->{name} }->{ok} = 0;
        }
        else {
          $pkg_status{ $pkg->{name} }->{data} = join( "\n  * ", 'Data: ', map { $_->{msg} } @{ $data_status } );
          my @notes = grep { defined $_ } map { $_->{note} } @{ $data_status };
          if ( @notes ) { $pkg_status{ $pkg->{name} }->{note} .= join( "\n", @notes ) }
        }
      }
      
     if ( $pkg_status{ $name }->{ok} ) {
       my $security_status = eval { $installer_class->apply({ 
                                               %args, action => 'install_security' }) };
       if ( $@ ) { 
         $pkg_status{ $pkg->{name} }->{ok} = 0;
       }
       else {
         $pkg_status{ $pkg->{name} }->{security} = join( "\n  * ", 'Security: ', map { $_->{msg} } @{ $security_status } );
         my @notes = grep { defined $_ } map { $_->{note} } @{ $security_status };
         if ( @notes ) { $pkg_status{ $pkg->{name} }->{note} .= join( "\n", @notes ) }
       }
     }
   }
 }

 $R->db->disconnect;

 foreach my $name ( sort keys %pkg_status ) {
   my $this_status = { ok => $pkg_status{ $name }->{ok},
                       name => $name, 
                       version => $pkg_status{ $name }->{version} };
   if ( $pkg_status{ $name }->{installer} ) {
     $this_status->{msg} = "\n\n" . join( "\n", $pkg_status{ $name }->{structure},
                                                $pkg_status{ $name }->{data},
                                                $pkg_status{ $name }->{security} ) . "\n";
     $this_status->{note} = $pkg_status{ $name }->{note};
   }
   else {
     $this_status->{msg} = "No installer defined, no changes made for package.\n";
   }
   push @status, $this_status;
 }
 return \@status;
}



#
# install_template
#

sub install_template {
 my $p = shift;
 my $R = initialize_db_actions( $p->{website_dir}, 'install templates' );

 # This is the class of our template object
 my $TMPL_CLASS = $R->site_template;
 
 # Get the ID of our admin group for security setting later on
 my $site_admin_id = $R->CONFIG->{default_objects}->{site_admin_group};

 # Cycle through the packages in the website package db and bring
 # in all their templates
 my @status = ();
PACKAGE:
 foreach my $package_name ( @{ $p->{package} } ) {
   my $this_status = { ok => 1, name => $package_name };
   my $this_status_msg = "\n";
   my $pkg  = OpenInteract::Package->fetch_by_name({
                  name => $package_name, 
                  directory => $p->{website_dir} 
              });
   unless ( $pkg ) {      
     $this_status->{ok} = 0;
     $this_status->{msg} = "Cannot find a package matching $package_name! No templates processed."; 
     push @status, $this_status;
     next PACKAGE;
   }
   my $pkg_dir      = join( '/', $pkg->{website_dir}, $pkg->{package_dir} );
   my $template_dir = join( '/', $pkg_dir, $TMPL_DIR );

   # First, find all the .meta files in the package -- every template
   # must have a .meta file
   opendir( META, $template_dir ) || die "Cannot open directory ($template_dir): $!";
   my @meta_files = grep { -f "$template_dir/$_" } grep /\.$META_EXT$/, readdir( META );
   closedir( META );
   
   # Now cycle through them, read them in and parse them,
   # constructing a template from either one already saved or
   # creating a new object, then read the .tmpl file into the object
   foreach my $meta_file ( @meta_files ) {
     my $part_meta = $meta_file;
     $part_meta =~ s/\.$META_EXT$//;
     my $full_meta = join( '/', $template_dir, $meta_file );
     my $ti = {};
     open( MF, $full_meta ) || die "Cannot open ($full_meta): $!";
     while ( <MF> ) {
       chomp;
       next if ( /^\s*\#/ ); # skip comments in meta file only
       my ( $action, $info ) = /^(\w+):\s*(.*)$/;
       if ( $action ) { 
         $ti->{ $action } = $info;
       }
       else { 
         $ti->{description} .= $_;
       }
     }
     
     # Now, see if we can retrieve an existing template using the
     # package/name combo
     my $tmpl_list = $TMPL_CLASS->fetch_group({
                          where => 'package = ? AND name = ?',
                          value => [ $ti->{package}, $ti->{name} ],
                          skip_security => 1, skip_cache => 1 
                     });
     
     # ... if so, just set the title and description right now
     my $tmpl = $tmpl_list->[0];
     if ( $tmpl ) {
       $tmpl->{title}       = $ti->{title};
       $tmpl->{description} = $ti->{description};
       $this_status_msg .= "$ti->{name} found in database ($tmpl->{template_id}), updating... ";
     }

     # Otherwise, create a new template object and set all the
     # parameters available
     else {
       $tmpl = $TMPL_CLASS->new( $ti );
       $this_status_msg .= "$ti->{name} being created as new... ";
     }

     # Read in the full template, which also includes the script part
     # of the object
     my $full_template_file = join( '/', $template_dir, "$part_meta.$TMPL_EXT" );
     open( TMPL, $full_template_file ) || die "Cannot open template file: $full_template_file: $!";
     { 
       local $/ = undef;
       my $full_tmpl = <TMPL>;
       ( $tmpl->{template}, $tmpl->{script} ) = split /$SCRIPT_SEPARATE/, $full_tmpl, 2;
     }
     close( TMPL ); 

     # Save the object
     eval { $tmpl->save( { skip_security => 1, skip_log => 1, skip_cache => 1 } ) };
     if ( $@ ) {
       my $ei = SPOPS::Error->get;
       $this_status_msg .= "failed! Could not save template object. Error: $@ -- $ei->{system_msg}";       
     }

     # If save ok, we need to set security
     else {
       $tmpl->set_security( { scope => SEC_SCOPE_WORLD,
                              level => SEC_LEVEL_READ } );
       $tmpl->set_security( { scope => SEC_SCOPE_GROUP,
                              scope_id => $site_admin_id,
                              level => SEC_LEVEL_WRITE } );
       $this_status_msg .= "ok! Template saved and security set ok.";
     }
     $this_status_msg .= "\n";
   }
   $this_status->{msg} = $this_status_msg;
   push @status, $this_status;
 }
 $R->db->disconnect;
 return \@status;
}


#
# remove_template
#

sub remove_template {
 my $p = shift;
 my $R = initialize_db_action( $p->{website_dir}, 'remove template' );

 # This is the class of our template object
 my $TMPL_CLASS = $R->site_template; 

 my @status = ();

 foreach my $package_name ( @{ $p->{package} } ) {
   my $this_status = { name => $package_name, ok => 1 };
   my $template_list = $TMPL_CLASS->fetch_group({ 
                            where => 'package = ?',
                            value => [ $package_name ] 
                       });
   foreach my $tmpl ( @{ $template_list } ) {
     eval { $tmpl->remove( { skip_security => 1, skip_log => 1 } ) };
     if ( $@ ) {
       $this_status->{ok} = 0;
       push @{ $this_status->{msg_list} }, "Template $tmpl->{name}: Failed to remove - $@";
     }
     else {
       push @{ $this_status->{msg_list} }, "Template $tmpl->{name}: removed ok";
     }
   }
   $this_status->{msg} = join( "\n", @{ $this_status->{msg_list} } );
   $this_status->{msg_list} = undef;
   push @status, $this_status;
 }
 $R->db->disconnect;
 return \@status;
}


# 
# dump_template
#

sub dump_template {
 my $p = shift;
 my $R = initialize_db_actions( $p->{website_dir}, 'dump templates' );

 # This is the class of our template object
 my $TMPL_CLASS = $R->site_template; 

 my @status = ();

PKG:
 foreach my $package_name ( @{ $p->{package} } ) {
   my $this_status = { name => $package_name, ok => 1 };
   my $pkg = OpenInteract::Package->fetch_by_name({
                 name => $package_name, 
                 directory => $p->{website_dir} 
             });
   unless ( $pkg ) {
     $this_status->{msg} = 'No templates dumped -- package not found in website.';
     push @status, $this_status;
     next PKG;
   }

   # If we specified a dump directory then just use that; otherwise,
   # create a dump dir and ensure it's clean before we fill it.

   my $template_dir  = $p->{dump_dir};
   unless ( $template_dir ) {
     $template_dir = join( '/', $pkg->{website_dir}, 
                                $pkg->{package_dir}, 
                                $TMPL_DIR, 
                                'dump' );
     if ( -d $template_dir ) {
       system( 'rm', '-rf', $template_dir );
     }
     mkdir( $template_dir, 0775 ) || die "[oi_manage]: Cannot create template directory $template_dir: $!";
   }
   my $template_list = $TMPL_CLASS->fetch_group({
                            skip_security => 1,
                            where => 'package = ?',
                            value => [ $package_name ] 
                       });
   $this_status->{msg_list} = [];
   foreach my $tmpl ( @{ $template_list } ) {
     my $filename = $tmpl->{name};
     $filename =~ s/[\s=\+!@\#\$%&*()\[\]\\\/]/_/g;   
     my $new_base = "$template_dir/$filename";

     # First dump the template itself, then the script information
     my $template_file = "$new_base.$TMPL_EXT";
     open( TMPL, "> $template_file" ) || die "Cannot open ($template_file) for writing: $!";
     print TMPL $tmpl->{template};
     print TMPL  "\n\n$SCRIPT_SEPARATE\n\n$tmpl->{script}"  if ( $tmpl->{script} );
     close( TMPL );
     
     # Then do the meta information
     my $meta_file = "$new_base.$META_EXT";
     open( META, "> $meta_file" ) || die "Cannot open $meta_file for writing: $!";
     print META "name: $tmpl->{name}\n",
                "title: $tmpl->{title}\n",
                "package: $package_name\n",
                "$tmpl->{description}";
     close( META );
     push @{ $this_status->{msg_list} }, "Template $tmpl->{name}: dumped ok";
   }
   $this_status->{msg} = "directory: $template_dir\n     --" . 
                         join( "\n     --", @{ $this_status->{msg_list} } );
   $this_status->{msg_list} = undef;
   push @status, $this_status;
 }
 $R->db->disconnect;
 return \@status;
}


#
# test_db_connection
#

sub test_db_connection {
 my $website_dir = shift;
 my $status = { ok => 1 };

 # Grab the base config, then the config object
 my $bc = open_base_config_file( $website_dir );
 my $C = OpenInteract::Startup->create_config( { base_config => $bc } );
 if ( ! $C->{db_info}->{dsn} ) {
   $status->{ok}  = 0;
   $status->{msg} = "You must at least define 'dsn' in the 'db_info' key of the configuration information.";
 }
 else {
   my $db = eval { OpenInteract::DBI->connect( $C->{db_info} ) };
   if ( $@ ) {
     my ( $error ) = $@ =~ /failed:\s*(.*)$/;
     $error ||= $@;
     if ( $@ =~ /^Use/ ) {
       $status->{msg} = "\n -- Basic connect: ok" .
                        "\n -- Basic use database: failed -- $error";
     }
     else {
       $status->{msg} = "\n -- Basic connect: failed -- $error" .
                        "\n -- Basic use database: not attempted";
     }
     $status->{ok}   = 0;
   }
   else {
     $status->{msg} = "\n -- Basic connect: ok" .
                      "\n -- Basic use database: ok";
     $db->disconnect;
   }
 }
 return $status;
}


#
# initialize_db_actions
#

sub initialize_db_actions {
 my $website_dir = shift;
 my $action      = shift;

 unshift @INC, $website_dir; # so we can find our stash class...

 # Grab the base config, then the config object
 my $bc = open_base_config_file( $website_dir );
 my ( $init, $C ) = OpenInteract::Startup->main_initialize({ 
                        base_config => $bc,
                        alias_init => 1,
                        spops_init => 1 
                    });

 my $REQUEST_CLASS = $C->{request_class};
 my $R = $REQUEST_CLASS->instance;
 $R->{stash_class} = $C->{stash_class};
 $R->stash( 'config', $C );

 # Do DBI connect
 my $dbh = eval { OpenInteract::DBI->connect( $C->{db_info} ) };
 if ( $@ ) {
   my $error_msg = <<MSG;
[oi_manage]: Failed to $action. Reason: could not open database
handle.

Specific error: $@ 

Are you sure all the information necessary to connect to a DBI
database is in your configuration file?

Configuration file used: $website_dir/conf/server.perl
MSG
   die $error_msg;
 }
 $R->stash( 'db', $dbh );
 return $R;
}



sub open_base_config_file {
 my $dir = shift;
 my $base_conf_file = join( '/', $dir, 'conf', 'base.conf' );
 my $bc = OpenInteract::Startup->read_base_config({
              filename => $base_conf_file 
          });
 unless ( $bc ) {
   my $msg = "[oi_manage]: Failure! Cannot find base configuration. Does " .
             "the file $base_conf_file exist in your website?";
   die wrap( undef, undef, $msg );
 }
 return $bc;
}



sub print_status_line {
 my $status = shift;
 my $opt    = shift;
 my $line = '';
 if ( $status->{name} ) {
   $line = $status->{name};   
   $line .= " ($status->{version})" if ( $status->{version} );
   $line .= "\n"  unless ( $opt->{same_line} );
   $line .= '  ';
 }
 
 $line .= ( $status->{ok} ) ? 'OK' : 'FAILED!';
 $line .= ': ' . $status->{msg}   if ( $status->{msg} );
 $line .= "\n\nNotes from Package Author (READ THESE)\n$status->{notes}\n$SEP\n"  if ( $status->{notes} );
 return $line;
}

__END__

=pod

=head1 NAME

oi_manage - Manage OpenInteract websites and packages

=head1 SYNOPSIS

oi_manage [options] [command]

Administration commands: 

   install, install_package

Package development commands: 

   create_skeleton, export_package, check_package

Website creator commands:

   create_website, apply_package, upgrade_package, 
   remove_package, install_sql, install_template, 
   dump_template, remove_template, test_db

Other commands:

   initial_packages, list_packages, list_actions

For more information, run 'oi_manage --man'

=head1 COMMON COMMANDS

Commands by the Administrator:

 install            - Install OpenInteract
 install_package    - Install package to the base

Commands by the Package Developer:

 create_skeleton    - Create a skeleton package for development
 export_package     - Export package(s) to distribution file(s)
 check_package      - Ensure that package passes initial inspection

Commands by the Website Creator:

 create_website     - Create a new website
 apply_package      - Install a package from base to website
 upgrade_package    - Upgrade a website package
 remove_package     - Remove a package from a website
 install_sql        - Install the SQL for packages
 install_template   - Install package templates to the database
 dump_template      - Dump package templates to the filesystem
 remove_template    - Remove package templates from the database
 test_db            - Test database settings in 'server.perl'

Other commands:

 initial_packages   - List packages marked as 'initial'
 list_packages      - List packages installed to app or base
 list_actions       - List actions currently implemented in website

=head1 COMMON OPTIONS

Summary of common options:

 --base_dir           OpenInteract install directory
 --website_dir        Website install directory
 --website_name       Website name
 --package_dir        Directory with package subdirectories (usually devel)
 --package_file       Distribution file containing an OpenInteract package
 --package            List packages to operate on
 --package_list_file  File specifying packages to operate on
 --dump_dir           Directory to dump stuff into
 --help               Display brief help
 --man                Display full help

Details:

 --base_dir=/path/to/OpenInteract

    Name the directory where OpenInteract is installed. You can set
    the environment variable 'OPENINTERACT' instead of passing the
    value on the command-line, We recommend you set this environment
    variable for all OpenInteract users and developers.

 --website_dir=/path/to/OpenInteract/website

    Name the directory where an OpenInteract website is
    installed. This directory will have the website package database
    in the 'conf/' directory. You can set the environment variable
    'OIWEBSITE' instead of passing the value on the command
    line. However, setting this permanently will cause you problems,
    so it is best to set on a temporary basis.

 --website_name=MyAppName

    Name of your website. Must be all one word (no underscores or
    anything), and StudlyCaps are A-OK (in fact, recommended). Note
    that this name becomes your perl namespace, so all your packages
    become 'MyAppName::News' and 'MyAppName::WebLink::Handler', etc.

 --package_dir=/path/to/my/devel/packages

    Name the directory where you do your OpenInteract
    development. This is used by the 'export_package' and the
    'check_package' commands. This directory can also be where a
    single package is -- we also look at the 'package' parameter to
    discern which way to use 'package_dir'.

 --package_file=an-oi-package-x.xx.tar.gz

    OpenInteract packages are distributed in common tarballs, which
    can be created by the 'export_package' command and installed by
    the 'install_package' command.

 --package=a,b,c,d  OR --package=a --package=b --package=d

 --package_list_file=/path/to/package_list

    File containing package names, one per line, without version
    numbers or anything else. Blank lines and lines beginning with a
    '#' are skipped. You can substitute this wherever you see
    '--package' specified as a parameter in the discussions below.

 --dump_dir=/path/to/dump/stuff

    Specify a directory where you would like to dump something -- such
    as the SQL for a package or the templates belonging to a
    package. Dumping routines typically have a designated place for
    this (usually the 'dump/' directory in the area pertaining to what
    is being dumped), but sometimes you might want to put the data
    elsewhere.

Other options depend on the I<command> you choose and are listed under
that command below.

=head1 DESCRIPTION

B<oi_manage> is the command-line interface for managing packages
within OpenInteract and installing new OpenInteract websites. It is
also useful for developers so they can export their work into a
I<tar.gz> file for distribution, or install it into the OpenInteract
package database.

=head1 COMMANDS

The following tools and actions are available from B<oi_manage>:

=head2 INSTALL

Command: install

Install OpenInteract. Note that you must be in the OpenInteract source
directory to run this command. For instance, a typical installation
might look like the following sequence:

 >> tar -zxvf OpenInteract-1.01.tar.gz
 >> cd OpenInteract-1.01
 >> perl Makefile.PL
 >> make
 >> make test
 >> make install
 (file 'oi_manage' is now in /usr/local/bin)
 >> /usr/local/bin/oi_manage --base_dir=/opt/OpenInteract install

You should only ever need to do this once. But just in case, it might
be a good idea to keep the initial source directory around.

=head2 INITIAL PACKAGES

Command: initial_packages

Just lists the initial package B<oi_manage> is currently configured to
install when given a 'create_website' command.

=head2 LIST PACKAGES

Command: list_packages

Required options -- one of the following:

 --base_dir=/path/to/OpenInteract
 --website_dir=/path/to/my_website

List the packages currently installed in a website or in the base
OI installation.

=head2 LIST ACTIONS

Command: list_actions

Required options:

 --website_dir=/path/to/my_website

Bootstrap an OpenInteract environment from the command line and
inspect it to see what actions are created in the action table.

Output includes the action name, the package the action is found in,
and either the class and method used to call it or the template which
implements it.

This can be extremely useful if you are unsure what actions your
website currently implements, and for ensuring that you do not chose
an action for your new package that is already in use elsewhere.

=head2 CREATE SKELETON

Command: create_skeleton

Required options:

 --package=mypackagename
 --base_dir=/path/to/OpenInteract

Creates skeleton package(s) in your current directory for
development. 

This includes:

=over 4

=item *

The necessary directories

=item *

An initial C<package.conf> file

=item *

A documentation template and index

=item *

Commented sample C<conf/spops.perl> and C<conf/action.perl>,
configuration files

=item *

Commented sample C<OpenInteract/SQLInstall> file

=item *

Commented sample template files in C<template/dummy.meta>, and
C<template/dummy.tmpl>

=item *

A starter changelog (Changes) and

=item *

A working MANIFEST file along with MANIFEST.SKIP with common patterns
for files not to include in the MANIFEST.

=back

If you specify multiple packages, multiple directories will be created
in your current directory.

=head2 EXPORT PACKAGE

Command: export_package

You can run this one of two ways:

=over 4

=item 1. Export a single package by changing to its directory and
running this command without any parameters.

=item 2. Specify the following parameters for multiple packages:

 --package_dir=/path/to/my/packages
 --package=x (at least one)

All packages you specify that the command can find in the
'package_dir' directory will get distributions created.

=back

This is a utility for people developing new packages for
OpenInteract. Some might consider it a Bad Idea to develop packages
under the base OpenInteract/pkg directory -- for new uninstalled packages
it is not a problem, but once you start doing that you start working
on packages in-place, and before you know it everything it out of
whack. Best not go there.

So what this utility does is peek into a directory for a
'package.conf' file. This is a simple file with information about your
package in a simple format. Here is an example:

 name           MyKillerApp
 version        1.14
 dependency     YourKillerApp 1.20
 dependency     TheirLameApp  0.89
 author         Zippy Doodad (zippy@doodad.com)
 author         Bolt (bolt@uno.com)
 url            http://mykillerapp.com/
 sql_installer  OpenInteract::SQLInstall::MyKillerApp
 description
 This package implements what everybody -- especially the town of
 Ottumwa, Iowa -- thinks is a KillerApp. You will too.

So we read in configuration and ensure you have the required
fields. (Currently, the fields 'name', 'version' and 'author' are
required, although no validation is performed on them.)

If the configuration file passes muster, we then check out the
MANIFEST that accompanies your package. MANIFEST lists the files that
should be distributed with your package. 

If 'export_package' finds any files in MANIFEST not in the directory
or if it finds files in the directory B<not> specified in MANIFEST, it
will give you a warning but still create the distribution. You then
have the option of distributing your package as-is (probably a bad
idea, but you might have your reasons) or fixing the problem and
re-creating the distribution. You can go through this process as many
times as necessary since the 'export_package' command does not change
any information in your package.

You may also specify a MANIFEST.SKIP file that determines which files
should not be compared to the MANIFEST to ensure that you do not have
any extra files floating around. Each line in the MANIFEST.SKIP file
is a regular expression matching files that should B<not> be included
in MANIFEST. For example, if you specify in MANIFEST.SKIP:

 \bCVS\b

Then when this command finds files matching this pattern (all your CVS
directories) in your package directory, it will not complain and tell
you there are extra files in the directory.

(Since we use L<ExtUtils::Manifest> to manipulate the MANIFEST file,
including the MANIFEST.SKIP file, you might find it helpful to read
more about it.)

The command 'create_skeleton' creates a default MANIFEST and
MANIFEST.SKIP for you, although it is your responsibility to add your
files to MANIFEST and your exclusion patterns to MANIFEST.SKIP after
that.

Once we get this file and directory listing, we use them to create a
B<distribution file> -- just a '.tar.gz' file conforming to certain
standards -- suitable for installation in another OpenInteract
installation with the 'install_package' command.

=head2 CHECK PACKAGE

Command: check_package

Required options:

One of the following:

=over 4

=item 1.

None (check the package in the current directory)

=item 2.

Check the package in a particular directory:

 --package_dir=/path/to/my/devel/package

=item 3.

Check one or more packages in a particular website:

 --website_dir=/path/to/my_website
 --package=x (at least one)

=item 4.

Check one or more packages in the installation directory.

 --base_dir=/path/to/OpenInteract
 --package=x (at least one)

=back

This command checks that a package has the bare necessities and that
the files at least pass some basic sanity checks.

Files we check:

 Changes
 package.conf
 conf/*.perl
 *.pm
 <website-name>/*.pm (if this is a website)

We also ensure that the files found in MANIFEST are all there give you
a warning if there are extra files in the directory not found in
MANIFEST.

It is probably a good idea to always run this before you send a
package to someone else. (Commands like this usually spring from the
embarrassment of someone else, so learn the lesson :)

This leads to the common idiom of:

 > ... work on package ...

 > cd /my/package/devel/dir

 > oi_manage check_package
 (all is ok)

 > oi_manage export_package

 > scp mypkg-1.21.tar.gz me@mywebste:/home/httpd/pkg

 > ...

=head2 INSTALL PACKAGE

Command: install_package

Required options:

 --base_dir=/path/to/OpenInteract
 --package_file=/path/to/package-x.xx.tar.gz

Installs a package from a B<distribution file>, which are just tar.gz
files with the package information in them. The steps to install the
package include:

=over 4

=item *

Unpack the distribution and determine the package name and version

=item *

See if that package and version are already installed

=item *

Copy the files to the base OpenInteract directory

=item *

Install the package information to the OpenInteract package database

=back

=head2 CREATE WEBSITE

Command: create_website

Required options:

 --website_name=MyAppName
 --website_dir=/path/to/my_website
 --base_dir=/path/to/OpenInteract

Creates a new directory for your website and all the necessary
subdirectories, so ensure that 'website_dir' does not exist
yet. 

This command also copies over configuration files and replaces various
keys with ones specific to your website. The program creates a
simple stash class for you as well as installs the base packages
necessary for OpenInteract to function.

After running this command, you typically have to only edit some
configuration files and your website can be up and running! See
the file C<INSTALL.website> distributed with O0penInteract for
more information.

=head2 TEST DB

Command: test_db

Required options:

 --website_dir=/path/to/my_website

Just tests out whether you can establish a connection to the database
for which you have specified parameters in the C<conf/server.perl>
configuration file in your website.

***NOTE***

Just because your database connection works from the command line does
B<NOT> mean that it will automatically work from your web
server. Hopefully, your web server runs under a user with minimal
permissions, which can affect shared library and other types of
access. In addition, your command-line environment might be set up
properly to connect to your database while the web server environment
is not. More is in the entry: I<When I run a perl script from the
command line, it works, but, when I run it under the httpd, it fails!
Why?> in C<perldoc DBI::FAQ>.

In the future it will also try to create a table, put data into the
table, get information from the table, remove information from the
table and remove it as well. (But this is also a relatively low
priority, so if you are feeling industrious then have a go at it!)

=head2 APPLY PACKAGE

Command: apply_package

Required options:

 --website_dir=/path/to/my_website
 --package=x (at least one)

Applies a package from the installed base of packages in OpenInteract
to your website. (The package must have first been installed with
'install_package', although future versions of OpenInteract may allow
you to install a package to a website only.) This includes
localizing all the files (changing the namespace from 'OpenInteract::'
to 'MyApp::') and copying all the templates, SQL structures and
default data/security to your website directory.

APPLYING THE PACKAGE DOES **NOT** INSTALL THE SQL FOR YOU. SEE
L<INSTALL SQL>.

This option will by default apply the latest version of the available
package for you. Applying earlier versions is not yet supported.

=head2 REMOVE PACKAGE

Command: remove_package

Required options:

 --website_dir=/path/to/my_website
 --package=x (at least one)

Removes the package from the website. Note that we do B<not>
currently support removing the files associated with the package, or
the templates in the database that belong to the package. If you want
to truly eradicate your package, you should do the following:

 >> oi_manage --pacakage=mypackage \
              --website_dir=/path/to/my_website \
              remove_template
 >> oi_manage --pacakage=mypackage \ 
              --website_dir=/path/to/my_website \
              remove_package
 >> cd /path/to/my_website/pkg
 >> rm -rf mypackage-x.xx 

=head2 UPGRADE PACKAGE

Command: upgrade_package

Required options:

 --website_dir=/path/to/my_website
 --package=x (at least one)

Upgrades a package from the base installation to a website. For
instance, if your website has the package 'classified-1.04' and
the base installation has the package 'classified-1.11', then you can
simply do:

 >> oi_manage --website_dir=/path/to/my_website --package=classified \
              upgrade_package 

Note that the files from the older version of your package are kept
intact, but they are no longer used. This program does not currently
support the more complicated task of trying to find the differences
between your files and the new files -- you are left to your own
devices.

=head1 SQL COMMANDS

One of the difficulties in any website framework is getting data
into the framework from somewhere else and getting data out of the
framework to somewhere else.

OpenInteract provides a flexible framework for the first and the
seedlings of something for the second. Exporting data into the format
used by OpenInteract is on the list of things to do and has a
relatively high priority, although your help could push it over the
top!

=head2 INSTALL SQL

Command: install_sql

Required options:

 --package=x (at least one)
 --website_dir=/path/to/my_website

This command goes through a list of packages and installs the initial
tables and data for each one. It is also empowered to run perl scripts
that set initial security or do other tasks.

Generally, this works by each package creating an I<Installer Class>
that is used by L<OpenInteract::SQLInstall>. The dispatching class
provides a number of tools for the implementing class so that each
package does not need to do terribly much. However, if the package
B<needs> to do quite a bit of customization, it can.

Please see the L<OpenInteract::SQLInstall> module for more information
on how this whole process works.

=head2 DUMP SQL

Command: dump_sql

Required options:

 --package=x (at least one)
 --website_dir=/path/to/my_website

Useful option:

 --dump_dir=/path/to/dump/sql

Dumps data from the tables and security used by the website into a
number of files. If 'dump_dir' is specified the files will go there;
if not, they will go into the 'data/dump/' directory under the
package specified in the 'website_dir'.

This is still under development. How this will work:

=over 4

=item *

You define parameters in the SQLInstall class for your package that
determines how you want to dump certain data.

=item *

After running the command in this script, you will have a set of files
in the C<dump/> subdirectory of one or both of the C<data/> and
C<struct/> package subdirectories. (Or in the 'dump_dir' that you
choose) These files can be distributed with the package and used to
install data on other OpenInteract installations.

=back

=head1 TEMPLATE COMMANDS

This script should help with the whole template editing
process. Editing templates via the browser interface can be
tedious. HTML interfaces are, shall we say, not very robust for
dealing with most data. (Now, if we can only get XEmacs embedded as a
Mozilla widget for the TEXTAREA item...)

Anyway, most people will naturally prefer editing templates in their
favorite editor -- say, XEmacs -- and we would like to encourage such
productive behavior. We support this in two ways:

B<Calling Templates>

This is covered elsewhere, but worth mentioning here. When you call a
template with a last parameter like the following:

 { db => 'my_template_name', package => 'my_pkg' }

The system first looks in the database for a template with the given
name. If it is not found, it then looks to the filesystem in the
specified package. If it is not found there, the system raises an
error which is displayed onscreen.

What this means is that you can start creating your templates using
files -- test them, go through umpteen iterations of input-view-debug
until everything is stable, then add the template to the database for
performance reasons. You can still update the template from there, but
the editing cycle once the template is in the database is stretched
out. (At least for now it is.)

There are two commands to let you transfer templates between the
filesystem and the database, and one to remove them from the database
altogether.

=head2 INSTALL TEMPLATE

Command: install_template

Required options:

 --package=x (at least one)
 --website_dir=/path/to/my_website

Each package has its own directory for templates, and you can choose
to transfer these files into the database at any time. You can even do
so once the templates are already in the database -- the system will
first check to see if a template exists and update its information
before creating an entirely new one.

Note that all templates require a 'tmpl' file and a 'meta' file. A
typical 'tmpl' file might be:

 Typical Template File
 ==============================
 [% IF user %]

 [%- label = 'User Info'  IF not label -%]

 <table border="0" cellpadding="1" bgcolor="[% th.box_border_color %]" 
        cellspacing="0" width="[% th.box_width %]">
 <tr><td><font size="+1" color="[% th.box_label_font_color %]">
     <b>[% label %]</b>
 </font></td></tr>
 <tr><td>
 
   <table border="0" width="100%" cellpadding="4" 
          bgcolor="[% th.box_bgcolor %]" cellspacing="0">
   <tr><td align="left">
     <font size="-1" color="[% tg.box_font_color %]">
      <b>[% user.first_name %] [% user.last_name %]</b> ([% user.login_name %])<br>
     </font>
   </td></tr>
   <tr><td align="right"><font size="-1">
     <a href="/User/show/?user_id=[% user.user_id %]">Edit my info</a> |  
     <a href="/NoTmpl/Logout/?return=[% return_url %]">Logout</a>
   </font></td></tr>
   </table>
 
 </td></tr>
 </table>
 
 [% END %]
 ==============================

And its accompanying 'meta' file might be:

 Typical Meta File
 ==============================
 name: user_info_box
 title: Box that shows login user information
 package: base_component
 Parameters for this component:
 --user: the user object (returns nothing if it does not exist)
 --return_url: URL of the current page, which we will come back to if
 the user logs out
 --label: What is the label for the box? (default: 'User Info')
 ==============================

We use the information in the 'meta' file to name or location template
object when we install the templates.

=head2 DUMP TEMPLATE

Command: dump_template

Required options:

 --package=x (at least one)
 --website_dir=/path/to/my_website

Useful option:

 --dump_dir=/path/to/dump/stuff

Dumps the templates for a package from the database to the
filesystem. If you do not specify a 'dump_dir', all dumped templates
are stored in the directory within a package 'template/dump/' rather
than just 'template/'. (What you do with the templates after that is
your business.)

=head2 REMOVE TEMPLATE

Command: remove_template

Required options:

 --package=x (at least one) 
 --website_dir=/path/to/my_website

Removes all templates in the specified package(s) from the database
used by 'website_dir'.

=head2 OTHER METHODS

If you are adding functionality to this script, these methods can be
useful.

B<initialize_db_actions( $website_dir, $action )>

This method initializes OpenInteract without mod_perl, reading in the
configuration, creating an L<OpenInteract::Request> object and
connecting the database specified in the configuration.

Note that this creates the B<whole> environment -- SPOPS classes are
created and everything.

Return value is an L<OpenInteract::Request> object ($R).

B<open_base_config_file>

Just a wrapper around the C<read_base_config> method in
L<OpenInteract::Package> -- we provide a common error message.

B<print_status_line>

Display information from a 'status' record. Each status record is a
hashref which can have the following keys:

 ok
   True if status is ok, undef/0 otherwise

 name
   Name of the package

 version
   Version of the package

 msg
   Message to accompany status (gets displayed whether status is 'ok'
   or not)

=head1 EXAMPLES

Some quick examples:

Installing OpenInteract for the first time, to the directory
'/opt/OpenInteract':

 >> tar -zxvf OpenInteract-1.01.tar.gz
 >> cd OpenInteract-1.01
 >> perl Makefile.PL
 >> make
 >> make test
 >> make install
 >> /usr/local/bin/oi_manage --base_dir=/opt/OpenInteract install

Create a new website:

 >> /usr/local/bin/oi_manage --website_dir=~/OIApp \
       --website_name=MyOIApp \
       --base_dir=/opt/OpenInteract create_website

Install a package to the OpenInteract installation directory with a
file you have downloaded:

 >> /usr/local/bin/oi_manage --base_dir=/opt_OpenInteract \
       --package_file=/tmp/downloadedpackage-1.41.tar.gz \
       install_package

Then apply the new package to your website:

 >> /usr/local/bin/oi_manage --website_dir=~/OIApp \
       --package=downloadedpackage apply_package

Create a new skeleton directory for a package you will develop:

 >> cd ~/OpenInteract
 >> /usr/local/bin/oi_manage --package=mydevelpackage \
       create_skeleton

After you have worked on your new package, you want to create a
distribution file. First, run a check on the package:

 >> /usr/local/bin/oi_manage \
       --package_dir=~/OpenInteract/mydevelpackage \
       check_package

If everything looks ok, then export it to a .tar.gz

 >> cd ~/OpenInteract/mydevelpackage
 >> /usr/local/bin/oi_manage export_package

Install it to the main OpenInteract installation:

 >> /usr/local/bin/oi_manage --base_dir=/opt_OpenInteract \
       --package_file=~/OpenInteract/mydevelpackage/mydevelpackage-0.01.tar.gz \
       install_package

And then apply it to your website:

 >> /usr/local/bin/oi_manage --website_dir=~/OIApp \
       --package=mydevelpackage apply_package

List the website packages to make sure it is there: 

 >> /usr/local/bin/oi_manage --website_dir=~/OIApp list_packages

You might want to bring its templates into the website database:

 >> /usr/local/bin/oi_manage --website_dir=~/OIApp \
       --package=mydevelpackage install_template

And then create the SQL structures:

 >> /usr/local/bin/oi_manage --website_dir=~/OIApp \
       --package=mydevelpackage install_sql

=head1 TO DO

B<Progress Indicator>

When you are doing actions on multiple packages -- installing
OpenInteract, creating a website, etc. -- you do not get feedback as
the action is happening but rather everything at the end.

B<'Upgrade' analog to 'Install'>

After you have installed OpenInteract using the 'install' command, you
do not want to install it again. You just want to do:

 perl Makefile.PL
 make
 make test
 make install
 oi_manage upgrade

And have all the packages in 'pkg/' be installed anew. Should not be
too hard.

B<Friendly Dependency Finder>

At install_package time, inspect the modules used by the package --
list_module.dat, ISA, etc. If we find one that is not installed, ask
the user if he/she would like to install it and use CPAN to do so.

B<File Verification>

Integrate MD5 checksum verification into the system for each file as
well as for the package distribution on the whole.

B<SQL: Create an Equivalent Dumper>

It would be nice to have a database-independent data dumping program
that put the data into the format used by
C<OpenInteract::SQLInstall>. This actually should not be too hard,
since the format is pretty simple. We might need to add information to
the object (in C<spops.perl>) so that it can tell which fields are
'class' fields or other fields that need to be transformed.

=head1 BUGS

=head1 SEE ALSO

L<OpenInteract::Package>, L<OpenInteract::SQLInstall>, L<OpenInteract::Startup>

=head1 COPYRIGHT

Copyright (c) 2001 intes.net, inc.. All rights reserved.

This script is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.

=head1 AUTHORS

Chris Winters <chris@cwinters.com>

Christian Lemburg <lemburg@aixonix.de> suffered through early
versions of this installer and package management system and offered
insightful feedback.

Nate Haas <nateh@intes.net> and Marcus Baker <mbaker@intes.net> also
worked with early versions of this installer and provided many helpful
usability, documentation and functionality comments.

=head1 REVISION

Revision: $Revision: 1.61 $

=cut
