Commit 952c34b7 authored by Michael Vernier's avatar Michael Vernier

Initial commit.

parents
node_modules/
.settings/
# Conductor
Simple project template manager created in node. Conductor uses a simple directory structure for a project template and allows the usage of variables to customize project specific elements. Conductor fills the gap between basic tools like [bagpack](https://github.com/scriptnull/bagpack) which only copies a directory tree and [yeoman](https://github.com/yeoman/yeoman) which is very powerful but has a steep learning curve when creating simple templates.
## Installation
$ npm install -g conductor
## Usage
```shell
$ conductor --help
Usage: conductor [options] [command]
Commands:
clone [templatename] Clone one or more templates.
list [templatename] List available templates. If [templatename] given, template variables are displayed.
publish [remote] Publish the current directory as a template.
remote Handle remote template sources.
help [cmd] display help for [cmd]
Options:
-h, --help output usage information
-V, --version output the version number
```
## Detailed Usage
### conductor-remote
```shell
$ conductor remote
Usage: conductor-remote [options] [command]
Commands:
add [remotename] [path] Add path to template files
remove [remotename] Remove template source path
list List all remote project template sources
Options:
-h, --help output usage information
```
Remotes are directories which hold one or more templates that can be cloned using the `clone` command.
Add new groups of templates with the `add` command. **Note:** `[path]` should be an absolute path.
```shell
$ conductor remote add mytemplates c:/Users/swyphcosmo/conductor-templates
```
Remove remotes with the `remove` command.
```shell
$ conductor remote remove mytemplates
```
List all the remotes and their paths with the `list` command.
```shell
$ conductor remote list
mytemplates 'c:/Users/swyphcosmo/conductor-templates'
```
### conductor-clone
Templates can be cloned into any directory. It is expected that templates be managed with git so the `.git` directory is ignored during cloning.
**Warning:** `conductor` does not check before overwriting files.
```shell
$ mkdir mydocument
$ cd mydocument
$ conductor clone conductor-pandoc-docx
Installing conductor-pandoc-docx into 'mydocument'
? Please enter a value for 'Document Name': mydocument
Creating new file 'Makefile'
Creating new directory 'Images'
Creating new file 'reference.docx'
Creating new file 'mydocument.md'
```
### conductor-list
List all available templates:
```shell
$ conductor list
mytemplates (c:/Users/swyphcosmo/conductor-templates):
conductor-pandoc-docx
```
List all variables in a template:
```shell
$ conductor list conductor-pandoc-docx
Variables used in 'conductor-pandoc-docx':
{{conductor.DocumentName}}
```
## Template Format
Templates are directory based. A `remote` is a directory containing one or more templates (directories). Inside each template is one or more files and directories which will be used in a new project.
Variables can be used in the file contents and also in directory or file names. Variables are formated as follows:
{{conductor.<variableName>}}
Examples:
{{conductor.DocumentName}}
{{conductor.compiledLibraryName}}
During the cloning process, `conductor` will ask for a value for each unique variable. To make the queries more human readable, use camelCase for variable names. The variable name will be automatically broken into words before each capital letter, and the first letter will be capitalized.
Examples:
* `{{conductor.DocumentName}}` => `Document Name`
* `{{conductor.compiledLibraryName}}` => `Compiled Library Name`
## License (MIT)
Copyright (c) 2015 Michael Vernier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
\ No newline at end of file
#!/usr/bin/env node
var program = require( 'commander' );
var conductor = require( './lib/conductor-utility.js' );
var inquirer = require( 'inquirer' );
program
.parse( process.argv );
var templates = program.args;
if( !templates.length )
{
console.error( 'One or more templates are required' );
process.exit( 1 );
}
console.log();
templates.forEach( function( template )
{
console.log( 'Installing %s into %s', template, process.cwd() );
var templateobject = conductor.getTemplate( template );
if( templateobject )
{
conductor.getTemplateVariables( templateobject )
.then( function( variables )
{
// console.log( 'Variables used in \'%s\':', template.name );
var questions = [];
variables.forEach( function( variable )
{
// console.log( ' %s', variable );
var newvariablename = conductor.prettyPrintCamelCase( variable );
questions.push(
{
type: 'input',
name: variable,
message: 'Please enter a value for \'' + newvariablename + '\':'
}
);
});
inquirer.prompt( questions, function( answers )
{
// console.log( JSON.stringify( answers, null, ' ' ) );
conductor.clone( templateobject, process.cwd(), answers );
});
})
.catch( function( error )
{
console.error( error );
process.exit( 1 );
})
.done();
}
else
{
console.error( 'Template \'%s\' not found.', arg );
process.exit( 1 );
}
});
console.log();
#!/usr/bin/env node
var program = require( 'commander' );
var path = require( 'path' );
var fs = require( 'fs' );
var mkdirp = require( 'mkdirp' );
var conductor = require( './lib/conductor-utility.js' );
var config = conductor.getConfig();
program.parse( process.argv );
// list the variables in a specific template
if( program.args.length )
{
program.args.forEach( function( arg )
{
var template = conductor.getTemplate( arg );
if( template )
{
conductor.getTemplateVariables( template )
.then( function( variables )
{
console.log( 'Variables used in \'%s\':', template.name );
variables.forEach( function( variable )
{
console.log( ' %s', variable );
});
})
.catch( function( error )
{
console.error( error );
process.exit( 1 );
})
.done();
}
else
{
console.error( 'Template \'%s\' not found.', arg );
process.exit( 1 );
}
});
}
// list all available templates
else
{
var remotes = config.get( 'remotes' );
Object.keys( remotes ).forEach( function( remote )
{
console.log( '%s (%s):', remote, remotes[ remote ] );
conductor.getTemplates( remote ).forEach( function( template )
{
console.log( ' %s', template.name );
});
});
}
\ No newline at end of file
#! /usr/bin/env node
var program = require( 'commander' );
var nconf = require( 'nconf' );
var path = require( 'path' );
var fs = require( 'fs' );
var mkdirp = require( 'mkdirp' );
var appdata = process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + 'Library/Preference' : '/var/local');
var configpath = path.join( appdata, 'conductor' );
var configfile = path.join( configpath, 'config.json' );
mkdirp.sync( configpath );
nconf.file( { file: configfile } );
program
.command( 'add [remotename] [path]' )
.description( 'Add path to template files' )
.action( function( remotename, path )
{
// check for valid parameters
if( !remotename || !path )
{
program.help();
}
var remotes = nconf.get( 'remotes' );
if( !remotes )
{
remotes = {};
}
if( remotes[ remotename ] )
{
console.error( 'Remote \'%s\' already exists with value \'%s\'', remotename, remotes[ remotename ] );
process.exit( 1 );
}
remotes[ remotename ] = path;
nconf.set( 'remotes', remotes );
nconf.save();
});
program
.command( 'remove [remotename]' )
.description( 'Remove template source path' )
.action( function( remote )
{
if( !remote )
{
program.help();
}
var remotes = nconf.get( 'remotes' );
if( remotes )
{
if( remotes[ remote ] )
{
delete remotes[ remote ];
nconf.set( 'remotes', remotes );
nconf.save();
}
else
{
console.error( 'Remote \'%s\' not found', remote );
process.exit( 1 );
}
}
else
{
console.error( 'No remotes defined' );
process.exit( 1 );
}
});
program
.command( 'list' )
.description( 'List all remote project template sources' )
.action( function()
{
var remotes = nconf.get( 'remotes' );
if( remotes )
{
Object.keys( remotes ).forEach( function( remote )
{
console.log( '%s \'%s\'', remote, remotes[ remote ] );
});
}
});
program.parse( process.argv );
// Default: print help for remote commands
if( !program.args.length )
{
program.help();
}
#!/usr/bin/env node
var program = require( 'commander' );
program
.version( '0.0.1' )
.command( 'clone [templatename]', 'Clone one or more templates.' )
.command( 'list [templatename]', 'List available templates. If [templatename] given, template variables are displayed.' )
.command( 'remote', 'Handle remote template sources.' )
.parse( process.argv );
// Utility functions that are used in several conductor modules
var nconf = require( 'nconf' );
var path = require( 'path' );
var fs = require( 'fs' );
var mkdirp = require( 'mkdirp' );
var find = require( 'findit' );
var q = require( 'q' );
var istextorbinary = require( 'istextorbinary' );
var conductor = exports;
conductor._config = null;
conductor.getConfig = function()
{
if( !conductor._config )
{
var appdata = process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + 'Library/Preference' : '/var/local');
var configpath = path.join( appdata, 'conductor' );
var configfile = path.join( configpath, 'config.json' );
mkdirp.sync( configpath );
nconf.file( { file: configfile } );
conductor._config = nconf;
}
return conductor._config;
};
conductor.getRemoteNames = function()
{
return Object.keys( conductor.getConfig().get( 'remotes' ) );
};
conductor.getTemplates = function( remotename )
{
var remotes = conductor.getConfig().get( 'remotes' );
var files = fs.readdirSync( remotes[ remotename ] );
var templates = [];
files.forEach( function( file )
{
if( fs.lstatSync( path.join( remotes[ remotename ], file ) ).isDirectory() )
{
templates.push( {
remote: remotename,
path: path.join( remotes[ remotename ], file ),
name: file
});
}
});
return templates;
};
conductor.getAllTemplates = function()
{
var alltemplates = [];
conductor.getRemoteNames().forEach( function( remote )
{
alltemplates.push.apply( alltemplates, conductor.getTemplates( remote ) );
});
return alltemplates;
};
conductor.getTemplate = function( templatename )
{
var template = null;
conductor.getAllTemplates().forEach( function( t )
{
if( templatename == t.name )
{
template = t;
}
});
return template;
}
conductor.getTemplateVariables = function( template )
{
var promise = q.defer();
// Make sure we were given a valid template object
if( template && template.name && template.remote && template.path )
{
// console.log( template );
var variables = [];
var finder = new find( template.path );
finder.on( 'directory', function( directory, stat, stop )
{
var base = path.basename( directory );
// ignore the `.git` directory
if( base === '.git' )
{
stop();
}
else
{
// check to see if the base name matches a variable
var dirmatches = base.match( /\{\{conductor\.(.*)\}\}/g )
if( dirmatches )
{
dirmatches.forEach( function( dirmatch )
{
if( variables.indexOf( dirmatch ) == -1 )
{
// console.log( 'dirmatch: %s', dirmatch );
// console.log( variables );
// console.log();
variables.push( dirmatch );
}
});
}
}
});
finder.on( 'file', function( file, stat )
{
// check to see if the file name matches a variable
var filematches = file.match( /\{\{conductor\.(.*)\}\}/g )
if( filematches )
{
filematches.forEach( function( filematch )
{
if( variables.indexOf( filematch ) == -1 )
{
// console.log( 'filematch: %s', filematch );
// console.log( variables );
// console.log();
variables.push( filematch );
}
});
}
var filedata = fs.readFileSync( file, 'utf8' );
if( filedata )
{
var matches = filedata.match( /\{\{conductor\.(.*)\}\}/g );
if( matches )
{
matches.forEach( function( match )
{
if( variables.indexOf( match ) == -1 )
{
// console.log( 'text match: %s', match );
// console.log( variables );
// console.log();
variables.push( match );
}
});
}
}
});
finder.on( 'end', function()
{
promise.resolve( variables );
});
}
else
{
promise.reject( 'Valid template object required' );
}
return promise.promise;
};
conductor.clone = function( template, targetpath, variables )
{
var promise = q.defer();
if( template && template.name && template.path && template.remote )
{
if( targetpath && fs.lstatSync( targetpath ).isDirectory() )
{
var finder = new find( template.path );
finder.on( 'directory', function( directory, stat, stop )
{
var base = path.basename( directory );
// ignore the `.git` directory
if( base === '.git' )
{
stop();
}
else
{
var newdir = directory.substring( template.path.length );
if( newdir.length )
{
// console.log( newdir );
Object.keys( variables ).forEach( function( variable )
{
newdir = newdir.replace( variable, variables[ variable ] );
});
newdir = path.join( targetpath, newdir );
console.log( 'Creating new directory \'%s\'', newdir );
mkdirp.sync( newdir );
}
}
});
finder.on( 'file', function( file, stat )
{
var newfile = file.substring( template.path.length );
if( newfile.length )
{
var filebuffer = fs.readFileSync( file );
var istext = istextorbinary.isTextSync( file, filebuffer );
// console.log( '%s is text? ' + istext, file );
var filedata;
if( istext )
{
//filedata = fs.readFileSync( file, 'utf8' );
filedata = filebuffer.toString( 'utf8' );
}
else
{
//filedata = fs.readFileSync( file );
filedata = filebuffer;
}
Object.keys( variables ).forEach( function( variable )
{
var regex = new RegExp( variable, 'g' );
newfile = newfile.replace( regex, variables[ variable ] );
if( istext )
{
filedata = filedata.replace( regex, variables[ variable ] );
}
});
newfile = path.join( targetpath, newfile );
console.log( 'Creating new file \'%s\'', newfile );
fs.writeFileSync( newfile, filedata );
}
});
finder.on( 'end', function()
{
promise.resolve( true );
});
}
else
{
promise.reject( 'Target path \'%s\' must exist', targetpath );
}
}
else
{
promise.reject( 'Valid template object required' );
}
return promise.promise;
};
conductor.prettyPrintCamelCase = function( str )
{
return str
// remove leading {{conductor. and trailing }}
.replace( /^\{\{conductor\.(.*)\}\}/, '$1' )
// insert a space before all caps
.replace( /([A-Z])/g, ' $1' )
// uppercase the first character
.replace( /^./, function( str )
{
return str.toUpperCase();
})
// remove potential leading space
.replace( /^ /, '' );
};
\ No newline at end of file
{
"name": "conductor",
"version": "0.0.1",
"description": "Simple project template orchestrator.",
"main": "conductor.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"bin": {
"conductor": "conductor.js"
},
"repository": {
"type": "git",
"url": "http://www.swyphcosmo.com/vernierm/conductor.git"
},
"keywords": [
"project",
"template"
],
"author": "Michael Vernier",
"license": "MIT",
"dependencies": {
"commander": "^2.8.1",
"findit": "^2.0.0",
"inquirer": "^0.9.0",
"istextorbinary": "^1.0.2",
"mkdirp": "^0.5.1",
"nconf": "^0.7.1",
"ngrep": "^0.1.0",
"q": "^1.4.1"
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment