You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by ka...@apache.org on 2014/05/01 20:32:11 UTC

[26/53] [abbrv] Split out cordova-lib: move cordova-plugman files

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/0318d8cd/cordova-lib/spec-plugman/util/plugins.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-plugman/util/plugins.spec.js b/cordova-lib/spec-plugman/util/plugins.spec.js
new file mode 100644
index 0000000..63d0aac
--- /dev/null
+++ b/cordova-lib/spec-plugman/util/plugins.spec.js
@@ -0,0 +1,82 @@
+#!/usr/bin/env node
+/*
+ *
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+*/
+
+var http   = require('http'),
+    osenv  = require('osenv'),
+    path   = require('path'),
+    fs     = require('fs'),
+    temp   = path.join(osenv.tmpdir(), 'plugman'),
+    shell  = require('shelljs'),
+    child_process = require('child_process'),
+    plugins = require('../../src/util/plugins'),
+    xml_helpers = require('../../src/util/xml-helpers');
+
+describe('plugins utility module', function(){
+    describe('clonePluginGitRepo', function(){
+        var fake_id = 'VillageDrunkard';
+        var execSpy, cp_spy, xml_spy, done;
+        beforeEach(function() {
+            execSpy = spyOn(child_process, 'exec').andCallFake(function(cmd, opts, cb) {
+                if (!cb) cb = opts;
+                cb(null, 'git output');
+            });
+            spyOn(shell, 'which').andReturn(true);
+            cp_spy = spyOn(shell, 'cp');
+            xml_spy = spyOn(xml_helpers, 'parseElementtreeSync').andReturn({
+                getroot:function() {
+                    return {
+                        attrib:{id:fake_id}
+                    };
+                }
+            });
+            done = false;
+        });
+        it('should shell out to git clone with correct arguments', function(){
+            var plugin_git_url = 'https://github.com/imhotep/ChildBrowser';
+            var callback = jasmine.createSpy();
+
+            runs(function() {
+                plugins.clonePluginGitRepo(plugin_git_url, temp, '.', undefined)
+                .then(function(val) { done = val; }, function(err) { done = err; });
+            });
+            waitsFor(function() { return done; }, 'promise never resolved', 500);
+            runs(function() {
+                expect(execSpy).toHaveBeenCalled();
+                var git_clone_regex = new RegExp('^git clone "' + plugin_git_url + '" ".*"$', 'gi');
+                expect(execSpy.mostRecentCall.args[0]).toMatch(git_clone_regex);
+
+                expect(done).toMatch(new RegExp(path.sep + fake_id + '$'));
+            });
+        });
+        it('should take into account subdirectory argument when copying over final repository into plugins+plugin_id directory', function() {
+            var plugin_git_url = 'https://github.com/imhotep/ChildBrowser';
+            var fake_subdir = 'TheBrainRecoilsInHorror';
+            runs(function() {
+                plugins.clonePluginGitRepo(plugin_git_url, temp, fake_subdir)
+                .then(function(val) { done = val || true; }, function(err) { done = err; });
+            });
+            waitsFor(function() { return done; }, 'promise never resolved', 500);
+            runs(function() {
+                var expected_subdir_cp_path = new RegExp(fake_subdir + '[\\\\\\/]\\*$', 'gi');
+                expect(cp_spy.mostRecentCall.args[1]).toMatch(expected_subdir_cp_path);
+                expect(cp_spy.mostRecentCall.args[2]).toEqual(path.join(temp, fake_id));
+            });
+        });
+    });
+});

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/0318d8cd/cordova-lib/spec-plugman/util/xml-helpers.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-plugman/util/xml-helpers.spec.js b/cordova-lib/spec-plugman/util/xml-helpers.spec.js
new file mode 100644
index 0000000..edcdedb
--- /dev/null
+++ b/cordova-lib/spec-plugman/util/xml-helpers.spec.js
@@ -0,0 +1,170 @@
+/*
+ *
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+*/
+
+var path = require('path')
+  , xml_helpers = require('../../src/util/xml-helpers')
+  , et = require('elementtree')
+
+  , title = et.XML('<title>HELLO</title>')
+  , usesNetworkOne = et.XML('<uses-permission ' +
+			'android:name="PACKAGE_NAME.permission.C2D_MESSAGE"/>')
+  , usesNetworkTwo = et.XML("<uses-permission android:name=\
+            \"PACKAGE_NAME.permission.C2D_MESSAGE\" />")
+  , usesReceive = et.XML("<uses-permission android:name=\
+            \"com.google.android.c2dm.permission.RECEIVE\"/>")
+  , helloTagOne = et.XML("<h1>HELLO</h1>")
+  , goodbyeTag = et.XML("<h1>GOODBYE</h1>")
+  , helloTagTwo = et.XML("<h1>  HELLO  </h1>");
+
+
+describe('xml-helpers', function(){
+    describe('equalNodes', function() {
+        it('should return false for different tags', function(){
+            expect(xml_helpers.equalNodes(usesNetworkOne, title)).toBe(false);
+        });
+
+        it('should return true for identical tags', function(){
+            expect(xml_helpers.equalNodes(usesNetworkOne, usesNetworkTwo)).toBe(true);
+        });
+
+        it('should return false for different attributes', function(){
+            expect(xml_helpers.equalNodes(usesNetworkOne, usesReceive)).toBe(false);
+        });
+
+        it('should distinguish between text', function(){
+            expect(xml_helpers.equalNodes(helloTagOne, goodbyeTag)).toBe(false);
+        });
+
+        it('should ignore whitespace in text', function(){
+            expect(xml_helpers.equalNodes(helloTagOne, helloTagTwo)).toBe(true);
+        });
+
+        describe('should compare children', function(){
+            it('by child quantity', function(){
+                var one = et.XML('<i><b>o</b></i>'),
+                    two = et.XML('<i><b>o</b><u></u></i>');
+
+                expect(xml_helpers.equalNodes(one, two)).toBe(false);
+            });
+
+            it('by child equality', function(){
+                var one = et.XML('<i><b>o</b></i>'),
+                    two = et.XML('<i><u></u></i>'),
+                    uno = et.XML('<i>\n<b>o</b>\n</i>');
+
+                expect(xml_helpers.equalNodes(one, uno)).toBe(true);
+                expect(xml_helpers.equalNodes(one, two)).toBe(false);
+            });
+        });
+    });
+    describe('pruneXML', function() {
+        var config_xml;
+
+        beforeEach(function() {
+            config_xml = xml_helpers.parseElementtreeSync(path.join(__dirname, '..', 'projects', 'android_two', 'res', 'xml', 'config.xml'));
+        });
+
+        it('should remove any children that match the specified selector', function() {
+            var children = config_xml.findall('plugins/plugin');
+            xml_helpers.pruneXML(config_xml, children, 'plugins');
+            expect(config_xml.find('plugins').getchildren().length).toEqual(0);
+        });
+        it('should do nothing if the children cannot be found', function() {
+            var children = [title];
+            xml_helpers.pruneXML(config_xml, children, 'plugins');
+            expect(config_xml.find('plugins').getchildren().length).toEqual(17);
+        });
+        it('should be able to handle absolute selectors', function() {
+            var children = config_xml.findall('plugins/plugin');
+            xml_helpers.pruneXML(config_xml, children, '/cordova/plugins');
+            expect(config_xml.find('plugins').getchildren().length).toEqual(0);
+        });
+        it('should be able to handle absolute selectors with wildcards', function() {
+            var children = config_xml.findall('plugins/plugin');
+            xml_helpers.pruneXML(config_xml, children, '/*/plugins');
+            expect(config_xml.find('plugins').getchildren().length).toEqual(0);
+        });
+    });
+
+    describe('graftXML', function() {
+        var config_xml, plugin_xml;
+
+        beforeEach(function() {
+            config_xml = xml_helpers.parseElementtreeSync(path.join(__dirname, '..', 'projects', 'android_two', 'res', 'xml', 'config.xml'));
+            plugin_xml = xml_helpers.parseElementtreeSync(path.join(__dirname, '..', 'plugins', 'ChildBrowser', 'plugin.xml'));
+        });
+
+        it('should add children to the specified selector', function() {
+            var children = plugin_xml.find('config-file').getchildren();
+            xml_helpers.graftXML(config_xml, children, 'plugins');
+            expect(config_xml.find('plugins').getchildren().length).toEqual(19);
+        });
+        it('should be able to handle absolute selectors', function() {
+            var children = plugin_xml.find('config-file').getchildren();
+            xml_helpers.graftXML(config_xml, children, '/cordova');
+            expect(config_xml.findall('access').length).toEqual(3);
+        });
+        it('should be able to handle absolute selectors with wildcards', function() {
+            var children = plugin_xml.find('config-file').getchildren();
+            xml_helpers.graftXML(config_xml, children, '/*');
+            expect(config_xml.findall('access').length).toEqual(3);
+        });
+
+        it('for simple XPath paths, the parent should be created if not present', function () {
+            var doc = new et.ElementTree(et.XML('<widget>')),
+                children = [et.XML('<rim:permits> super_awesome_permission </rim:permits>')],
+                selector= "/widget/rim:permissions";
+            expect(xml_helpers.graftXML(doc, children, selector)).toBe(true);
+            expect(et.tostring(doc.getroot())).toContain("<rim:permissions><rim:permits> super_awesome_permission </rim:permits></rim:permissions>");
+        });
+
+        it('returns false for more complicated selectors', function () {
+            var doc = new et.ElementTree(et.XML('<widget>')),
+                children = [et.XML('<rim:permits> super_awesome_permission </rim:permits>')],
+                selector= "/bookstore/book[price>35]/title";
+            expect(xml_helpers.graftXML(doc, children, selector)).toBe(false);
+        });
+
+        it('appends children after the specified sibling', function () {
+            var doc = new et.ElementTree(et.XML('<widget><A/><B/><C/></widget>')),
+                children = [et.XML('<B id="new"/>'), et.XML('<B id="new2"/>')],
+                selector= "/widget",
+                after= "B;A";
+            expect(xml_helpers.graftXML(doc, children, selector, after)).toBe(true);
+            expect(et.tostring(doc.getroot())).toContain('<B /><B id="new" /><B id="new2" />');
+        });
+
+        it('appends children after the 2nd priority sibling if the 1st one is missing', function () {
+            var doc = new et.ElementTree(et.XML('<widget><A/><C/></widget>')),
+                children = [et.XML('<B id="new"/>'), et.XML('<B id="new2"/>')],
+                selector= "/widget",
+                after= "B;A";
+            expect(xml_helpers.graftXML(doc, children, selector, after)).toBe(true);
+            expect(et.tostring(doc.getroot())).toContain('<A /><B id="new" /><B id="new2" />');
+        });
+
+        it('inserts children at the beginning if specified sibling is missing', function () {
+            var doc = new et.ElementTree(et.XML('<widget><B/><C/></widget>')),
+                children = [et.XML('<A id="new"/>'), et.XML('<A id="new2"/>')],
+                selector= "/widget",
+                after= "A";
+            expect(xml_helpers.graftXML(doc, children, selector, after)).toBe(true);
+            expect(et.tostring(doc.getroot())).toContain('<widget><A id="new" /><A id="new2" />');
+        });
+    });
+});

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/0318d8cd/cordova-lib/spec-plugman/wrappers.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-plugman/wrappers.spec.js b/cordova-lib/spec-plugman/wrappers.spec.js
new file mode 100644
index 0000000..3e61eb7
--- /dev/null
+++ b/cordova-lib/spec-plugman/wrappers.spec.js
@@ -0,0 +1,40 @@
+var Q = require('q'),
+    plugman = require('../plugman');
+
+describe('callback wrapper', function() {
+    var calls = ['install', 'uninstall', 'fetch', 'config', 'owner', 'adduser', 'publish', 'unpublish', 'search', 'info', 'create', 'platform'];
+    for (var i = 0; i < calls.length; i++) {
+        var call = calls[i];
+
+        describe('`' + call + '`', function() {
+            var raw;
+            beforeEach(function() {
+                raw = spyOn(plugman.raw, call);
+            });
+
+            it('should work with no callback and success', function() {
+                raw.andReturn(Q());
+                plugman[call]();
+                expect(raw).toHaveBeenCalled();
+            });
+
+            it('should call the callback on success', function(done) {
+                raw.andReturn(Q(1));
+                plugman[call](function(err) {
+                    expect(err).toBeUndefined();
+                    done();
+                });
+            });
+
+            it('should call the callback with the error on failure', function(done) {
+                var err = new Error('junk');
+                raw.andCallFake(function() { return Q.reject(err)});
+                plugman[call](function(err) {
+                    expect(err).toEqual(err);
+                    done();
+                });
+            });
+        });
+    }
+});
+

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/0318d8cd/cordova-lib/src/plugman/adduser.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/plugman/adduser.js b/cordova-lib/src/plugman/adduser.js
new file mode 100644
index 0000000..b83a676
--- /dev/null
+++ b/cordova-lib/src/plugman/adduser.js
@@ -0,0 +1,5 @@
+var registry = require('./registry/registry')
+
+module.exports = function() {
+    return registry.adduser(null);
+}

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/0318d8cd/cordova-lib/src/plugman/config.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/plugman/config.js b/cordova-lib/src/plugman/config.js
new file mode 100644
index 0000000..e892425
--- /dev/null
+++ b/cordova-lib/src/plugman/config.js
@@ -0,0 +1,5 @@
+var registry = require('./registry/registry')
+
+module.exports = function(params) {
+    return registry.config(params)
+}

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/0318d8cd/cordova-lib/src/plugman/create.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/plugman/create.js b/cordova-lib/src/plugman/create.js
new file mode 100644
index 0000000..7db67b7
--- /dev/null
+++ b/cordova-lib/src/plugman/create.js
@@ -0,0 +1,81 @@
+/**
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+*/
+var Q = require('q'),
+    fs = require('fs'),
+    path = require('path'),
+    shell = require('shelljs'),
+    et = require('elementtree');
+
+module.exports = function create( name, id, version, pluginPath, options ) {
+    var cwd = pluginPath + "/" + name + "/",
+        docDir = path.join(__dirname, '..', 'doc/'),
+        baseJS,
+        root,
+        pluginName,
+        clobber,
+        jsMod,
+        pluginxml;
+
+    //check we are not already in a plugin
+    if( fs.existsSync( cwd + 'plugin.xml' ) ) {
+        return Q.reject( new Error( 'plugin.xml already exists. Are you already in a plugin?' ) );
+    }
+
+    //Create a plugin.xml file
+    root = et.Element( 'plugin' );
+    root.set( 'xmlns', 'http://apache.org/cordova/ns/plugins/1.0' );
+    root.set( 'xmlns:android', 'http://schemas.android.com/apk/res/android' );
+    root.set( 'id', id );
+    root.set( 'version', version );
+
+    //Add the name tag
+    pluginName = et.XML( "<name>" );
+    pluginName.text = name;
+    root.append( pluginName );
+
+    //loop through the options( variables ) for other tags
+    for( var key in options ) {
+        var temp = et.XML( "<" + key + ">");
+        temp.text = options[ key ];
+        root.append( temp );
+    }
+
+    //setup the directory structure
+    shell.mkdir( '-p', cwd + "www" );
+    shell.mkdir( '-p', cwd + "src" );
+
+    //create a base plugin.js file
+    baseJS = fs.readFileSync( docDir + 'base.js', 'utf-8').replace( /%pluginName%/g, name );
+    fs.writeFileSync( cwd + 'www/' + name + '.js', baseJS, 'utf-8' );
+    //Add it to the xml as a js module
+    jsMod = et.Element( 'js-module' );
+    jsMod.set( 'src', 'www/' + name + '.js' );
+    jsMod.set( 'name', name );
+
+    clobber = et.Element( 'clobbers' );
+    clobber.set( 'target', 'cordova.plugins.' + name );
+    jsMod.append( clobber );
+
+    root.append( jsMod );
+
+    //Write out the plugin.xml file
+    fs.writeFileSync( cwd + "plugin.xml", new et.ElementTree( root ).write( {indent: 4} ), 'utf-8' );
+
+    return Q();
+};

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/0318d8cd/cordova-lib/src/plugman/events.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/plugman/events.js b/cordova-lib/src/plugman/events.js
new file mode 100644
index 0000000..ed78839
--- /dev/null
+++ b/cordova-lib/src/plugman/events.js
@@ -0,0 +1,2 @@
+
+module.exports = new (require('events').EventEmitter)();

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/0318d8cd/cordova-lib/src/plugman/fetch.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/plugman/fetch.js b/cordova-lib/src/plugman/fetch.js
new file mode 100644
index 0000000..9948b7f
--- /dev/null
+++ b/cordova-lib/src/plugman/fetch.js
@@ -0,0 +1,175 @@
+var shell   = require('shelljs'),
+    fs      = require('fs'),
+    url     = require('url'),
+    plugins = require('./util/plugins'),
+    xml_helpers = require('./util/xml-helpers'),
+    events = require('./events'),
+    metadata = require('./util/metadata'),
+    path    = require('path'),
+    Q       = require('q'),
+    registry = require('./registry/registry');
+// XXX: leave the require('../plugman') because jasmine shits itself if you declare it up top
+// possible options: link, subdir, git_ref, client, expected_id
+// Returns a promise.
+module.exports = function fetchPlugin(plugin_src, plugins_dir, options) {
+    // Ensure the containing directory exists.
+    shell.mkdir('-p', plugins_dir);
+
+    options = options || {};
+    options.subdir = options.subdir || '.';
+    options.searchpath = options.searchpath || [];
+    if ( typeof options.searchpath === 'string' ) {
+        options.searchpath = options.searchpath.split(path.delimiter);
+    }
+
+    // clone from git repository
+    var uri = url.parse(plugin_src);
+
+    // If the hash exists, it has the form from npm: http://foo.com/bar#git-ref[:subdir]
+    // NB: No leading or trailing slash on the subdir.
+    if (uri.hash) {
+        var result = uri.hash.match(/^#([^:]*)(?::\/?(.*?)\/?)?$/);
+        if (result) {
+            if (result[1])
+                options.git_ref = result[1];
+            if(result[2])
+                options.subdir = result[2];
+
+            // Recurse and exit with the new options and truncated URL.
+            var new_dir = plugin_src.substring(0, plugin_src.indexOf('#'));
+            return fetchPlugin(new_dir, plugins_dir, options);
+        }
+    }
+
+    // If it looks like a network URL, git clone it.
+    if ( uri.protocol && uri.protocol != 'file:' && uri.protocol != 'c:' && !plugin_src.match(/^\w+:\\/)) {
+        events.emit('log', 'Fetching plugin "' + plugin_src + '" via git clone');
+        if (options.link) {
+            return Q.reject(new Error('--link is not supported for git URLs'));
+        } else {
+            var data = {
+                source: {
+                    type: 'git',
+                    url:  plugin_src,
+                    subdir: options.subdir,
+                    ref: options.git_ref
+                }
+            };
+
+            return plugins.clonePluginGit(plugin_src, plugins_dir, options)
+            .then(function(dir) {
+                return checkID(options.expected_id, dir);
+            })
+            .then(function(dir) {
+                metadata.save_fetch_metadata(dir, data);
+                return dir;
+            });
+        }
+    } else {
+        // If it's not a network URL, it's either a local path or a plugin ID.
+
+        var p,  // The Q promise to be returned.
+            linkable = true,
+            plugin_dir = path.join(plugin_src, options.subdir);
+
+        if (fs.existsSync(plugin_dir)) {
+            p = Q(plugin_dir);
+        } else {
+            // If there is no such local path, it's a plugin id.
+            // First look for it in the local search path (if provided).
+            var local_dir = findLocalPlugin(plugin_src, options.searchpath);
+            if (local_dir) {
+                p = Q(local_dir);
+                events.emit('verbose', 'Found ' + plugin_src + ' at ' + local_dir);
+            } else {
+                // If not found in local search path, fetch from the registry.
+                linkable = false;
+                events.emit('log', 'Fetching plugin "' + plugin_src + '" via plugin registry');
+                p = registry.fetch([plugin_src], options.client);
+            }
+        }
+
+        return p
+        .then(function(dir) {
+                options.plugin_src_dir = dir;
+
+                return copyPlugin(dir, plugins_dir, options.link && linkable);
+            })
+        .then(function(dir) {
+                return checkID(options.expected_id, dir);
+            });
+    }
+};
+
+function readId(dir) {
+    var xml_path = path.join(dir, 'plugin.xml');
+    var et = xml_helpers.parseElementtreeSync(path.join(dir, 'plugin.xml'));
+    var plugin_id = et.getroot().attrib.id;
+    return plugin_id;
+}
+
+// Helper function for checking expected plugin IDs against reality.
+function checkID(expected_id, dir) {
+    if ( expected_id ) {
+        var id = readId(dir);
+        if (expected_id != id) {
+            throw new Error('Expected fetched plugin to have ID "' + expected_id + '" but got "' + id + '".');
+        }
+    }
+    return dir;
+}
+
+var idCache = Object.create(null);
+// Look for plugin in local search path.
+function findLocalPlugin(plugin_id, searchpath) {
+    function tryPath(p) {
+        if (!(p in idCache)) {
+            var id = null;
+            if (fs.existsSync(path.join(p, 'plugin.xml'))) {
+                id = readId(p);
+            }
+            idCache[p] = id;
+        }
+        return (plugin_id === idCache[p]);
+    }
+
+    for (var i = 0; i < searchpath.length; i++) {
+        // Allow search path to point right to a plugin.
+        if (tryPath(searchpath[i])) {
+            return searchpath[i];
+        }
+        var files = fs.readdirSync(searchpath[i]);
+        for (var j = 0; j < files.length; j++) {
+            var pluginPath = path.join(searchpath[i], files[j]);
+            if (tryPath(pluginPath)) {
+                return pluginPath;
+            }
+        }
+    }
+    return null;
+}
+
+
+// Copy or link a plugin from plugin_dir to plugins_dir/plugin_id.
+function copyPlugin(plugin_dir, plugins_dir, link) {
+    var plugin_id = readId(plugin_dir);
+    var dest = path.join(plugins_dir, plugin_id);
+    shell.rm('-rf', dest);
+    if (link) {
+        events.emit('verbose', 'Linking plugin "' + plugin_dir + '" => "' + dest + '"');
+        fs.symlinkSync(plugin_dir, dest, 'dir');
+    } else {
+        shell.mkdir('-p', dest);
+        events.emit('verbose', 'Copying plugin "' + plugin_dir + '" => "' + dest + '"');
+        shell.cp('-R', path.join(plugin_dir, '*') , dest);
+    }
+
+    var data = {
+        source: {
+        type: 'local',
+              path: plugin_dir
+        }
+    };
+    metadata.save_fetch_metadata(dest, data);
+    return dest;
+}

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/0318d8cd/cordova-lib/src/plugman/info.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/plugman/info.js b/cordova-lib/src/plugman/info.js
new file mode 100644
index 0000000..34c2af7
--- /dev/null
+++ b/cordova-lib/src/plugman/info.js
@@ -0,0 +1,6 @@
+var registry = require('./registry/registry')
+
+// Returns a promise.
+module.exports = function(plugin) {
+    return registry.info(plugin);
+}

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/0318d8cd/cordova-lib/src/plugman/install.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/plugman/install.js b/cordova-lib/src/plugman/install.js
new file mode 100644
index 0000000..cce145d
--- /dev/null
+++ b/cordova-lib/src/plugman/install.js
@@ -0,0 +1,629 @@
+var path = require('path'),
+    fs   = require('fs'),
+    action_stack = require('./util/action-stack'),
+    dep_graph = require('dep-graph'),
+    elementtree = require('elementtree'),
+    child_process = require('child_process'),
+    semver = require('semver'),
+    config_changes = require('./util/config-changes'),
+    xml_helpers = require('./util/xml-helpers'),
+    Q = require('q'),
+    platform_modules = require('./platforms'),
+    os = require('os'),
+    underscore = require('underscore'),
+    shell   = require('shelljs'),
+    events = require('./events'),
+    plugman = require('../plugman'),
+    isWindows = (os.platform().substr(0,3) === 'win');
+
+/* INSTALL FLOW
+   ------------
+   There are four functions install "flows" through. Here is an attempt at
+   providing a high-level logic flow overview.
+   1. module.exports (installPlugin)
+     a) checks that the platform is supported
+     b) invokes possiblyFetch
+   2. possiblyFetch
+     a) checks that the plugin is fetched. if so, calls runInstall
+     b) if not, invokes plugman.fetch, and when done, calls runInstall
+   3. runInstall
+     a) checks if the plugin is already installed. if so, calls back (done).
+     b) if possible, will check the version of the project and make sure it is compatible with the plugin (checks <engine> tags)
+     c) makes sure that any variables required by the plugin are specified. if they are not specified, plugman will throw or callback with an error.
+     d) if dependencies are listed in the plugin, it will recurse for each dependent plugin and call possiblyFetch (2) on each one. When each dependent plugin is successfully installed, it will then proceed to call handleInstall (4)
+   4. handleInstall
+     a) queues up actions into a queue (asset, source-file, headers, etc)
+     b) processes the queue
+     c) calls back (done)
+*/
+
+// possible options: subdir, cli_variables, www_dir
+// Returns a promise.
+module.exports = function installPlugin(platform, project_dir, id, plugins_dir, options) {
+    options = options || {};
+    options.is_top_level = true;
+    plugins_dir = plugins_dir || path.join(project_dir, 'cordova', 'plugins');
+
+    if (!platform_modules[platform]) {
+        return Q.reject(new Error(platform + " not supported."));
+    }
+
+    var current_stack = new action_stack();
+
+    return possiblyFetch(id, plugins_dir, options)
+    .then(function(plugin_dir) {
+        return runInstall(current_stack, platform, project_dir, plugin_dir, plugins_dir, options);
+    });
+};
+
+// possible options: subdir, cli_variables, www_dir, git_ref, is_top_level
+// Returns a promise.
+function possiblyFetch(id, plugins_dir, options) {
+
+    // if plugin is a relative path, check if it already exists
+    var plugin_src_dir = path.join(plugins_dir, id);
+    if( isAbsolutePath(id) )
+        plugin_src_dir = id;
+
+    // Check that the plugin has already been fetched.
+    if (fs.existsSync(plugin_src_dir)) {
+        return Q(plugin_src_dir);
+    }
+
+    var opts = underscore.extend({}, options, {
+        link: false,
+        client: 'plugman'
+    });
+
+    // if plugin doesnt exist, use fetch to get it.
+    return plugman.raw.fetch(id, plugins_dir, opts);
+}
+
+function checkEngines(engines) {
+
+    for(var i = 0; i < engines.length; i++) {
+        var engine = engines[i];
+
+        if(semver.satisfies(engine.currentVersion, engine.minVersion) || engine.currentVersion === null){
+            // engine ok!
+        }else{
+            return Q.reject(new Error('Plugin doesn\'t support this project\'s '+engine.name+' version. '+engine.name+': ' + engine.currentVersion + ', failed version requirement: ' + engine.minVersion));
+        }
+    }
+
+    return Q(true);
+}
+
+function cleanVersionOutput(version, name){
+    var out = version.trim();
+    var rc_index = out.indexOf('rc');
+    var dev_index = out.indexOf('dev');
+    if (rc_index > -1) {
+        out = out.substr(0, rc_index) + '-' + out.substr(rc_index);
+    }
+
+    // put a warning about using the dev branch
+    if (dev_index > -1) {
+        // some platform still lists dev branches as just dev, set to null and continue
+        if(out=="dev"){
+            out = null;
+        }
+        events.emit('verbose', name+' has been detected as using a development branch. Attemping to install anyways.');
+    }
+
+    // add extra period/digits to conform to semver - some version scripts will output
+    // just a major or major minor version number
+    var majorReg = /\d+/,
+        minorReg = /\d+\.\d+/,
+        patchReg = /\d+\.\d+\.\d+/;
+
+    if(patchReg.test(out)){
+
+    }else if(minorReg.test(out)){
+        out = out.match(minorReg)[0]+'.0';
+    }else if(majorReg.test(out)){
+        out = out.match(majorReg)[0]+'.0.0';
+    }
+
+    return out;
+}
+
+// exec engine scripts in order to get the current engine version
+// Returns a promise for the array of engines.
+function callEngineScripts(engines) {
+    var engineScriptVersion;
+
+    return Q.all(
+        engines.map(function(engine){
+            // CB-5192; on Windows scriptSrc doesn't have file extension so we shouldn't check whether the script exists
+
+            var scriptPath = engine.scriptSrc ? '"' + engine.scriptSrc + '"' : null;
+
+            if(scriptPath && (isWindows || fs.existsSync(engine.scriptSrc)) ) {
+
+                var d = Q.defer();
+                if(!isWindows) { // not required on Windows
+                    fs.chmodSync(engine.scriptSrc, '755');
+                }
+                child_process.exec(scriptPath, function(error, stdout, stderr) {
+                    if (error) {
+                        events.emit('warn', engine.name +' version check failed ('+ scriptPath +'), continuing anyways.');
+                        engine.currentVersion = null;
+                    } else {
+                        engine.currentVersion = cleanVersionOutput(stdout, engine.name);
+                    }
+
+                    d.resolve(engine);
+                });
+                return d.promise;
+
+            } else {
+
+                if(engine.currentVersion) {
+                    engine.currentVersion = cleanVersionOutput(engine.currentVersion, engine.name)
+                } else {
+                    events.emit('warn', engine.name +' version not detected (lacks script '+ scriptPath +' ), continuing.');
+                }
+
+                return Q(engine);
+            }
+        })
+    );
+}
+
+// return only the engines we care about/need
+function getEngines(pluginElement, platform, project_dir, plugin_dir){
+    var engines = pluginElement.findall('engines/engine');
+    var defaultEngines = require('./util/default-engines')(project_dir);
+    var uncheckedEngines = [];
+    var cordovaEngineIndex, cordovaPlatformEngineIndex, theName, platformIndex, defaultPlatformIndex;
+    // load in known defaults and update when necessary
+
+    engines.forEach(function(engine){
+        theName = engine.attrib["name"];
+
+        // check to see if the engine is listed as a default engine
+        if(defaultEngines[theName]){
+            // make sure engine is for platform we are installing on
+            defaultPlatformIndex = defaultEngines[theName].platform.indexOf(platform);
+            if(defaultPlatformIndex > -1 || defaultEngines[theName].platform === '*'){
+                defaultEngines[theName].minVersion = defaultEngines[theName].minVersion ? defaultEngines[theName].minVersion : engine.attrib["version"];
+                defaultEngines[theName].currentVersion = defaultEngines[theName].currentVersion ? defaultEngines[theName].currentVersion : null;
+                defaultEngines[theName].scriptSrc = defaultEngines[theName].scriptSrc ? defaultEngines[theName].scriptSrc : null;
+                defaultEngines[theName].name = theName;
+
+                // set the indices so we can pop the cordova engine when needed
+                if(theName==='cordova') cordovaEngineIndex = uncheckedEngines.length;
+                if(theName==='cordova-'+platform) cordovaPlatformEngineIndex = uncheckedEngines.length;
+
+                uncheckedEngines.push(defaultEngines[theName]);
+            }
+        // check for other engines
+        }else{
+            platformIndex = engine.attrib["platform"].indexOf(platform);
+            if(platformIndex > -1 || engine.attrib["platform"] === '*'){
+                uncheckedEngines.push({ 'name': theName, 'platform': engine.attrib["platform"], 'scriptSrc':path.resolve(plugin_dir, engine.attrib["scriptSrc"]), 'minVersion' :  engine.attrib["version"]});
+            }
+        }
+    });
+
+    // make sure we check for platform req's and not just cordova reqs
+    if(cordovaEngineIndex && cordovaPlatformEngineIndex) uncheckedEngines.pop(cordovaEngineIndex);
+    return uncheckedEngines;
+}
+
+
+function isPluginInstalled(plugins_dir, platform, plugin_id) {
+    var platform_config = config_changes.get_platform_json(plugins_dir, platform);
+    for (var installed_plugin_id in platform_config.installed_plugins) {
+        if (installed_plugin_id == plugin_id) {
+            return true;
+        }
+    }
+    for (var installed_plugin_id in platform_config.dependent_plugins) {
+        if (installed_plugin_id == plugin_id) {
+            return true;
+        }
+    }
+    return false;
+}
+
+// possible options: cli_variables, www_dir, is_top_level
+// Returns a promise.
+var runInstall = module.exports.runInstall = function runInstall(actions, platform, project_dir, plugin_dir, plugins_dir, options) {
+    var xml_path     = path.join(plugin_dir, 'plugin.xml')
+      , plugin_et    = xml_helpers.parseElementtreeSync(xml_path)
+      , filtered_variables = {};
+    var name         = plugin_et.findall('name').text;
+    var plugin_id    = plugin_et.getroot().attrib['id'];
+
+    options = options || {};
+    options.graph = options.graph || new dep_graph();
+
+    if (isPluginInstalled(plugins_dir, platform, plugin_id)) {
+        if (options.is_top_level) {
+            events.emit('results', 'Plugin "' + plugin_id + '" already installed on ' + platform + '.');
+        } else {
+            events.emit('verbose', 'Dependent plugin "' + plugin_id + '" already installed on ' + platform + '.');
+        }
+        return Q();
+    }
+    events.emit('log', 'Installing "' + plugin_id + '" for ' + platform);
+
+    var theEngines = getEngines(plugin_et, platform, project_dir, plugin_dir);
+
+    var install = {
+        actions: actions,
+        platform: platform,
+        project_dir: project_dir,
+        plugins_dir: plugins_dir,
+        top_plugin_id: plugin_id,
+        top_plugin_dir: plugin_dir
+    }
+
+    return callEngineScripts(theEngines)
+    .then(checkEngines)
+    .then(
+        function() {
+            // checking preferences, if certain variables are not provided, we should throw.
+            var prefs = plugin_et.findall('./preference') || [];
+            prefs = prefs.concat(plugin_et.findall('./platform[@name="'+platform+'"]/preference'));
+            var missing_vars = [];
+            prefs.forEach(function (pref) {
+                var key = pref.attrib["name"].toUpperCase();
+                options.cli_variables = options.cli_variables || {};
+                if (options.cli_variables[key] === undefined)
+                    missing_vars.push(key)
+                else
+                    filtered_variables[key] = options.cli_variables[key]
+            });
+            install.filtered_variables = filtered_variables;
+
+            if (missing_vars.length > 0) {
+                throw new Error('Variable(s) missing: ' + missing_vars.join(", "));
+            }
+
+            // Check for dependencies
+            var dependencies = plugin_et.findall('dependency') || [];
+            dependencies = dependencies.concat(plugin_et.findall('./platform[@name="'+platform+'"]/dependency'));
+            if(dependencies && dependencies.length) {
+                return installDependencies(install, dependencies, options);
+            }
+            return Q(true);
+        }
+    ).then(
+        function(){
+            var install_plugin_dir = path.join(plugins_dir, plugin_id);
+
+            // may need to copy to destination...
+            if ( !fs.existsSync(install_plugin_dir) ) {
+                copyPlugin(plugin_dir, plugins_dir, options.link);
+            }
+
+            return handleInstall(actions, plugin_id, plugin_et, platform, project_dir, plugins_dir, install_plugin_dir, filtered_variables, options.www_dir, options.is_top_level);
+        }
+    ).fail(
+        function (error) {
+            events.emit('warn', "Failed to install '"+plugin_id+"':"+ error.stack);
+            throw error;
+        }
+    );
+}
+
+function installDependencies(install, dependencies, options) {
+    events.emit('verbose', 'Dependencies detected, iterating through them...');
+
+    var top_plugins = path.join(options.plugin_src_dir || install.top_plugin_dir, '..')
+
+    // Add directory of top-level plugin to search path
+    options.searchpath = options.searchpath || [];
+    if( top_plugins != install.plugins_dir && options.searchpath.indexOf(top_plugins) == -1 )
+        options.searchpath.push(top_plugins);
+
+    // Search for dependency by Id is:
+    // a) Look for {$top_plugins}/{$depId} directory
+    // b) Scan the top level plugin directory {$top_plugins} for matching id (searchpath)
+    // c) Fetch from registry
+
+    return dependencies.reduce(function(soFar, depXml) {
+        return soFar.then(
+            function() {
+                var dep = {
+                    id: depXml.attrib.id,
+                    subdir: depXml.attrib.subdir || '',
+                    url: depXml.attrib.url || '',
+                    git_ref: depXml.attrib.commit
+                }
+
+                if (dep.subdir.length) {
+                    dep.subdir = path.normalize(dep.subdir);
+                }
+
+                if (!dep.id) {
+                    throw new Error('<dependency> tag is missing id attribute: ' + elementtree.tostring(depXml, {xml_declaration:false}));
+                }
+
+                // We build the dependency graph only to be able to detect cycles, getChain will throw an error if it detects one
+                options.graph.add(install.top_plugin_id, dep.id);
+                options.graph.getChain(install.top_plugin_id);
+
+                return tryFetchDependency(dep, install, options)
+                .then(
+                    function(url){
+                        dep.url = url;
+                        return installDependency(dep, install, options);
+                    }
+                );
+            }
+        );
+
+    }, Q(true));
+}
+
+function tryFetchDependency(dep, install, options) {
+
+    // Handle relative dependency paths by expanding and resolving them.
+    // The easy case of relative paths is to have a URL of '.' and a different subdir.
+    // TODO: Implement the hard case of different repo URLs, rather than the special case of
+    // same-repo-different-subdir.
+    if ( dep.url == '.' ) {
+
+        // Look up the parent plugin's fetch metadata and determine the correct URL.
+        var fetchdata = require('./util/metadata').get_fetch_metadata(install.top_plugin_dir);
+        if (!fetchdata || !(fetchdata.source && fetchdata.source.type)) {
+
+            var relativePath = dep.subdir || dep.id;
+
+            events.emit('warn', 'No fetch metadata found for plugin ' + install.top_plugin_id + '. checking for ' + relativePath + ' in '+ options.searchpath.join(','));
+
+            return Q(relativePath);
+        }
+
+        // Now there are two cases here: local directory, and git URL.
+        var d = Q.defer();
+
+        if (fetchdata.source.type === 'local') {
+
+            dep.url = fetchdata.source.path;
+
+            child_process.exec('git rev-parse --show-toplevel', { cwd:dep.url }, function(err, stdout, stderr) {
+                if (err) {
+                    if (err.code == 128) {
+                        return d.reject(new Error('Plugin ' + dep.id + ' is not in git repository. All plugins must be in a git repository.'));
+                    } else {
+                        return d.reject(new Error('Failed to locate git repository for ' + dep.id + ' plugin.'));
+                    }
+                }
+                return d.resolve(stdout.trim());
+            });
+
+            return d.promise.then(function(git_repo) {
+                //Clear out the subdir since the url now contains it
+                var url = path.join(git_repo, dep.subdir);
+                dep.subdir = "";
+                return Q(url);
+            }).fail(function(error){
+//console.log("Failed to resolve url='.': " + error);
+                return Q(dep.url);
+            });
+
+        } else if (fetchdata.source.type === 'git') {
+            return Q(fetchdata.source.url);
+        } else if (fetchdata.source.type === 'dir') {
+
+            // Note: With fetch() independant from install()
+            // $md5 = md5(uri)
+            // Need a Hash(uri) --> $tmpDir/cordova-fetch/git-hostname.com-$md5/
+            // plugin[id].install.source --> searchpath that matches fetch uri
+
+            // mapping to a directory of OS containing fetched plugins
+            var tmpDir = fetchdata.source.url;
+            tmpDir = tmpDir.replace('$tmpDir', os.tmpdir());
+
+            var pluginSrc = '';
+            if(dep.subdir.length) {
+                // Plugin is relative to directory
+                pluginSrc = path.join(tmpDir, dep.subdir);
+            }
+
+            // Try searchpath in dir, if that fails re-fetch
+            if( !pluginSrc.length || !fs.existsSync(pluginSrc) ) {
+                pluginSrc = dep.id;
+
+                // Add search path
+                if( options.searchpath.indexOf(tmpDir) == -1 )
+                    options.searchpath.unshift(tmpDir); // place at top of search
+            }
+
+            return Q( pluginSrc );
+        }
+    }
+
+    // Test relative to parent folder
+    if( dep.url && isRelativePath(dep.url) ) {
+        var relativePath = path.resolve(install.top_plugin_dir, '../' + dep.url);
+
+        if( fs.existsSync(relativePath) ) {
+           dep.url = relativePath;
+        }
+    }
+
+    // CB-4770: registry fetching
+    if(dep.url === undefined) {
+        dep.url = dep.id;
+    }
+
+    return Q(dep.url);
+}
+
+function installDependency(dep, install, options) {
+
+    dep.install_dir = path.join(install.plugins_dir, dep.id);
+
+    if ( fs.existsSync(dep.install_dir) ) {
+        events.emit('verbose', 'Dependent plugin "' + dep.id + '" already fetched, using that version.');
+        var opts = underscore.extend({}, options, {
+            cli_variables: install.filtered_variables,
+            is_top_level: false
+        });
+
+       return runInstall(install.actions, install.platform, install.project_dir, dep.install_dir, install.plugins_dir, opts);
+
+    } else {
+        events.emit('verbose', 'Dependent plugin "' + dep.id + '" not fetched, retrieving then installing.');
+
+        var opts = underscore.extend({}, options, {
+            cli_variables: install.filtered_variables,
+            is_top_level: false,
+            subdir: dep.subdir,
+            git_ref: dep.git_ref,
+            expected_id: dep.id
+        });
+
+        var dep_src = dep.url.length ? dep.url : dep.id;
+
+        return possiblyFetch(dep_src, install.plugins_dir, opts)
+        .then(
+            function(plugin_dir) {
+                return runInstall(install.actions, install.platform, install.project_dir, plugin_dir, install.plugins_dir, opts);
+            }
+        );
+    };
+}
+
+function handleInstall(actions, plugin_id, plugin_et, platform, project_dir, plugins_dir, plugin_dir, filtered_variables, www_dir, is_top_level) {
+
+    // @tests - important this event is checked spec/install.spec.js
+    events.emit('verbose', 'Install start for "' + plugin_id + '" on ' + platform + '.');
+
+    var handler = platform_modules[platform];
+    www_dir = www_dir || handler.www_dir(project_dir);
+
+    var platformTag = plugin_et.find('./platform[@name="'+platform+'"]');
+    var assets = plugin_et.findall('asset');
+    if (platformTag) {
+
+
+        var sourceFiles = platformTag.findall('./source-file'),
+            headerFiles = platformTag.findall('./header-file'),
+            resourceFiles = platformTag.findall('./resource-file'),
+            frameworkFiles = platformTag.findall('./framework[@custom="true"]'), // CB-5238 adding only custom frameworks
+            libFiles = platformTag.findall('./lib-file'),
+            assets = assets.concat(platformTag.findall('./asset'));
+
+        // queue up native stuff
+        sourceFiles && sourceFiles.forEach(function(item) {
+            actions.push(actions.createAction(handler["source-file"].install,
+                                              [item, plugin_dir, project_dir, plugin_id],
+                                              handler["source-file"].uninstall,
+                                              [item, project_dir, plugin_id]));
+        });
+
+        headerFiles && headerFiles.forEach(function(item) {
+            actions.push(actions.createAction(handler["header-file"].install,
+                                             [item, plugin_dir, project_dir, plugin_id],
+                                             handler["header-file"].uninstall,
+                                             [item, project_dir, plugin_id]));
+        });
+
+        resourceFiles && resourceFiles.forEach(function(item) {
+            actions.push(actions.createAction(handler["resource-file"].install,
+                                              [item, plugin_dir, project_dir, plugin_id],
+                                              handler["resource-file"].uninstall,
+                                              [item, project_dir, plugin_id]));
+        });
+        // CB-5238 custom frameworks only
+        frameworkFiles && frameworkFiles.forEach(function(item) {
+            actions.push(actions.createAction(handler["framework"].install,
+                                             [item, plugin_dir, project_dir, plugin_id],
+                                             handler["framework"].uninstall,
+                                             [item, project_dir, plugin_id]));
+        });
+
+        libFiles && libFiles.forEach(function(item) {
+            actions.push(actions.createAction(handler["lib-file"].install,
+                                                [item, plugin_dir, project_dir, plugin_id],
+                                                handler["lib-file"].uninstall,
+                                                [item, project_dir, plugin_id]));
+
+        });
+    }
+
+    // run through the action stack
+    return actions.process(platform, project_dir)
+    .then(function(err) {
+        // queue up the plugin so prepare knows what to do.
+        config_changes.add_installed_plugin_to_prepare_queue(plugins_dir, plugin_id, platform, filtered_variables, is_top_level);
+        // call prepare after a successful install
+        plugman.prepare(project_dir, platform, plugins_dir, www_dir);
+
+        events.emit('verbose', 'Install complete for ' + plugin_id + ' on ' + platform + '.');
+        // WIN!
+        // Log out plugin INFO element contents in case additional install steps are necessary
+        var info = plugin_et.findall('./info');
+        if(info.length) {
+            events.emit('results', interp_vars(filtered_variables, info[0].text));
+        }
+        info = (platformTag ? platformTag.findall('./info') : []);
+        if(info.length) {
+            events.emit('results', interp_vars(filtered_variables, info[0].text));
+        }
+    });
+}
+
+function interp_vars(vars, text) {
+    vars && Object.keys(vars).forEach(function(key) {
+        var regExp = new RegExp("\\$" + key, "g");
+        text = text.replace(regExp, vars[key]);
+    });
+    return text;
+}
+
+function isAbsolutePath(path) {
+    return path && (path[0] === '/' || path[0] === '\\' || path.indexOf(':\\') > 0 );
+}
+
+function isRelativePath(path) {
+    return !isAbsolutePath();
+}
+
+function readId(plugin_dir) {
+    var xml_path = path.join(plugin_dir, 'plugin.xml');
+    events.emit('verbose', 'Fetch is reading plugin.xml from location "' + xml_path + '"...');
+    var et = xml_helpers.parseElementtreeSync(xml_path);
+
+    return et.getroot().attrib.id;
+}
+
+// Copy or link a plugin from plugin_dir to plugins_dir/plugin_id.
+function copyPlugin(plugin_src_dir, plugins_dir, link) {
+    var plugin_id = readId(plugin_src_dir);
+    var dest = path.join(plugins_dir, plugin_id);
+    shell.rm('-rf', dest);
+
+    if (link) {
+        events.emit('verbose', 'Symlinking from location "' + plugin_src_dir + '" to location "' + dest + '"');
+        fs.symlinkSync(plugin_src_dir, dest, 'dir');
+    } else {
+        shell.mkdir('-p', dest);
+        events.emit('verbose', 'Copying from location "' + plugin_src_dir + '" to location "' + dest + '"');
+        shell.cp('-R', path.join(plugin_src_dir, '*') , dest);
+    }
+
+    return dest;
+}
+
+function isPluginInstalled(plugins_dir, platform, plugin_id) {
+    var platform_config = config_changes.get_platform_json(plugins_dir, platform);
+    for (var installed_plugin_id in platform_config.installed_plugins) {
+        if (installed_plugin_id == plugin_id) {
+            return true;
+        }
+    }
+    for (var installed_plugin_id in platform_config.dependent_plugins) {
+        if (installed_plugin_id == plugin_id) {
+            return true;
+        }
+    }
+    return false;
+}

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/0318d8cd/cordova-lib/src/plugman/owner.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/plugman/owner.js b/cordova-lib/src/plugman/owner.js
new file mode 100644
index 0000000..9eaf152
--- /dev/null
+++ b/cordova-lib/src/plugman/owner.js
@@ -0,0 +1,6 @@
+var registry = require('./registry/registry');
+
+// Returns a promise.
+module.exports = function(args) {
+    return registry.owner(args);
+};

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/0318d8cd/cordova-lib/src/plugman/platform.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/plugman/platform.js b/cordova-lib/src/plugman/platform.js
new file mode 100644
index 0000000..c80ac75
--- /dev/null
+++ b/cordova-lib/src/plugman/platform.js
@@ -0,0 +1,119 @@
+var Q = require('q'),
+    et = require('elementtree'),
+    fs = require('fs'),
+    shell = require('shelljs'),
+    path = require('path');
+
+module.exports = {
+    add: function( platformName ) {
+        var pluginxml,
+            platform;
+
+        //check to make sure we are in the plugin first
+        if( !fs.existsSync( 'plugin.xml' ) ) {
+            return Q.reject( new Error( "can't find a plugin.xml.  Are you in the plugin?" ) );
+        }
+
+        //Get the current plugin.xml file
+        pluginxml = et.parse( fs.readFileSync('plugin.xml', 'utf-8') );
+
+        //Check if this platform exists
+        if( pluginxml.find("./platform/[@name='"+ platformName +"']") ) {
+            return Q.reject( new Error( "platform: " + platformName + " already added"  ) );
+        }
+
+        //Get the platform specific elements
+        platform = doPlatform( platformName, pluginxml.find("./name").text, pluginxml.getroot().get( "id" ) );
+
+        //Make sure we support it
+        if( !platform ) {
+            return Q.reject( new Error( "platform: " + platformName + " not yet supported"  ) );
+        }
+
+        pluginxml.getroot().append( platform.getroot() );
+
+        fs.writeFileSync( "plugin.xml", pluginxml.write( "plugin.xml", {indent: 4} ), 'utf-8' );
+        return Q();
+    },
+    remove: function( platformName ) {
+        //check to make sure we are in the plugin first
+        if( !fs.existsSync( 'plugin.xml' ) ) {
+            return Q.reject( new Error( "can't find a plugin.xml.  Are you in the plugin?" ) );
+        }
+
+        //Get the current plugin.xml file
+        pluginxml = et.parse( fs.readFileSync('plugin.xml', 'utf-8') );
+
+        //Check if this platform exists
+        if( !pluginxml.find("./platform/[@name='"+ platformName +"']") ) {
+            return Q.reject( new Error( "platform: " + platformName + " hasn't been added"  ) );
+        }
+
+        //Remove the Platform in question
+        pluginxml.getroot().remove( 0, pluginxml.find("./platform/[@name='"+ platformName +"']") );
+
+        //Rewrite the plugin.xml file back out
+        fs.writeFileSync( "plugin.xml", pluginxml.write( "plugin.xml", {indent: 4} ), 'utf-8' );
+
+        //Remove the src/"platform"
+        shell.rm( '-rf', 'src/' + platformName );
+
+        return Q();
+    }
+};
+
+function doPlatform( platformName, pluginName, pluginID, pluginVersion ) {
+    var docDir = path.join(__dirname, '..', 'doc/platforms/' + platformName + "/"),
+        platformFile = docDir + platformName + ".xml",
+        platform;
+
+    if( !fs.existsSync( platformFile ) ) {
+        return false;
+    }
+
+    platform = fs.readFileSync( platformFile, 'utf-8' )
+                .replace( /%pluginName%/g, pluginName )
+                .replace( /%pluginID%/g, pluginID )
+                .replace( /%packageName%/g, pluginID.replace( /[.]/g, '/' ) );
+    platform = new et.ElementTree( et.XML( platform ) );
+
+    doPlatformBase( docDir, platformName, pluginName, pluginID, pluginVersion );
+
+    return platform;
+}
+
+function doPlatformBase( docDir, platformName, pluginName, pluginID, pluginVersion ) {
+    //Create the default plugin file
+    var baseFiles = [],
+        i = 0;
+
+    switch( platformName ) {
+    case 'android':
+        baseFiles.push (
+            {
+                file: fs.readFileSync( docDir + "base.java", "utf-8" )
+                    .replace( /%pluginName%/g, pluginName )
+                    .replace( /%pluginID%/g, pluginID ),
+                extension: "java"
+            }
+        );
+
+        break;
+    case 'ios':
+        baseFiles.push(
+            {
+                file: fs.readFileSync( docDir + "base.m", "utf-8" )
+                    .replace( /%pluginName%/g, pluginName ),
+                extension: "m"
+            }
+        );
+        break;
+    }
+
+    shell.mkdir( '-p', 'src/' + platformName );
+
+    for( i; i < baseFiles.length; i++ ) {
+        fs.writeFileSync( 'src/' + platformName + '/' + pluginName + '.' + baseFiles[ i ].extension, baseFiles[ i ].file, 'utf-8' );
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/0318d8cd/cordova-lib/src/plugman/platform_operation.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/plugman/platform_operation.js b/cordova-lib/src/plugman/platform_operation.js
new file mode 100644
index 0000000..e17f5db
--- /dev/null
+++ b/cordova-lib/src/plugman/platform_operation.js
@@ -0,0 +1,5 @@
+var platform = require('./platform');
+
+module.exports = function( args ) {
+    return platform[ args.operation ]( args.platform_name );
+};

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/0318d8cd/cordova-lib/src/plugman/platforms.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/plugman/platforms.js b/cordova-lib/src/plugman/platforms.js
new file mode 100644
index 0000000..a2ad634
--- /dev/null
+++ b/cordova-lib/src/plugman/platforms.js
@@ -0,0 +1,12 @@
+module.exports = {
+    'android': require('./platforms/android'),
+    'amazon-fireos': require('./platforms/amazon-fireos'),
+    'ios': require('./platforms/ios'),
+    'blackberry10': require('./platforms/blackberry10'),
+    'wp7': require('./platforms/wp7'),
+    'wp8': require('./platforms/wp8'),
+    'windows8' : require('./platforms/windows8'),
+    'firefoxos': require('./platforms/firefoxos'),
+    'ubuntu': require('./platforms/ubuntu'),
+    'tizen': require('./platforms/tizen')
+};

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/0318d8cd/cordova-lib/src/plugman/platforms/amazon-fireos.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/plugman/platforms/amazon-fireos.js b/cordova-lib/src/plugman/platforms/amazon-fireos.js
new file mode 100644
index 0000000..adfc847
--- /dev/null
+++ b/cordova-lib/src/plugman/platforms/amazon-fireos.js
@@ -0,0 +1,88 @@
+/*
+ *
+ * Copyright 2013 Anis Kadri
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+*/
+
+var fs = require('fs')  // use existsSync in 0.6.x
+   , path = require('path')
+   , common = require('./common')
+   , events = require('../events')
+   , xml_helpers = require(path.join(__dirname, '..', 'util', 'xml-helpers'));
+
+module.exports = {
+    www_dir:function(project_dir) {
+        return path.join(project_dir, 'assets', 'www');
+    },
+    // reads the package name out of the Android Manifest file
+    // @param string project_dir the absolute path to the directory containing the project
+    // @return string the name of the package
+    package_name:function (project_dir) {
+        var mDoc = xml_helpers.parseElementtreeSync(path.join(project_dir, 'AndroidManifest.xml'));
+
+        return mDoc._root.attrib['package'];
+    },
+    "source-file":{
+        install:function(source_el, plugin_dir, project_dir, plugin_id) {
+            var dest = path.join(source_el.attrib['target-dir'], path.basename(source_el.attrib['src']));
+            common.copyFile(plugin_dir, source_el.attrib['src'], project_dir, dest);
+        },
+        uninstall:function(source_el, project_dir, plugin_id) {
+            var dest = path.join(source_el.attrib['target-dir'], path.basename(source_el.attrib['src']));
+            common.deleteJava(project_dir, dest);
+        }
+    },
+    "header-file": {
+        install:function(source_el, plugin_dir, project_dir, plugin_id) {
+            events.emit('verbose', 'header-fileinstall is not supported for amazon-fireos');
+        },
+        uninstall:function(source_el, project_dir, plugin_id) {
+            events.emit('verbose', 'header-file.uninstall is not supported for amazon-fireos');
+        }
+    },
+    "lib-file":{
+        install:function(lib_el, plugin_dir, project_dir, plugin_id) {
+            var src = lib_el.attrib.src;
+            var dest = path.join("libs", path.basename(src));
+            common.copyFile(plugin_dir, src, project_dir, dest);
+        },
+        uninstall:function(lib_el, project_dir, plugin_id) {
+            var src = lib_el.attrib.src;
+            var dest = path.join("libs", path.basename(src));
+            common.removeFile(project_dir, dest);
+        }
+    },
+    "resource-file":{
+        install:function(el, plugin_dir, project_dir, plugin_id) {
+            var src = el.attrib.src;
+            var target = el.attrib.target;
+            events.emit('verbose', 'Copying resource file ' + src + ' to ' + target);
+            common.copyFile(plugin_dir, src, project_dir, target);
+        },
+        uninstall:function(el, project_dir, plugin_id) {
+            var target = el.attrib.target;
+            common.removeFile(project_dir, target);
+        }
+    },
+    "framework": {
+        install:function(source_el, plugin_dir, project_dir, plugin_id) {
+            events.emit('verbose', 'framework.install is not supported for amazon-fireos');
+        },
+        uninstall:function(source_el, project_dir, plugin_id) {
+            events.emit('verbose', 'framework.uninstall is not supported for amazon-fireos');
+        }
+    }
+};

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/0318d8cd/cordova-lib/src/plugman/platforms/android.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/plugman/platforms/android.js b/cordova-lib/src/plugman/platforms/android.js
new file mode 100644
index 0000000..c323790
--- /dev/null
+++ b/cordova-lib/src/plugman/platforms/android.js
@@ -0,0 +1,89 @@
+/*
+ *
+ * Copyright 2013 Anis Kadri
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+*/
+
+var fs = require('fs')  // use existsSync in 0.6.x
+   , path = require('path')
+   , common = require('./common')
+   , events = require('../events')
+   , xml_helpers = require(path.join(__dirname, '..', 'util', 'xml-helpers'));
+
+module.exports = {
+    www_dir:function(project_dir) {
+        return path.join(project_dir, 'assets', 'www');
+    },
+    // reads the package name out of the Android Manifest file
+    // @param string project_dir the absolute path to the directory containing the project
+    // @return string the name of the package
+    package_name:function (project_dir) {
+        var mDoc = xml_helpers.parseElementtreeSync(path.join(project_dir, 'AndroidManifest.xml'));
+
+        return mDoc._root.attrib['package'];
+    },
+    "source-file":{
+        install:function(source_el, plugin_dir, project_dir, plugin_id) {
+            var dest = path.join(source_el.attrib['target-dir'], path.basename(source_el.attrib['src']));
+
+            common.copyNewFile(plugin_dir, source_el.attrib['src'], project_dir, dest);
+        },
+        uninstall:function(source_el, project_dir, plugin_id) {
+            var dest = path.join(source_el.attrib['target-dir'], path.basename(source_el.attrib['src']));
+            common.deleteJava(project_dir, dest);
+        }
+    },
+    "header-file": {
+        install:function(source_el, plugin_dir, project_dir, plugin_id) {
+            events.emit('verbose', 'header-file.install is not supported for android');
+        },
+        uninstall:function(source_el, project_dir, plugin_id) {
+            events.emit('verbose', 'header-file.uninstall is not supported for android');
+        }
+    },
+    "lib-file":{
+        install:function(lib_el, plugin_dir, project_dir, plugin_id) {
+            var src = lib_el.attrib.src;
+            var dest = path.join("libs", path.basename(src));
+            common.copyFile(plugin_dir, src, project_dir, dest);
+        },
+        uninstall:function(lib_el, project_dir, plugin_id) {
+            var src = lib_el.attrib.src;
+            var dest = path.join("libs", path.basename(src));
+            common.removeFile(project_dir, dest);
+        }
+    },
+    "resource-file":{
+        install:function(el, plugin_dir, project_dir, plugin_id) {
+            var src = el.attrib.src;
+            var target = el.attrib.target;
+            events.emit('verbose', 'Copying resource file ' + src + ' to ' + target);
+            common.copyFile(plugin_dir, src, project_dir, path.normalize(target));
+        },
+        uninstall:function(el, project_dir, plugin_id) {
+            var target = el.attrib.target;
+            common.removeFile(project_dir, path.normalize(target));
+        }
+    },
+    "framework": {
+        install:function(source_el, plugin_dir, project_dir, plugin_id) {
+            events.emit('verbose', 'framework.install is not supported for android');
+        },
+        uninstall:function(source_el, project_dir, plugin_id) {
+            events.emit('verbose', 'framework.uninstall is not supported for android');
+        }
+    }
+};

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/0318d8cd/cordova-lib/src/plugman/platforms/blackberry10.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/plugman/platforms/blackberry10.js b/cordova-lib/src/plugman/platforms/blackberry10.js
new file mode 100644
index 0000000..9e370b6
--- /dev/null
+++ b/cordova-lib/src/plugman/platforms/blackberry10.js
@@ -0,0 +1,94 @@
+/*
+ *
+ * Copyright 2013 Anis Kadri
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+*/
+
+var fs = require('fs')  // use existsSync in 0.6.x
+   , path = require('path')
+   , common = require('./common')
+   , events = require('../events')
+   , xml_helpers = require(path.join(__dirname, '..', 'util', 'xml-helpers'));
+
+var TARGETS = ["device", "simulator"];
+
+module.exports = {
+    www_dir:function(project_dir) {
+        return path.join(project_dir, 'www');
+    },
+    package_name:function(project_dir) {
+        var config_path = path.join(module.exports.www_dir(project_dir), 'config.xml');
+        var widget_doc = xml_helpers.parseElementtreeSync(config_path);
+        return widget_doc._root.attrib['id'];
+    },
+    "source-file":{
+        install:function(source_el, plugin_dir, project_dir, plugin_id) {
+            var src = source_el.attrib['src'];
+            var target = source_el.attrib['target-dir'] || plugin_id;
+            TARGETS.forEach(function(arch) {
+                var dest = path.join("native", arch, "chrome", "plugin", target, path.basename(src));
+
+                common.copyNewFile(plugin_dir, src, project_dir, dest);
+            });
+        },
+        uninstall:function(source_el, project_dir, plugin_id) {
+            var src = source_el.attrib['src'];
+            var target = source_el.attrib['target-dir'] || plugin_id;
+            TARGETS.forEach(function(arch) {
+                var dest = path.join("native", arch, "chrome", "plugin", target, path.basename(src));
+                common.removeFile(project_dir, dest);
+            });
+        }
+    },
+    "header-file": {
+        install:function(source_el, plugin_dir, project_dir, plugin_id) {
+            events.emit('verbose', 'header-file.install is not supported for blackberry');
+        },
+        uninstall:function(source_el, project_dir, plugin_id) {
+            events.emit('verbose', 'header-file.uninstall is not supported for blackberry');
+        }
+    },
+    "lib-file":{
+        install:function(lib_el, plugin_dir, project_dir, plugin_id) {
+            var src = lib_el.attrib.src;
+            var arch = lib_el.attrib.arch;
+            var dest = path.join("native", arch, "plugins", "jnext", path.basename(src));
+            common.copyFile(plugin_dir, src, project_dir, dest);
+        },
+        uninstall:function(lib_el, project_dir, plugin_id) {
+            var src = lib_el.attrib.src;
+            var arch = lib_el.attrib.arch;
+            var dest = path.join("native", arch, "plugins", "jnext", path.basename(src));
+            common.removeFile(project_dir, dest);
+        }
+    },
+    "resource-file":{
+        install:function(el, plugin_dir, project_dir, plugin_id) {
+            events.emit('verbose', 'resource-file.install is not supported for blackberry');
+        },
+        uninstall:function(el, project_dir, plugin_id) {
+            events.emit('verbose', 'resource-file.uninstall is not supported for blackberry');
+        }
+    },
+    "framework": {
+        install:function(source_el, plugin_dir, project_dir, plugin_id) {
+            events.emit('verbose', 'framework.install is not supported for blackberry');
+        },
+        uninstall:function(source_el, project_dir, plugin_id) {
+            events.emit('verbose', 'framework.uninstall is not supported for blackberry');
+        }
+    }
+};

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/0318d8cd/cordova-lib/src/plugman/platforms/common.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/plugman/platforms/common.js b/cordova-lib/src/plugman/platforms/common.js
new file mode 100644
index 0000000..399da00
--- /dev/null
+++ b/cordova-lib/src/plugman/platforms/common.js
@@ -0,0 +1,94 @@
+var shell = require('shelljs'),
+    path  = require('path'),
+    fs    = require('fs'),
+    common;
+
+module.exports = common = {
+    // helper for resolving source paths from plugin.xml
+    resolveSrcPath:function(plugin_dir, relative_path) {
+        var full_path = path.resolve(plugin_dir, relative_path);
+        return full_path;
+    },
+    // helper for resolving target paths from plugin.xml into a cordova project
+    resolveTargetPath:function(project_dir, relative_path) {
+        var full_path = path.resolve(project_dir, relative_path);
+        return full_path;
+    },
+    // Many times we simply need to copy shit over, knowing if a source path doesnt exist or if a target path already exists
+    copyFile:function(plugin_dir, src, project_dir, dest) {
+        src = module.exports.resolveSrcPath(plugin_dir, src);
+        if (!fs.existsSync(src)) throw new Error('"' + src + '" not found!');
+        dest = module.exports.resolveTargetPath(project_dir, dest);
+        shell.mkdir('-p', path.dirname(dest));
+
+        // XXX shelljs decides to create a directory when -R|-r is used which sucks. http://goo.gl/nbsjq
+        if(fs.statSync(src).isDirectory()) {
+            shell.cp('-Rf', src+'/*', dest);
+        } else {
+            shell.cp('-f', src, dest);
+        }
+    },
+    // Same as copy file but throws error if target exists
+    copyNewFile:function(plugin_dir, src, project_dir, dest) {
+        var target_path = common.resolveTargetPath(project_dir, dest);
+        if (fs.existsSync(target_path))
+            throw new Error('"' + target_path + '" already exists!');
+
+        common.copyFile(plugin_dir, src, project_dir, dest);
+    },
+    // checks if file exists and then deletes. Error if doesn't exist
+    removeFile:function(project_dir, src) {
+        var file = module.exports.resolveSrcPath(project_dir, src);
+        shell.rm('-Rf', file);
+    },
+    // deletes file/directory without checking
+    removeFileF:function(file) {
+        shell.rm('-Rf', file);
+    },
+    // Sometimes we want to remove some java, and prune any unnecessary empty directories
+    deleteJava:function(project_dir, destFile) {
+        var file = path.resolve(project_dir, destFile);
+        if (!fs.existsSync(file)) return;
+
+        common.removeFileF(file);
+
+        // check if directory is empty
+        var curDir = path.dirname(file);
+
+        while(curDir !== path.resolve(project_dir, 'src')) {
+            if(fs.existsSync(curDir) && fs.readdirSync(curDir) == 0) {
+                fs.rmdirSync(curDir);
+                curDir = path.resolve(curDir, '..');
+            } else {
+                // directory not empty...do nothing
+                break;
+            }
+        }
+    },
+    // handle <asset> elements
+    asset:{
+        install:function(asset_el, plugin_dir, www_dir) {
+            var src = asset_el.attrib.src;
+            var target = asset_el.attrib.target;
+
+            if (!src) {
+                throw new Error('<asset> tag without required "src" attribute');
+            }
+            if (!target) {
+                throw new Error('<asset> tag without required "target" attribute');
+            }
+
+            common.copyFile(plugin_dir, src, www_dir, target);
+        },
+        uninstall:function(asset_el, www_dir, plugin_id) {
+            var target = asset_el.attrib.target || asset_el.attrib.src;
+
+            if (!target) {
+                throw new Error('<asset> tag without required "target" attribute');
+            }
+
+            common.removeFile(www_dir, target);
+            common.removeFileF(path.resolve(www_dir, 'plugins', plugin_id));
+        }
+    }
+};

http://git-wip-us.apache.org/repos/asf/cordova-lib/blob/0318d8cd/cordova-lib/src/plugman/platforms/firefoxos.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/plugman/platforms/firefoxos.js b/cordova-lib/src/plugman/platforms/firefoxos.js
new file mode 100644
index 0000000..efbeba9
--- /dev/null
+++ b/cordova-lib/src/plugman/platforms/firefoxos.js
@@ -0,0 +1,72 @@
+var path = require('path')
+    , fs = require('fs')
+    , common = require('./common')
+    , events = require('../events')
+    , xml_helpers = require(path.join(__dirname, '..', 'util', 'xml-helpers'));
+
+module.exports = {
+    www_dir: function(project_dir) {
+        return path.join(project_dir, 'www');
+    },
+    package_name:function(project_dir) {
+        // preferred location if cordova >= 3.4
+        var preferred_path = path.join(project_dir, 'config.xml');
+        if (!fs.existsSync(preferred_path)) {
+            // older location
+            old_config_path = path.join(module.exports.www_dir(project_dir), 'config.xml');
+            if (!fs.existsSync(old_config_path)) {
+                // output newer location and fail reading
+                config_path = preferred_path;
+                events.emit('verbose', 'unable to find '+config_path);
+            } else {
+                config_path = old_config_path;
+            }
+        } else {
+            config_path = preferred_path;
+        }
+        var widget_doc = xml_helpers.parseElementtreeSync(config_path);
+        return widget_doc._root.attrib['id'];
+    },
+    "source-file":{
+        install:function(source_el, plugin_dir, project_dir, plugin_id) {
+            var dest = path.join(source_el.attrib['target-dir'], path.basename(source_el.attrib['src']));
+            common.copyFile(plugin_dir, source_el.attrib['src'], project_dir, dest);
+        },
+        uninstall:function(source_el, project_dir, plugin_id) {
+            var dest = path.join(source_el.attrib['target-dir'], path.basename(source_el.attrib['src']));
+            common.removeFile(project_dir, dest);
+        }
+    },
+    "header-file": {
+        install:function(source_el, plugin_dir, project_dir, plugin_id) {
+            events.emit('verbose', 'header-fileinstall is not supported for firefoxos');
+        },
+        uninstall:function(source_el, project_dir, plugin_id) {
+            events.emit('verbose', 'header-file.uninstall is not supported for firefoxos');
+        }
+    },
+    "resource-file":{
+        install:function(el, plugin_dir, project_dir, plugin_id) {
+            events.emit('verbose', 'resource-file.install is not supported for firefoxos');
+        },
+        uninstall:function(el, project_dir, plugin_id) {
+            events.emit('verbose', 'resource-file.uninstall is not supported for firefoxos');
+        }
+    },
+    "framework": {
+        install:function(source_el, plugin_dir, project_dir, plugin_id) {
+            events.emit('verbose', 'framework.install is not supported for firefoxos');
+        },
+        uninstall:function(source_el, project_dir, plugin_id) {
+            events.emit('verbose', 'framework.uninstall is not supported for firefoxos');
+        }
+    },
+    "lib-file": {
+        install:function(source_el, plugin_dir, project_dir, plugin_id) {
+            events.emit('verbose', 'lib-file.install is not supported for firefoxos');
+        },
+        uninstall:function(source_el, project_dir, plugin_id) {
+            events.emit('verbose', 'lib-file.uninstall is not supported for firefoxos');
+        }
+    }
+};