cordova-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From kam...@apache.org
Subject [10/24] Split out cordova-lib: move cordova-cli files
Date Fri, 02 May 2014 18:33:29 GMT
http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/b51e1c12/cordova-lib/spec-cordova/plugin.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/plugin.spec.js b/cordova-lib/spec-cordova/plugin.spec.js
new file mode 100644
index 0000000..2ad7b08
--- /dev/null
+++ b/cordova-lib/spec-cordova/plugin.spec.js
@@ -0,0 +1,68 @@
+
+var helpers = require('./helpers'),
+    path = require('path'),
+    fs = require('fs'),
+    shell = require('shelljs'),
+    Q = require('q'),
+    events = require('../src/events'),
+    cordova = require('../cordova');
+
+var tmpDir = helpers.tmpDir('plugin_test');
+var project = path.join(tmpDir, 'project');
+var pluginsDir = path.join(__dirname, 'fixtures', 'plugins');
+var pluginId = 'org.apache.cordova.fakeplugin1';
+
+describe('plugin end-to-end', function() {
+    var results;
+
+    beforeEach(function() {
+        shell.rm('-rf', project);
+    });
+    afterEach(function() {
+        process.chdir(path.join(__dirname, '..'));  // Needed to rm the dir on Windows.
+        shell.rm('-rf', tmpDir);
+    });
+
+    // The flow tested is: ls, add, ls, rm, ls.
+    // Plugin dependencies are not tested as that should be corvered in plugman tests.
+    // TODO (kamrik): Test the 'plugin search' command.
+    it('should successfully run', function(done) {
+        // cp then mv because we need to copy everything, but that means it'll copy the whole directory.
+        // Using /* doesn't work because of hidden files.
+        shell.cp('-R', path.join(__dirname, 'fixtures', 'base'), tmpDir);
+        shell.mv(path.join(tmpDir, 'base'), project);
+        // Copy some platform to avoid working on a project with no platforms.
+        shell.cp('-R', path.join(__dirname, 'fixtures', 'platforms', helpers.testPlatform), path.join(project, 'platforms'));
+        process.chdir(project);
+
+        events.on('results', function(res) { results = res; });
+
+        // Check there are no plugins yet.
+        cordova.raw.plugin('list').then(function() {
+            expect(results).toMatch(/No plugins added/gi);
+        }).then(function() {
+            // Add a fake plugin from fixtures.
+            return cordova.raw.plugin('add', path.join(pluginsDir, 'fake1'));
+        }).then(function() {
+           expect(path.join(project, 'plugins', pluginId, 'plugin.xml')).toExist();
+        }).then(function() {
+            return cordova.raw.plugin('ls');
+        }).then(function() {
+            expect(results).toContain(pluginId);
+            expect(results.length).toEqual(1);
+        }).then(function() {
+            // And now remove it.
+            return cordova.raw.plugin('rm', pluginId);
+        }).then(function() {
+            // The whole dir should be gone.
+            expect(path.join(project, 'plugins', pluginId)).not.toExist();
+        }).then(function() {
+            return cordova.raw.plugin('ls');
+        }).then(function() {
+            expect(results).toMatch(/No plugins added/gi);
+        }).fail(function(err) {
+            console.log(err);
+            expect(err).toBeUndefined();
+        }).fin(done);
+    });
+});

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/b51e1c12/cordova-lib/spec-cordova/plugin_parser.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/plugin_parser.spec.js b/cordova-lib/spec-cordova/plugin_parser.spec.js
new file mode 100644
index 0000000..9a63154
--- /dev/null
+++ b/cordova-lib/spec-cordova/plugin_parser.spec.js
@@ -0,0 +1,48 @@
+/**
+    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 cordova = require('../cordova'),
+    path = require('path'),
+    fs = require('fs'),
+    plugin_parser = require('../src/plugin_parser'),
+    et = require('elementtree'),
+    xml = path.join(__dirname, 'fixtures', 'plugins', 'test', 'plugin.xml');
+
+var xml_contents = fs.readFileSync(xml, 'utf-8');
+
+describe('plugin.xml parser', function () {
+    var readfile;
+    beforeEach(function() {
+        readfile = spyOn(fs, 'readFileSync').andReturn(xml_contents);
+    });
+
+    it('should read a proper plugin.xml file', function() {
+        var cfg;
+        expect(function () {
+            cfg = new plugin_parser(xml);
+        }).not.toThrow();
+        expect(cfg).toBeDefined();
+        expect(cfg.doc).toBeDefined();
+    });
+    it('should be able to figure out which platforms the plugin supports', function() {
+        var cfg = new plugin_parser(xml);
+        expect(cfg.platforms.length).toBe(1);
+        expect(cfg.platforms.indexOf('ios') > -1).toBe(true);
+    });
+});
+

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/b51e1c12/cordova-lib/spec-cordova/prepare.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/prepare.spec.js b/cordova-lib/spec-cordova/prepare.spec.js
new file mode 100644
index 0000000..eac225d
--- /dev/null
+++ b/cordova-lib/spec-cordova/prepare.spec.js
@@ -0,0 +1,290 @@
+/**
+    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 cordova = require('../cordova'),
+    shell = require('shelljs'),
+    plugman = require('plugman'),
+    path = require('path'),
+    fs = require('fs'),
+    util = require('../src/util'),
+    prepare = require('../src/prepare'),
+    lazy_load = require('../src/lazy_load'),
+    ConfigParser = require('../src/ConfigParser'),
+    platforms = require('../platforms'),
+    hooker = require('../src/hooker'),
+    xmlHelpers = require('../src/xml-helpers'),
+    fixtures = path.join(__dirname, 'fixtures'),
+    et = require('elementtree'),
+    Q = require('q'),
+    hooks = path.join(fixtures, 'hooks');
+
+var project_dir = '/some/path';
+var supported_platforms = Object.keys(platforms).filter(function(p) { return p != 'www'; });
+var supported_platforms_paths = supported_platforms.map(function(p) { return path.join(project_dir, 'platforms', p, 'www'); });
+
+var TEST_XML = '<?xml version="1.0" encoding="UTF-8"?>\n' +
+    '<widget xmlns     = "http://www.w3.org/ns/widgets"\n' +
+    '        xmlns:cdv = "http://cordova.apache.org/ns/1.0"\n' +
+    '        id        = "io.cordova.hellocordova"\n' +
+    '        version   = "0.0.1">\n' +
+    '    <name>Hello Cordova</name>\n' +
+    '    <description>\n' +
+    '        A sample Apache Cordova application that responds to the deviceready event.\n' +
+    '    </description>\n' +
+    '    <author href="http://cordova.io" email="dev@cordova.apache.org">\n' +
+    '        Apache Cordova Team\n' +
+    '    </author>\n' +
+    '    <content src="index.html" />\n' +
+    '    <access origin="*" />\n' +
+    '    <preference name="fullscreen" value="true" />\n' +
+    '    <preference name="webviewbounce" value="true" />\n' +
+    '</widget>\n';
+
+describe('prepare command', function() {
+    var is_cordova,
+        cd_project_root,
+        list_platforms,
+        fire,
+        parsers = {},
+        plugman_prepare,
+        find_plugins,
+        plugman_get_json,
+        cp,
+        mkdir,
+        load;
+    beforeEach(function() {
+        is_cordova = spyOn(util, 'isCordova').andReturn(project_dir);
+        cd_project_root = spyOn(util, 'cdProjectRoot').andReturn(project_dir);
+        list_platforms = spyOn(util, 'listPlatforms').andReturn(supported_platforms);
+        fire = spyOn(hooker.prototype, 'fire').andReturn(Q());
+        supported_platforms.forEach(function(p) {
+            parsers[p] = jasmine.createSpy(p + ' update_project').andReturn(Q());
+            spyOn(platforms[p], 'parser').andReturn({
+                update_project:parsers[p],
+                update_www: jasmine.createSpy(p + ' update_www'),
+                cordovajs_path: function(libDir) { return 'path/to/cordova.js/in/.cordova/lib';},
+                www_dir:function() { return path.join(project_dir, 'platforms', p, 'www'); },
+                config_xml: function () { return path.join(project_dir, "platforms", p, "www", "config.xml");}
+            });
+        });
+        plugman_prepare = spyOn(plugman, 'prepare').andReturn(Q());
+        find_plugins = spyOn(util, 'findPlugins').andReturn([]);
+        plugman_get_json = spyOn(plugman.config_changes, 'get_platform_json').andReturn({
+            prepare_queue:{installed:[], uninstalled:[]},
+            config_munge:{},
+            installed_plugins:{},
+            dependent_plugins:{}
+        });
+        load = spyOn(lazy_load, 'based_on_config').andReturn(Q());
+        cp = spyOn(shell, 'cp').andReturn(true);
+        mkdir = spyOn(shell, 'mkdir');
+        spyOn(prepare, '_mergeXml');
+        spyOn(ConfigParser.prototype, 'write');
+        spyOn(xmlHelpers, 'parseElementtreeSync').andCallFake(function() {
+            return new et.ElementTree(et.XML(TEST_XML));
+        });
+    });
+
+    describe('failure', function() {
+        it('should not run outside of a cordova-based project by calling util.isCordova', function(done) {
+            is_cordova.andReturn(false);
+            Q().then(prepare).then(function() {
+                expect('this call').toBe('fail');
+            }, function(err) {
+                expect('' + err).toContain('Current working directory is not a Cordova-based project.');
+            }).fin(done);
+        });
+        it('should not run inside a cordova-based project with no platforms', function(done) {
+            list_platforms.andReturn([]);
+            Q().then(prepare).then(function() {
+                expect('this call').toBe('fail');
+            }, function(err) {
+                expect('' + err).toContain('No platforms added to this project. Please use `cordova platform add <platform>`.');
+            }).fin(done);
+        });
+    });
+
+    describe('success', function() {
+        it('should run inside a Cordova-based project by calling util.isCordova', function(done) {
+            prepare().then(function() {
+                expect(is_cordova).toHaveBeenCalled();
+            }, function(err) {
+                expect(err).toBeUndefined();
+            }).fin(done);
+        });
+        it('should invoke each platform\'s parser\'s update_project method', function(done) {
+            prepare().then(function() {
+                supported_platforms.forEach(function(p) {
+                    expect(parsers[p]).toHaveBeenCalled();
+                });
+            }, function(err) {
+                expect(err).toBeUndefined();
+            }).fin(done);
+        });
+        it('should invoke lazy_load for each platform to make sure platform libraries are loaded', function(done) {
+            prepare().then(function() {
+                supported_platforms.forEach(function(p) {
+                    expect(load).toHaveBeenCalledWith(project_dir, p);
+                });
+            }, function(err) {
+                expect(err).toBeUndefined();
+            }).fin(done);
+        });
+        describe('plugman integration', function() {
+            it('should invoke plugman.prepare after update_project', function(done) {
+                prepare().then(function() {
+                    var plugins_dir = path.join(project_dir, 'plugins');
+                    supported_platforms.forEach(function(p) {
+                        var platform_path = path.join(project_dir, 'platforms', p);
+                        expect(plugman_prepare).toHaveBeenCalledWith(platform_path, (p=='blackberry'?'blackberry10':p), plugins_dir);
+                    });
+                }, function(err) {
+                    expect(err).toBeUndefined();
+                }).fin(done);
+            });
+        });
+    });
+
+    describe('hooks', function() {
+        describe('when platforms are added', function() {
+            it('should fire before hooks through the hooker module, and pass in platforms and paths as data object', function(done) {
+                prepare().then(function() {
+                    expect(fire).toHaveBeenCalledWith('before_prepare', {verbose: false, platforms:supported_platforms, options: [], paths:supported_platforms_paths});
+                }, function(err) {
+                    expect(err).toBeUndefined();
+                }).fin(done);
+            });
+            it('should fire after hooks through the hooker module, and pass in platforms and paths as data object', function(done) {
+                prepare('android').then(function() {
+                     expect(fire).toHaveBeenCalledWith('after_prepare', {verbose: false, platforms:['android'], options: [], paths:[path.join(project_dir, 'platforms', 'android', 'www')]});
+                }, function(err) {
+                    expect(err).toBeUndefined();
+                }).fin(done);
+            });
+        });
+
+        describe('with no platforms added', function() {
+            beforeEach(function() {
+                list_platforms.andReturn([]);
+            });
+            it('should not fire the hooker', function(done) {
+                Q().then(prepare).then(function() {
+                    expect('this call').toBe('fail');
+                }, function(err) {
+                    expect(err).toEqual(jasmine.any(Error));
+                    expect(fire).not.toHaveBeenCalledWith('before_prepare');
+                    expect(fire).not.toHaveBeenCalledWith('after_prepare');
+                }).fin(done);
+            });
+        });
+    });
+});
+
+describe('prepare._mergeXml', function () {
+    var dstXml;
+    beforeEach(function() {
+        dstXml = et.XML(TEST_XML);
+    });
+    it("should merge attributes and text of the root element without clobbering", function () {
+        var testXml = et.XML("<widget foo='bar' id='NOTANID'>TEXT</widget>");
+        prepare._mergeXml(testXml, dstXml);
+        expect(dstXml.attrib.foo).toEqual("bar");
+        expect(dstXml.attrib.id).not.toEqual("NOTANID");
+        expect(dstXml.text).not.toEqual("TEXT");
+    });
+
+    it("should merge attributes and text of the root element with clobbering", function () {
+        var testXml = et.XML("<widget foo='bar' id='NOTANID'>TEXT</widget>");
+        prepare._mergeXml(testXml, dstXml, "foo", true);
+        expect(dstXml.attrib.foo).toEqual("bar");
+        expect(dstXml.attrib.id).toEqual("NOTANID");
+        expect(dstXml.text).toEqual("TEXT");
+    });
+
+    it("should not merge platform tags with the wrong platform", function () {
+        var testXml = et.XML("<widget><platform name='bar'><testElement testAttrib='value'>testTEXT</testElement></platform></widget>"),
+            origCfg = et.tostring(dstXml);
+
+        prepare._mergeXml(testXml, dstXml, "foo", true);
+        expect(et.tostring(dstXml)).toEqual(origCfg);
+    });
+
+    it("should merge platform tags with the correct platform", function () {
+        var testXml = et.XML("<widget><platform name='bar'><testElement testAttrib='value'>testTEXT</testElement></platform></widget>"),
+            origCfg = et.tostring(dstXml);
+
+        prepare._mergeXml(testXml, dstXml, "bar", true);
+        expect(et.tostring(dstXml)).not.toEqual(origCfg);
+        var testElement = dstXml.find("testElement");
+        expect(testElement).toBeDefined();
+        expect(testElement.attrib.testAttrib).toEqual("value");
+        expect(testElement.text).toEqual("testTEXT");
+    });
+
+    it("should merge singelton children without clobber", function () {
+        var testXml = et.XML("<widget><author testAttrib='value' href='http://www.nowhere.com'>SUPER_AUTHOR</author></widget>");
+
+        prepare._mergeXml(testXml, dstXml);
+        var testElements = dstXml.findall("author");
+        expect(testElements).toBeDefined();
+        expect(testElements.length).toEqual(1);
+        expect(testElements[0].attrib.testAttrib).toEqual("value");
+        expect(testElements[0].attrib.href).toEqual("http://cordova.io");
+        expect(testElements[0].attrib.email).toEqual("dev@cordova.apache.org");
+        expect(testElements[0].text).toContain("Apache Cordova Team");
+    });
+
+    it("should clobber singelton children with clobber", function () {
+        var testXml = et.XML("<widget><author testAttrib='value' href='http://www.nowhere.com'>SUPER_AUTHOR</author></widget>");
+
+        prepare._mergeXml(testXml, dstXml, '', true);
+        var testElements = dstXml.findall("author");
+        expect(testElements).toBeDefined();
+        expect(testElements.length).toEqual(1);
+        expect(testElements[0].attrib.testAttrib).toEqual("value");
+        expect(testElements[0].attrib.href).toEqual("http://www.nowhere.com");
+        expect(testElements[0].attrib.email).toEqual("dev@cordova.apache.org");
+        expect(testElements[0].text).toEqual("SUPER_AUTHOR");
+    });
+
+    it("should append non singelton children", function () {
+        var testXml = et.XML("<widget><preference num='1'/> <preference num='2'/></widget>");
+
+        prepare._mergeXml(testXml, dstXml, '', true);
+        var testElements = dstXml.findall("preference");
+        expect(testElements.length).toEqual(4);
+    });
+
+    it("should handle namespaced elements", function () {
+        var testXml = et.XML("<widget><foo:bar testAttrib='value'>testText</foo:bar></widget>");
+
+        prepare._mergeXml(testXml, dstXml, 'foo', true);
+        var testElement = dstXml.find("foo:bar");
+        expect(testElement).toBeDefined();
+        expect(testElement.attrib.testAttrib).toEqual("value");
+        expect(testElement.text).toEqual("testText");
+    });
+
+    it("should not append duplicate non singelton children", function () {
+        var testXml = et.XML("<widget><preference name='fullscreen' value='true'/></widget>");
+
+        prepare._mergeXml(testXml, dstXml, '', true);
+        var testElements = dstXml.findall("preference");
+        expect(testElements.length).toEqual(2);
+    });
+});

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/b51e1c12/cordova-lib/spec-cordova/run.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/run.spec.js b/cordova-lib/spec-cordova/run.spec.js
new file mode 100644
index 0000000..bfaf3f8
--- /dev/null
+++ b/cordova-lib/spec-cordova/run.spec.js
@@ -0,0 +1,114 @@
+/**
+    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 cordova = require('../cordova'),
+    platforms = require('../platforms'),
+    superspawn = require('../src/superspawn'),
+    path = require('path'),
+    fs = require('fs'),
+    hooker = require('../src/hooker'),
+    Q = require('q'),
+    util = require('../src/util');
+
+var supported_platforms = Object.keys(platforms).filter(function(p) { return p != 'www'; });
+
+describe('run command', function() {
+    var is_cordova, cd_project_root, list_platforms, fire;
+    var project_dir = '/some/path';
+    var prepare_spy;
+
+    beforeEach(function() {
+        is_cordova = spyOn(util, 'isCordova').andReturn(project_dir);
+        cd_project_root = spyOn(util, 'cdProjectRoot').andReturn(project_dir);
+        list_platforms = spyOn(util, 'listPlatforms').andReturn(supported_platforms);
+        fire = spyOn(hooker.prototype, 'fire').andReturn(Q());
+        prepare_spy = spyOn(cordova.raw, 'prepare').andReturn(Q());
+        spyOn(superspawn, 'spawn').andReturn(Q);
+    });
+    describe('failure', function() {
+        it('should not run inside a Cordova-based project with no added platforms by calling util.listPlatforms', function(done) {
+            list_platforms.andReturn([]);
+            Q().then(cordova.raw.run).then(function() {
+                expect('this call').toBe('fail');
+            }, function(err) {
+                expect(err.message).toEqual('No platforms added to this project. Please use `cordova platform add <platform>`.');
+            }).fin(done);
+        });
+        it('should not run outside of a Cordova-based project', function(done) {
+            var msg = 'Dummy message about not being in a cordova dir.';
+            cd_project_root.andThrow(new Error(msg));
+            is_cordova.andReturn(false);
+            Q().then(cordova.raw.run).then(function() {
+                expect('this call').toBe('fail');
+            }, function(err) {
+                expect(err.message).toEqual(msg);
+            }).fin(done);
+        });
+    });
+
+    describe('success', function() {
+        it('should run inside a Cordova-based project with at least one added platform and call prepare and shell out to the run script', function(done) {
+            cordova.raw.run(['android','ios']).then(function() {
+                expect(prepare_spy).toHaveBeenCalledWith(['android', 'ios']);
+                expect(superspawn.spawn).toHaveBeenCalledWith(path.join(project_dir, 'platforms', 'android', 'cordova', 'run'), [], jasmine.any(Object));
+                expect(superspawn.spawn).toHaveBeenCalledWith(path.join(project_dir, 'platforms', 'ios', 'cordova', 'run'), [], jasmine.any(Object));
+            }, function(err) {
+                expect(err).toBeUndefined();
+            }).fin(done);
+        });
+        it('should pass down parameters', function(done) {
+            cordova.raw.run({platforms: ['blackberry10'], options:['--password', '1q1q']}).then(function() {
+                expect(prepare_spy).toHaveBeenCalledWith(['blackberry10']);
+                expect(superspawn.spawn).toHaveBeenCalledWith(path.join(project_dir, 'platforms', 'blackberry10', 'cordova', 'run'), ['--password', '1q1q'], jasmine.any(Object));
+            }, function(err) {
+                expect(err).toBeUndefined();
+            }).fin(done);
+        });
+    });
+
+    describe('hooks', function() {
+        describe('when platforms are added', function() {
+            it('should fire before hooks through the hooker module', function(done) {
+                cordova.raw.run(['android', 'ios']).then(function() {
+                    expect(fire).toHaveBeenCalledWith('before_run', {verbose: false, platforms:['android', 'ios'], options: []});
+                }, function(err) {
+                    expect(err).toBeUndefined();
+                }).fin(done);
+            });
+            it('should fire after hooks through the hooker module', function(done) {
+                cordova.raw.run('android').then(function() {
+                     expect(fire).toHaveBeenCalledWith('after_run', {verbose: false, platforms:['android'], options: []});
+                }, function(err) {
+                    expect(err).toBeUndefined();
+                }).fin(done);
+            });
+        });
+
+        describe('with no platforms added', function() {
+            it('should not fire the hooker', function(done) {
+                list_platforms.andReturn([]);
+                Q().then(cordova.raw.run).then(function() {
+                    expect('this call').toBe('fail');
+                }, function(err) {
+                    expect(fire).not.toHaveBeenCalled();
+                    expect(err.message).toEqual('No platforms added to this project. Please use `cordova platform add <platform>`.');
+                }).fin(done);
+            });
+        });
+    });
+});

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/b51e1c12/cordova-lib/spec-cordova/serve.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/serve.spec.js b/cordova-lib/spec-cordova/serve.spec.js
new file mode 100644
index 0000000..75eec4f
--- /dev/null
+++ b/cordova-lib/spec-cordova/serve.spec.js
@@ -0,0 +1,132 @@
+/**
+    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 cordova = require('../cordova'),
+    path = require('path'),
+    shell = require('shelljs'),
+    fs = require('fs'),
+    util = require('../src/util'),
+    hooker = require('../src/hooker'),
+    tempDir = path.join(__dirname, '..', 'temp'),
+    http = require('http'),
+    android_parser = require('../src/metadata/android_parser'),
+    ios_parser = require('../src/metadata/ios_parser'),
+    blackberry_parser = require('../src/metadata/blackberry10_parser'),
+    wp7_parser        = require('../src/metadata/wp7_parser'),
+    wp8_parser        = require('../src/metadata/wp8_parser');
+
+var cwd = process.cwd();
+
+xdescribe('serve command', function() {
+    beforeEach(function() {
+        // Make a temp directory
+        shell.rm('-rf', tempDir);
+        shell.mkdir('-p', tempDir);
+    });
+    it('should not run outside of a Cordova-based project', function() {
+        this.after(function() {
+            process.chdir(cwd);
+        });
+
+        process.chdir(tempDir);
+
+        expect(function() {
+            cordova.serve('android');
+        }).toThrow();
+    });
+
+
+    describe('`serve`', function() {
+        var payloads = {
+            android: 'This is the Android test file.',
+            ios: 'This is the iOS test file.'
+        };
+
+        beforeEach(function() {
+            cordova.raw.create(tempDir);
+            process.chdir(tempDir);
+            cordova.raw.platform('add', 'android');
+            cordova.raw.platform('add', 'ios');
+
+            // Write testing HTML files into the directory.
+            fs.writeFileSync(path.join(tempDir, 'platforms', 'android', 'assets', 'www', 'test.html'), payloads.android);
+            fs.writeFileSync(path.join(tempDir, 'platforms', 'ios', 'www', 'test.html'), payloads.ios);
+        });
+
+        afterEach(function() {
+            process.chdir(cwd);
+        });
+
+        function test_serve(platform, path, expectedContents, port) {
+            return function() {
+                var ret;
+                runs(function() {
+                    ret = port ? cordova.serve(platform, port) : cordova.serve(platform);
+                });
+
+                waitsFor(function() {
+                    return ret.server;
+                }, 'the server should start', 1000);
+
+                var done, errorCB;
+                runs(function() {
+                    expect(ret.server).toBeDefined();
+                    errorCB = jasmine.createSpy();
+                    http.get({
+                        host: 'localhost',
+                        port: port || 8000,
+                        path: path
+                    }).on('response', function(res) {
+                        var response = '';
+                        res.on('data', function(data) {
+                            response += data;
+                        });
+                        res.on('end', function() {
+                            expect(res.statusCode).toEqual(200);
+                            expect(response).toEqual(expectedContents);
+                            done = true;
+                        });
+                    }).on('error', errorCB);
+                });
+
+                waitsFor(function() {
+                    return done;
+                }, 'the HTTP request should complete', 1000);
+
+                runs(function() {
+                    expect(done).toBeTruthy();
+                    expect(errorCB).not.toHaveBeenCalled();
+
+                    ret.server.close();
+                });
+            };
+        };
+
+        it('should serve from top-level www if the file exists there', function() {
+            var payload = 'This is test file.';
+            fs.writeFileSync(path.join(util.projectWww(tempDir), 'test.html'), payload);
+            test_serve('android', '/test.html', payload)();
+        });
+
+        it('should fall back to assets/www on Android', test_serve('android', '/test.html', payloads.android));
+        it('should fall back to www on iOS', test_serve('ios', '/test.html', payloads.ios));
+
+        it('should honour a custom port setting', test_serve('android', '/test.html', payloads.android, 9001));
+    });
+});
+

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/b51e1c12/cordova-lib/spec-cordova/test-config.xml
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/test-config.xml b/cordova-lib/spec-cordova/test-config.xml
new file mode 100644
index 0000000..5e01e48
--- /dev/null
+++ b/cordova-lib/spec-cordova/test-config.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<widget xmlns     = "http://www.w3.org/ns/widgets"
+        xmlns:cdv = "http://cordova.apache.org/ns/1.0"
+        id        = "io.cordova.hellocordova"
+        version   = "0.0.1">
+    <name>Hello Cordova</name>
+
+    <description>
+        A sample Apache Cordova application that responds to the deviceready event.
+    </description>
+
+    <author href="http://cordova.io" email="dev@cordova.apache.org">
+        Apache Cordova Team
+    </author>
+
+    <content src="index.html" />
+
+    <access origin="*" />
+    <preference name="fullscreen" value="true" />
+    <preference name="webviewbounce" value="true" />
+    <icon id="icon" src="icon.png" />
+    <icon id="logo" src="logo.png" width="255" height="255" />
+    <platform name="android">
+        <icon src="logo-android.png" width="255" height="255" density="mdpi" />
+    </platform>    
+</widget>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/b51e1c12/cordova-lib/spec-cordova/util.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/util.spec.js b/cordova-lib/spec-cordova/util.spec.js
new file mode 100644
index 0000000..cb24f53
--- /dev/null
+++ b/cordova-lib/spec-cordova/util.spec.js
@@ -0,0 +1,186 @@
+/**
+    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 cordova = require('../cordova'),
+    shell = require('shelljs'),
+    path = require('path'),
+    fs = require('fs'),
+    util = require('../src/util'),
+    temp = path.join(__dirname, '..', 'temp'),
+    fixtures = path.join(__dirname, 'fixtures');
+
+var cwd = process.cwd();
+var home = process.env[(process.platform == 'win32') ? 'USERPROFILE' : 'HOME'];
+var origPWD = process.env['PWD'];
+
+describe('util module', function() {
+    describe('isCordova method', function() {
+        afterEach(function() {
+            process.env['PWD'] = origPWD;
+            process.chdir(cwd);
+        });
+        it('should return false if it hits the home directory', function() {
+            var somedir = path.join(home, 'somedir');
+            this.after(function() {
+                shell.rm('-rf', somedir);
+            });
+            shell.mkdir(somedir);
+            expect(util.isCordova(somedir)).toEqual(false);
+        });
+        it('should return false if it cannot find a .cordova directory up the directory tree', function() {
+            var somedir = path.join(home, '..');
+            expect(util.isCordova(somedir)).toEqual(false);
+        });
+        it('should return the first directory it finds with a .cordova folder in it', function() {
+            var somedir = path.join(home,'somedir');
+            var anotherdir = path.join(somedir, 'anotherdir');
+            this.after(function() {
+                shell.rm('-rf', somedir);
+            });
+            shell.mkdir('-p', anotherdir);
+            shell.mkdir('-p', path.join(somedir, 'www', 'config.xml'));
+            expect(util.isCordova(somedir)).toEqual(somedir);
+        });
+        it('should ignore PWD when its undefined', function() {
+            delete process.env['PWD'];
+            var somedir = path.join(home,'somedir');
+            var anotherdir = path.join(somedir, 'anotherdir');
+            this.after(function() {
+                shell.rm('-rf', somedir);
+            });
+            shell.mkdir('-p', anotherdir);
+            shell.mkdir('-p', path.join(somedir, 'www'));
+            shell.mkdir('-p', path.join(somedir, 'config.xml'));
+            process.chdir(anotherdir);
+            expect(util.isCordova()).toEqual(somedir);
+        });
+        it('should use PWD when available', function() {
+            var somedir = path.join(home,'somedir');
+            var anotherdir = path.join(somedir, 'anotherdir');
+            this.after(function() {
+                shell.rm('-rf', somedir);
+            });
+            shell.mkdir('-p', anotherdir);
+            shell.mkdir('-p', path.join(somedir, 'www', 'config.xml'));
+            process.env['PWD'] = anotherdir;
+            process.chdir(path.sep);
+            expect(util.isCordova()).toEqual(somedir);
+        });
+        it('should use cwd as a fallback when PWD is not a cordova dir', function() {
+            var somedir = path.join(home,'somedir');
+            var anotherdir = path.join(somedir, 'anotherdir');
+            this.after(function() {
+                shell.rm('-rf', somedir);
+            });
+            shell.mkdir('-p', anotherdir);
+            shell.mkdir('-p', path.join(somedir, 'www', 'config.xml'));
+            process.env['PWD'] = path.sep;
+            process.chdir(anotherdir);
+            expect(util.isCordova()).toEqual(somedir);
+        });
+        it('should ignore platform www/config.xml', function() {
+            var somedir = path.join(home,'somedir');
+            var anotherdir = path.join(somedir, 'anotherdir');
+            this.after(function() {
+                shell.rm('-rf', somedir);
+            });
+            shell.mkdir('-p', anotherdir);
+            shell.mkdir('-p', path.join(anotherdir, 'www', 'config.xml'));
+            shell.mkdir('-p', path.join(somedir, 'www'));
+            shell.mkdir('-p', path.join(somedir, 'config.xml'));
+            expect(util.isCordova(anotherdir)).toEqual(somedir);
+        });
+    });
+    describe('deleteSvnFolders method', function() {
+        afterEach(function() {
+            shell.rm('-rf', temp);
+        });
+        it('should delete .svn folders in any subdirectory of specified dir', function() {
+            var one = path.join(temp, 'one');
+            var two = path.join(temp, 'two');
+            var one_svn = path.join(one, '.svn');
+            var two_svn = path.join(two, '.svn');
+            shell.mkdir('-p', one_svn);
+            shell.mkdir('-p', two_svn);
+            util.deleteSvnFolders(temp);
+            expect(fs.existsSync(one_svn)).toEqual(false);
+            expect(fs.existsSync(two_svn)).toEqual(false);
+        });
+    });
+    describe('listPlatforms method', function() {
+        afterEach(function() {
+            shell.rm('-rf', temp);
+        });
+        it('should only return supported platform directories present in a cordova project dir', function() {
+            var platforms = path.join(temp, 'platforms');
+            var android = path.join(platforms, 'android');
+            var ios = path.join(platforms, 'ios');
+            var wp7 = path.join(platforms, 'wp7');
+            var atari = path.join(platforms, 'atari');
+            shell.mkdir('-p', android);
+            shell.mkdir('-p', ios);
+            shell.mkdir('-p', wp7);
+            shell.mkdir('-p', atari);
+            var res = util.listPlatforms(temp);
+            expect(res.length).toEqual(3);
+            expect(res.indexOf('atari')).toEqual(-1);
+        });
+    });
+    describe('findPlugins method', function() {
+        afterEach(function() {
+            shell.rm('-rf', temp);
+        });
+        it('should only return plugin directories present in a cordova project dir', function() {
+            var plugins = path.join(temp, 'plugins');
+            var android = path.join(plugins, 'android');
+            var ios = path.join(plugins, 'ios');
+            var wp7 = path.join(plugins, 'wp7');
+            var atari = path.join(plugins, 'atari');
+            shell.mkdir('-p', android);
+            shell.mkdir('-p', ios);
+            shell.mkdir('-p', wp7);
+            shell.mkdir('-p', atari);
+            var res = util.findPlugins(plugins);
+            expect(res.length).toEqual(4);
+        });
+        it('should not return ".svn" directories', function() {
+            var plugins = path.join(temp, 'plugins');
+            var android = path.join(plugins, 'android');
+            var ios = path.join(plugins, 'ios');
+            var svn = path.join(plugins, '.svn');
+            shell.mkdir('-p', android);
+            shell.mkdir('-p', ios);
+            shell.mkdir('-p', svn);
+            var res = util.findPlugins(plugins);
+            expect(res.length).toEqual(2);
+            expect(res.indexOf('.svn')).toEqual(-1);
+        });
+        it('should not return "CVS" directories', function() {
+            var plugins = path.join(temp, 'plugins');
+            var android = path.join(plugins, 'android');
+            var ios = path.join(plugins, 'ios');
+            var cvs = path.join(plugins, 'CVS');
+            shell.mkdir('-p', android);
+            shell.mkdir('-p', ios);
+            shell.mkdir('-p', cvs);
+            var res = util.findPlugins(plugins);
+            expect(res.length).toEqual(2);
+            expect(res.indexOf('CVS')).toEqual(-1);
+        });
+    });
+});

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/b51e1c12/cordova-lib/spec-cordova/wrappers.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/wrappers.spec.js b/cordova-lib/spec-cordova/wrappers.spec.js
new file mode 100644
index 0000000..2afbe9b
--- /dev/null
+++ b/cordova-lib/spec-cordova/wrappers.spec.js
@@ -0,0 +1,40 @@
+var Q = require('q'),
+    cordova = require('../cordova');
+
+describe('callback wrapper', function() {
+    var calls = ['prepare', 'build', 'create', 'emulate', 'plugin', 'platform', 'compile', 'run'];
+    for (var i = 0; i < calls.length; i++) {
+        var call = calls[i];
+
+        describe('`' + call + '`', function() {
+            var raw;
+            beforeEach(function() {
+                raw = spyOn(cordova.raw, call);
+            });
+
+            it('should work with no callback and success', function() {
+                raw.andReturn(Q());
+                cordova[call]();
+                expect(raw).toHaveBeenCalled();
+            });
+
+            it('should call the callback on success', function(done) {
+                raw.andReturn(Q());
+                cordova[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.andReturn(Q.reject(err));
+                cordova[call](function(e) {
+                    expect(e).toEqual(err);
+                    done();
+                });
+            });
+        });
+    }
+});
+

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/b51e1c12/cordova-lib/spec-cordova/xml-helpers.spec.js
----------------------------------------------------------------------
diff --git a/cordova-lib/spec-cordova/xml-helpers.spec.js b/cordova-lib/spec-cordova/xml-helpers.spec.js
new file mode 100644
index 0000000..6af8c4b
--- /dev/null
+++ b/cordova-lib/spec-cordova/xml-helpers.spec.js
@@ -0,0 +1,137 @@
+/**
+    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 path = require('path')
+  , xml_helpers = require('../src/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('parseElementtreeSync', function() {
+        it('should parse xml with a byte order mark', function() {
+            var xml_path = path.join(__dirname, 'fixtures', 'projects', 'windows', 'bom_test.xml');
+            expect(function() {
+                xml_helpers.parseElementtreeSync(xml_path);
+            }).not.toThrow();
+        })
+    });
+    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, 'fixtures', 'projects', 'android', '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, 'fixtures', 'projects', 'android', 'res', 'xml', 'config.xml'));
+            plugin_xml = xml_helpers.parseElementtreeSync(path.join(__dirname, 'fixtures', '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);
+        });
+    });
+});

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/b51e1c12/cordova-lib/src/cordova/ConfigParser.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/cordova/ConfigParser.js b/cordova-lib/src/cordova/ConfigParser.js
new file mode 100644
index 0000000..067c30f
--- /dev/null
+++ b/cordova-lib/src/cordova/ConfigParser.js
@@ -0,0 +1,163 @@
+/**
+    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 et = require('elementtree'),
+    xml= require('./xml-helpers'),
+    CordovaError = require('./CordovaError'),
+    fs = require('fs');
+
+/** Wraps a config.xml file */
+function ConfigParser(path) {
+    this.path = path;
+    try {
+        this.doc = xml.parseElementtreeSync(path);
+    } catch (e) {
+        console.error('Parsing '+path+' failed');
+        throw e;
+    }
+    var r = this.doc.getroot();
+    if (r.tag !== 'widget') {
+        throw new CordovaError(path + ' has incorrect root node name (expected "widget", was "' + r.tag + '")');
+    }
+}
+
+function getNodeTextSafe(el) {
+    return el && el.text && el.text.trim();
+}
+
+function findOrCreate(doc, name) {
+    var ret = doc.find(name);
+    if (!ret) {
+        ret = new et.Element(name);
+        doc.getroot().append(content);
+    }
+    return ret;
+}
+
+ConfigParser.prototype = {
+    packageName: function(id) {
+        return this.doc.getroot().attrib['id'];
+    },
+    setPackageName: function(id) {
+        this.doc.getroot().attrib['id'] = id;
+    },
+    name: function() {
+        return getNodeTextSafe(this.doc.find('name'));
+    },
+    setName: function(name) {
+        var el = findOrCreate(this.doc, 'name');
+        el.text = name;
+    },
+    description: function() {
+        return this.doc.find('description').text.trim();
+    },
+    setDescription: function() {
+        this.doc.find('description').text = name;
+        var el = findOrCreate(this.doc, 'description');
+    },
+    version: function() {
+        return this.doc.getroot().attrib['version'];
+    },
+    android_versionCode: function() {
+        return this.doc.getroot().attrib['android-versionCode'];
+    },
+    ios_CFBundleVersion: function() {
+        return this.doc.getroot().attrib['ios-CFBundleVersion'];
+    },
+    setVersion: function(value) {
+        this.doc.getroot().attrib['version'] = value;
+    },
+    author: function() {
+        return getNodeTextSafe(this.doc.find('author'));
+    },
+    getPreference: function(name) {
+        var preferences = this.doc.findall('preference');
+        var ret = null;
+        preferences.forEach(function (preference) {
+            // Take the last one that matches.
+            if (preference.attrib.name.toLowerCase() === name.toLowerCase()) {
+                ret = preference.attrib.value;
+            }
+        });
+        return ret;
+    },
+    /**
+     * Returns all icons for the platform specified.
+     * @param  {String} platform The platform.
+     * @return {Array} Icons for the platform specified.
+     */
+    getIcons: function(platform) {
+        var ret = [];
+            iconElements = [];
+
+        if (platform) { // platform specific icons
+            this.doc.findall('platform[@name=\'' + platform + '\']/icon').forEach(function(elt){
+                elt.platform = platform; // mark as platform specific icon
+                iconElements.push(elt)
+            });
+        }
+        // root level icons
+        iconElements = iconElements.concat(this.doc.findall('icon'));
+        // parse icon elements
+        iconElements.forEach(function (elt) {
+            var icon = {};
+            icon.src = elt.attrib.src;
+            icon.density = elt.attrib['density'] || elt.attrib['cdv:density'] || elt.attrib['gap:density'];
+            icon.platform = elt.platform || null; // null means icon represents default icon (shared between platforms)
+            icon.width = elt.attrib.width;
+            icon.height = elt.attrib.height;
+            // If one of width or Height is undefined, assume they are equal.
+            icon.width = icon.width || icon.height;
+            icon.height = icon.height || icon.width;
+
+            // default icon
+            if (!icon.width && !icon.height && !icon.density) {
+                ret.defaultIcon = icon;
+            }
+            ret.push(icon);
+        });
+
+        /**
+         * Returns icon with specified width and height
+         * @param  {number} w  Width of icon
+         * @param  {number} h  Height of icon
+         * @return {Icon}      Icon object or null if not found
+         */
+        ret.getIconBySize = function(w, h){
+            // If only one of width and height is given
+            // then we assume that they are equal.
+            var width = w || h, height = h || w;
+            for (var idx in this) {
+                var icon = this[idx];
+                if (width == icon.width && height == icon.width) return icon;
+            }
+            return null;
+        };
+        /** Returns default icons */
+        ret.getDefault = function() {
+            return ret.defaultIcon;
+        }
+
+        return ret;
+    },
+    write:function() {
+        fs.writeFileSync(this.path, this.doc.write({indent: 4}), 'utf-8');
+    }
+};
+
+module.exports = ConfigParser;

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/b51e1c12/cordova-lib/src/cordova/CordovaError.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/cordova/CordovaError.js b/cordova-lib/src/cordova/CordovaError.js
new file mode 100644
index 0000000..5576e06
--- /dev/null
+++ b/cordova-lib/src/cordova/CordovaError.js
@@ -0,0 +1,31 @@
+/**
+    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.
+*/
+
+
+// A derived exception class. See usage example in cli.js
+// Based on:
+// stackoverflow.com/questions/1382107/whats-a-good-way-to-extend-error-in-javascript/8460753#8460753
+function CordovaError(message) {
+    Error.captureStackTrace(this, this.constructor);
+    this.name = this.constructor.name;
+    this.message = message;
+}
+CordovaError.prototype.__proto__ = Error.prototype;
+
+module.exports = CordovaError;

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/b51e1c12/cordova-lib/src/cordova/build.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/cordova/build.js b/cordova-lib/src/cordova/build.js
new file mode 100644
index 0000000..5a0f0d3
--- /dev/null
+++ b/cordova-lib/src/cordova/build.js
@@ -0,0 +1,47 @@
+/**
+    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 cordova_util      = require('./util'),
+    Q                 = require('q'),
+    hooker            = require('./hooker');
+
+// Returns a promise.
+module.exports = function build(options) {
+    var projectRoot = cordova_util.cdProjectRoot();
+
+    if (!options) {
+        options = {
+            verbose: false,
+            platforms: [],
+            options: []
+        };
+    }
+
+    options = cordova_util.preProcessOptions(options);
+
+    // fire build hooks
+    var hooks = new hooker(projectRoot);
+    return hooks.fire('before_build', options)
+    .then(function() {
+        return require('../cordova').raw.prepare(options);
+    }).then(function() {
+        return require('../cordova').raw.compile(options);
+    }).then(function() {
+        return hooks.fire('after_build', options);
+    });
+};

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/b51e1c12/cordova-lib/src/cordova/compile.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/cordova/compile.js b/cordova-lib/src/cordova/compile.js
new file mode 100644
index 0000000..857c3a5
--- /dev/null
+++ b/cordova-lib/src/cordova/compile.js
@@ -0,0 +1,45 @@
+/**
+    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.
+*/
+
+/*global require: true, module: true, process: true*/
+/*jslint sloppy: true, white: true, newcap: true */
+
+var path              = require('path'),
+    cordova_util      = require('./util'),
+    hooker            = require('./hooker'),
+    superspawn        = require('./superspawn');
+
+// Returns a promise.
+module.exports = function compile(options) {
+    var projectRoot = cordova_util.cdProjectRoot();
+    options = cordova_util.preProcessOptions(options);
+
+    var hooks = new hooker(projectRoot);
+    var ret = hooks.fire('before_compile', options);
+    options.platforms.forEach(function(platform) {
+        ret = ret.then(function() {
+            var cmd = path.join(projectRoot, 'platforms', platform, 'cordova', 'build');
+            return superspawn.spawn(cmd, options.options, { stdio: 'inherit', printCommand: true });
+        });
+    });
+    ret = ret.then(function() {
+        return hooks.fire('after_compile', options);
+    });
+    return ret;
+};

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/b51e1c12/cordova-lib/src/cordova/config.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/cordova/config.js b/cordova-lib/src/cordova/config.js
new file mode 100644
index 0000000..d0c8d9a
--- /dev/null
+++ b/cordova-lib/src/cordova/config.js
@@ -0,0 +1,82 @@
+/**
+    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 path          = require('path'),
+    fs            = require('fs'),
+    url           = require('url'),
+    shell         = require('shelljs');
+
+// Map of project_root -> JSON
+var configCache = {};
+var autoPersist = true;
+
+function config(project_root, opts) {
+    var json = config.read(project_root);
+    for (var p in opts) {
+        json[p] = opts[p];
+    }
+    if (autoPersist) {
+        config.write(project_root, json);
+    } else {
+        configCache[project_root] = JSON.stringify(json);
+    }
+    return json;
+};
+
+config.setAutoPersist = function(value) {
+    autoPersist = value;
+};
+
+config.read = function get_config(project_root) {
+    var data = configCache[project_root];
+    if (data === undefined) {
+        var configPath = path.join(project_root, '.cordova', 'config.json');
+        if (!fs.existsSync(configPath)) {
+            data = '{}';
+        } else {
+            data = fs.readFileSync(configPath, 'utf-8');
+        }
+    }
+    configCache[project_root] = data;
+    return JSON.parse(data);
+};
+
+config.write = function set_config(project_root, json) {
+    var configPath = path.join(project_root, '.cordova', 'config.json');
+    var contents = JSON.stringify(json, null, 4);
+    configCache[project_root] = contents;
+    // Don't write the file for an empty config.
+    if (contents != '{}' || fs.existsSync(configPath)) {
+        shell.mkdir('-p', path.join(project_root, '.cordova'));
+        fs.writeFileSync(configPath, contents, 'utf-8');
+    }
+    return json;
+};
+
+config.has_custom_path = function(project_root, platform) {
+    var json = config.read(project_root);
+    if (json.lib && json.lib[platform]) {
+        var uri = url.parse(json.lib[platform].uri);
+        if (!(uri.protocol)) return uri.path;
+        else if (uri.protocol && uri.protocol[1] ==':') return uri.href;
+    }
+    return false;
+};
+
+module.exports = config;

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/b51e1c12/cordova-lib/src/cordova/cordova.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/cordova/cordova.js b/cordova-lib/src/cordova/cordova.js
new file mode 100644
index 0000000..687392b
--- /dev/null
+++ b/cordova-lib/src/cordova/cordova.js
@@ -0,0 +1,68 @@
+/**
+    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 cordova_events = require('./src/events');
+var cordova_util = require('./src/util');
+
+var off = function() {
+    cordova_events.removeListener.apply(cordova_events, arguments);
+};
+
+var emit = function() {
+    cordova_events.emit.apply(cordova_events, arguments);
+};
+
+exports = module.exports = {
+    on:        function() {
+        cordova_events.on.apply(cordova_events, arguments);
+    },
+    off:       off,
+    removeListener:off,
+    removeAllListeners:function() {
+        cordova_events.removeAllListeners.apply(cordova_events, arguments);
+    },
+    emit:      emit,
+    trigger:   emit,
+    raw: {}
+};
+
+exports.findProjectRoot = function(opt_startDir) {
+    return cordova_util.isCordova(opt_startDir);
+}
+
+// Each of these APIs takes a final parameter that is a callback function.
+// The callback is passed the error object upon failure, or undefined upon success.
+// To use a promise instead, call the APIs via cordova.raw.FOO(), which returns
+// a promise instead of using a final-parameter-callback.
+var addModuleProperty = cordova_util.addModuleProperty;
+addModuleProperty(module, 'prepare', './src/prepare', true);
+addModuleProperty(module, 'build', './src/build', true);
+addModuleProperty(module, 'help', './src/help');
+addModuleProperty(module, 'config', './src/config');
+addModuleProperty(module, 'create', './src/create', true);
+addModuleProperty(module, 'emulate', './src/emulate', true);
+addModuleProperty(module, 'plugin', './src/plugin', true);
+addModuleProperty(module, 'plugins', './src/plugin', true);
+addModuleProperty(module, 'serve', './src/serve');
+addModuleProperty(module, 'platform', './src/platform', true);
+addModuleProperty(module, 'platforms', './src/platform', true);
+addModuleProperty(module, 'compile', './src/compile', true);
+addModuleProperty(module, 'run', './src/run', true);
+addModuleProperty(module, 'info', './src/info', true);
+
+

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/b51e1c12/cordova-lib/src/cordova/create.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/cordova/create.js b/cordova-lib/src/cordova/create.js
new file mode 100644
index 0000000..025e56b
--- /dev/null
+++ b/cordova-lib/src/cordova/create.js
@@ -0,0 +1,220 @@
+/**
+    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 path          = require('path'),
+    fs            = require('fs'),
+    shell         = require('shelljs'),
+    platforms     = require('../platforms'),
+    help          = require('./help'),
+    events        = require('./events'),
+    config        = require('./config'),
+    lazy_load     = require('./lazy_load'),
+    Q             = require('q'),
+    CordovaError  = require('./CordovaError'),
+    ConfigParser = require('./ConfigParser'),
+    util          = require('./util');
+
+var DEFAULT_NAME = "HelloCordova",
+    DEFAULT_ID   = "io.cordova.hellocordova";
+
+/**
+ * Usage:
+ * @dir - directory where the project will be created. Required.
+ * @id - app id. Optional, default is DEFAULT_ID.
+ * @name - app name. Optional, default is DEFAULT_NAME.
+ * @cfg - extra config to be saved in .cordova/config.json
+ **/
+// Returns a promise.
+module.exports = create;
+function create(dir, id, name, cfg) {
+    if (!dir ) {
+        return Q(help());
+    }
+
+    // Massage parameters
+    if (typeof cfg == 'string') {
+        cfg = JSON.parse(cfg);
+    }
+    cfg = cfg || {};
+    id = id || cfg.id || DEFAULT_ID;
+    name = name || cfg.name || DEFAULT_NAME;
+
+    // Make absolute.
+    dir = path.resolve(dir);
+
+    events.emit('log', 'Creating a new cordova project with name "' + name + '" and id "' + id + '" at location "' + dir + '"');
+
+    var www_dir = path.join(dir, 'www');
+
+    // dir must be either empty or not exist at all.
+
+    // dir must be either empty except for .cordova config file or not exist at all..
+    var sanedircontents = function (d) {
+        var contents = fs.readdirSync(d);
+        if (contents.length === 0) {
+            return true;
+        } else if (contents.length == 1) {
+            if (contents[0] == '.cordova') {
+                return true;
+            }
+        }
+        return false;
+    };
+
+    if (fs.existsSync(dir) && !sanedircontents(dir)) {
+        return Q.reject(new CordovaError('Path already exists and is not empty: ' + dir));
+    }
+
+    // Read / Write .cordova/config.json file if necessary.
+    var config_json = config(dir, cfg);
+
+    var p;
+    var symlink = false; // Whether to symlink the www dir instead of copying.
+    var www_parent_dir;
+    var custom_config_xml;
+    var custom_merges;
+    var custom_hooks;
+
+    if (config_json.lib && config_json.lib.www) {
+        events.emit('log', 'Using custom www assets from '+config_json.lib.www.uri);
+        // TODO (kamrik): extend lazy_load for retrieval without caching to allow net urls for --src.
+        var www_version = config_json.lib.www.version || 'not_versioned';
+        var www_id = config_json.lib.www.id || 'dummy_id';
+        symlink  = !!config_json.lib.www.link;
+        if ( www_dir.indexOf(path.resolve(config_json.lib.www.uri)) === 0 ) {
+            throw new CordovaError(
+                'Project must not be created inside the www assets dir.' +
+                '\n    project dir:\t' + dir +
+                '\n    www assets dir:\t' + config_json.lib.www.uri
+            );
+        }
+        if(symlink) {
+            p = Q(config_json.lib.www.uri);
+            events.emit('verbose', 'Symlinking custom www assets into "' + www_dir + '"');
+        } else {
+            p = lazy_load.custom(config_json.lib.www.uri, www_id, 'www', www_version)
+            .then(function(d) {
+                events.emit('verbose', 'Copying custom www assets into "' + www_dir + '"');
+                return d;
+            });
+        }
+    } else {
+        // No custom www - use stock cordova-hello-world-app.
+        events.emit('verbose', 'Using stock cordova hello-world application.');
+        p = lazy_load.cordova('www')
+        .then(function(d) {
+            events.emit('verbose', 'Copying stock Cordova www assets into "' + www_dir + '"');
+            return d;
+        });
+    }
+
+    return p.then(function(www_lib) {
+        if (!fs.existsSync(www_lib)) {
+            throw new CordovaError('Could not find directory: '+www_lib);
+        }
+        // Keep going into child "www" folder if exists in stock app package.
+        while (fs.existsSync(path.join(www_lib, 'www'))) {
+            www_parent_dir = www_lib;
+            www_lib = path.join(www_lib, 'www');
+        }
+
+        // Find if we also have custom merges and config.xml as siblings of custom www.
+        if (www_parent_dir && config_json.lib && config_json.lib.www) {
+            custom_config_xml = path.join(www_parent_dir, 'config.xml');
+            if ( !fs.existsSync(custom_config_xml) ) {
+                custom_config_xml = null;
+            }
+            custom_merges = path.join(www_parent_dir, 'merges');
+            if ( !fs.existsSync(custom_merges) ) {
+                custom_merges = null;
+            }
+            custom_hooks = path.join(www_parent_dir, 'hooks');
+            if ( !fs.existsSync(custom_hooks) ) {
+                custom_hooks = null;
+            }
+        }
+
+        var dirAlreadyExisted = fs.existsSync(dir);
+        if (!dirAlreadyExisted) {
+            shell.mkdir(dir);
+        }
+        if (symlink) {
+            try {
+                fs.symlinkSync(www_lib, www_dir, 'dir');
+                if (custom_merges) {
+                    fs.symlinkSync(custom_merges, path.join(dir, 'merges'), 'dir');
+                }
+                if (custom_hooks) {
+                    fs.symlinkSync(custom_hooks, path.join(dir, 'hooks'), 'dir');
+                }
+                if (custom_config_xml) {
+                    fs.symlinkSync(custom_config_xml, path.join(dir, 'config.xml'));
+                }
+            } catch (e) {
+                if (!dirAlreadyExisted) {
+                    fs.rmdirSync(dir);
+                }
+                if (process.platform.slice(0, 3) == 'win' && e.code == 'EPERM')  {
+                    throw new CordovaError('Symlinks on Windows require Administrator privileges');
+                }
+                throw e;
+            }
+        } else {
+            shell.mkdir(www_dir);
+            shell.cp('-R', path.join(www_lib, '*'), www_dir);
+            if (custom_merges) {
+                var merges_dir = path.join(dir, 'merges');
+                shell.mkdir(merges_dir);
+                shell.cp('-R', path.join(custom_merges, '*'), merges_dir);
+            }
+            if (custom_hooks) {
+                var hooks_dir = path.join(dir, 'hooks');
+                shell.mkdir(hooks_dir);
+                shell.cp('-R', path.join(custom_hooks, '*'), hooks_dir);
+            }
+            if (custom_config_xml) {
+                shell.cp(custom_config_xml, path.join(dir, 'config.xml'));
+            }
+
+        }
+
+        // Create basic project structure.
+        shell.mkdir(path.join(dir, 'platforms'));
+        if ( !custom_merges) {
+            shell.mkdir(path.join(dir, 'merges'));
+        }
+        shell.mkdir(path.join(dir, 'plugins'));
+        shell.mkdir(path.join(dir, 'hooks'));
+
+        // Add hooks README.md
+        shell.cp(path.join(__dirname, '..', 'templates', 'hooks-README.md'), path.join(dir, 'hooks', 'README.md'));
+
+        var configPath = util.projectConfig(dir);
+        // Add template config.xml for apps that are missing it
+        if (!fs.existsSync(configPath)) {
+            var template_config_xml = path.join(__dirname, '..', 'templates', 'config.xml');
+            shell.cp(template_config_xml, configPath);
+            // Write out id and name to config.xml
+            var config = new ConfigParser(configPath);
+            config.setPackageName(id);
+            config.setName(name);
+            config.write();
+        }
+    });
+}

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/b51e1c12/cordova-lib/src/cordova/emulate.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/cordova/emulate.js b/cordova-lib/src/cordova/emulate.js
new file mode 100644
index 0000000..c1d3a10
--- /dev/null
+++ b/cordova-lib/src/cordova/emulate.js
@@ -0,0 +1,50 @@
+/**
+    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.
+*/
+
+/*global require: true, module: true, process: true*/
+/*jslint sloppy: true, white: true, newcap: true */
+
+var cordova_util      = require('./util'),
+    path              = require('path'),
+    hooker            = require('./hooker'),
+    superspawn        = require('./superspawn'),
+    Q                 = require('q');
+
+// Returns a promise.
+module.exports = function emulate(options) {
+    var projectRoot = cordova_util.cdProjectRoot();
+    options = cordova_util.preProcessOptions(options);
+
+    var hooks = new hooker(projectRoot);
+    return hooks.fire('before_emulate', options)
+    .then(function() {
+        // Run a prepare first!
+        return require('../cordova').raw.prepare(options.platforms);
+    }).then(function() {
+        // Deploy in parallel (output gets intermixed though...)
+        return Q.all(options.platforms.map(function(platform) {
+            var cmd = path.join(projectRoot, 'platforms', platform, 'cordova', 'run');
+            var args = ['--emulator'].concat(options.options);
+
+            return superspawn.spawn(cmd, args, {stdio: 'inherit', printCommand: true});
+        }));
+    }).then(function() {
+        return hooks.fire('after_emulate', options);
+    });
+};

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/b51e1c12/cordova-lib/src/cordova/events.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/cordova/events.js b/cordova-lib/src/cordova/events.js
new file mode 100644
index 0000000..be40fec
--- /dev/null
+++ b/cordova-lib/src/cordova/events.js
@@ -0,0 +1,23 @@
+/**
+    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 events = require('events');
+
+var emitter = new events.EventEmitter();
+
+module.exports = emitter;

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/b51e1c12/cordova-lib/src/cordova/hooker.js
----------------------------------------------------------------------
diff --git a/cordova-lib/src/cordova/hooker.js b/cordova-lib/src/cordova/hooker.js
new file mode 100644
index 0000000..e4ccaff
--- /dev/null
+++ b/cordova-lib/src/cordova/hooker.js
@@ -0,0 +1,161 @@
+/**
+    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 util  = require('./util'),
+    fs    = require('fs'),
+    os    = require('os'),
+    events= require('./events'),
+    superspawn = require('./superspawn'),
+    CordovaError = require('./CordovaError'),
+    Q     = require('q'),
+    path  = require('path'),
+    _ = require('underscore');
+
+module.exports = function hooker(root) {
+    var r = util.isCordova(root);
+    if (!r) throw new CordovaError('Not a Cordova project ("'+root+'"), can\'t use hooks.');
+    else this.root = r;
+};
+
+// Returns a promise.
+module.exports.fire = function global_fire(hook, opts) {
+    opts = opts || {};
+    var handlers = events.listeners(hook);
+    return execute_handlers_serially(handlers, opts);
+};
+
+function compareNumbers(a, b) {
+    return isNaN (parseInt(a))
+        ? a.toLowerCase().localeCompare(b.toLowerCase ? b.toLowerCase(): b)
+        : parseInt(a) > parseInt(b) ? 1 : parseInt(a) < parseInt(b) ? -1 : 0;
+}
+
+module.exports.prototype = {
+    // Returns a promise.
+    fire:function fire(hook, opts) {
+        var root = this.root;
+        opts = opts || {};
+        opts.root = root;
+
+        function fireHooksInDir(dir) {
+            if (!(fs.existsSync(dir))) {
+                return Q();
+            } else {
+                var scripts = fs.readdirSync(dir).sort(compareNumbers).filter(function(s) {
+                    return s[0] != '.';
+                });
+                return execute_scripts_serially(scripts, root, dir, opts);
+            }
+        }
+        // Fire JS hook for the event
+        // These ones need to "serialize" events, that is, each handler attached to the event needs to finish processing (if it "opted in" to the callback) before the next one will fire.
+        var handlers = events.listeners(hook);
+        return execute_handlers_serially(handlers, opts)
+        .then(function() {
+            return fireHooksInDir(path.join(root, '.cordova', 'hooks', hook));
+        }).then(function() {
+            return fireHooksInDir(path.join(root, 'hooks', hook));
+        });
+    }
+};
+
+function extractSheBangInterpreter(fullpath) {
+    var hookFd = fs.openSync(fullpath, "r");
+    try {
+        // this is a modern cluster size. no need to read less
+        var fileData = new Buffer (4096);
+        var octetsRead = fs.readSync(hookFd, fileData, 0, 4096, 0);
+        var fileChunk = fileData.toString();
+    } finally {
+        fs.closeSync(hookFd);
+    }
+
+    var hookCmd, shMatch;
+    // Filter out /usr/bin/env so that "/usr/bin/env node" works like "node".
+    var shebangMatch = fileChunk.match(/^#!(?:\/usr\/bin\/env )?([^\r\n]+)/m);
+    if (octetsRead == 4096 && !fileChunk.match(/[\r\n]/))
+        events.emit('warn', 'shebang is too long for "' + fullpath + '"');
+    if (shebangMatch)
+        hookCmd = shebangMatch[1];
+    // Likewise, make /usr/bin/bash work like "bash".
+    if (hookCmd)
+        shMatch = hookCmd.match(/bin\/((?:ba)?sh)$/)
+    if (shMatch)
+        hookCmd = shMatch[1]
+    return hookCmd;
+}
+
+// Returns a promise.
+function execute_scripts_serially(scripts, root, dir, opts) {
+    opts = opts || {};
+    var isWindows = os.platform().slice(0, 3) === 'win';
+    if (scripts.length) {
+        var s = scripts.shift();
+        var fullpath = path.join(dir, s);
+        if (fs.statSync(fullpath).isDirectory()) {
+            events.emit('verbose', 'skipped directory "' + fullpath + '" within hook directory');
+            return execute_scripts_serially(scripts, root, dir, opts); // skip directories if they're in there.
+        } else {
+            var command = fullpath;
+            var args = [root];
+            if (os.platform().slice(0, 3) == 'win') {
+                // TODO: Make shebang sniffing a setting (not everyone will want this).
+                var interpreter = extractSheBangInterpreter(fullpath);
+                // we have shebang, so try to run this script using correct interpreter
+                if (interpreter) {
+                    args.unshift(command);
+                    command = interpreter;
+                }
+            }
+
+            var execOpts = {cwd: root, printCommand: true, stdio: 'inherit'};
+            execOpts.env = {};
+            execOpts.env.CORDOVA_VERSION = require('../package').version;
+            execOpts.env.CORDOVA_PLATFORMS = opts.platforms ? opts.platforms.join() : '';
+            execOpts.env.CORDOVA_PLUGINS = opts.plugins?opts.plugins.join():'';
+            execOpts.env.CORDOVA_HOOK = fullpath;
+            execOpts.env.CORDOVA_CMDLINE = process.argv.join(' ');
+
+            return superspawn.spawn(command, args, execOpts)
+            .catch(function(err) {
+                // Don't treat non-executable files as errors. They could be READMEs, or Windows-only scripts.
+                if (!isWindows && err.code == 'EACCES') {
+                    events.emit('verbose', 'skipped non-executable file: ' + fullpath);
+                } else {
+                    throw new CordovaError('Hook failed with error code ' + err.code + ': ' + fullpath);
+                }
+            }).then(function() {
+                return execute_scripts_serially(scripts, root, dir, opts);
+            });
+        }
+    } else {
+        return Q(); // Nothing to do.
+    }
+}
+
+// Returns a promise.
+function execute_handlers_serially(handlers, opts) {
+    if (handlers.length) {
+        // Chain the handlers in series.
+        return handlers.reduce(function(soFar, f) {
+            return soFar.then(function() { return f(opts) });
+        }, Q());
+    } else {
+        return Q(); // Nothing to do.
+    }
+}


Mime
View raw message