Comparing version 0.8.0 to 0.8.1
@@ -31,1 +31,6 @@ plugman installs and uninstalls plugin.xml-compatible cordova plugins into cordova-generated projects. | ||
- plugins_dir <directory>: a copy of the plugin will be stored in this directory. Default is to install into the <project directory>/plugins folder | ||
Optional flags | ||
-------------- | ||
--debug : Verbose mode |
@@ -63,2 +63,9 @@ #!/usr/bin/env node | ||
// Set up appropriate logging based on events | ||
if (cli_opts.debug) { | ||
plugman.on('log', console.log); | ||
} | ||
plugman.on('warn', console.warn); | ||
plugman.on('error', console.error); | ||
if (cli_opts.v) { | ||
@@ -68,3 +75,3 @@ console.log(package.name + ' version ' + package.version); | ||
else if (!cli_opts.platform || !cli_opts.project || !cli_opts.plugin) { | ||
plugman.help(); | ||
console.log(plugman.help()); | ||
} | ||
@@ -71,0 +78,0 @@ else if (cli_opts.uninstall) { |
@@ -5,3 +5,3 @@ { | ||
"description": "install/uninstall Cordova plugins", | ||
"version": "0.8.0", | ||
"version": "0.8.1", | ||
"repository": { | ||
@@ -24,3 +24,3 @@ "type": "git", | ||
"elementtree": "0.1.x", | ||
"xcode": "git+https://github.com/filmaj/node-xcode.git", | ||
"xcode": "0.6.1", | ||
"plist": "0.4.x", | ||
@@ -69,4 +69,7 @@ "bplist-parser": "0.0.x", | ||
"name":"Fil Maj" | ||
}, | ||
{ | ||
"name":"Michael Brooks" | ||
} | ||
] | ||
} |
@@ -21,9 +21,15 @@ /* | ||
// copyright (c) 2013 Andrew Lunny, Adobe Systems | ||
var emitter = require('./src/events'); | ||
module.exports = { | ||
help: require('./src/help'), | ||
install: require('./src/install'), | ||
uninstall:require('./src/uninstall'), | ||
fetch: require('./src/fetch'), | ||
prepare: require('./src/prepare'), | ||
config_changes:require('./src/util/config-changes') | ||
help: require('./src/help'), | ||
install: require('./src/install'), | ||
uninstall: require('./src/uninstall'), | ||
fetch: require('./src/fetch'), | ||
prepare: require('./src/prepare'), | ||
config_changes: require('./src/util/config-changes'), | ||
on: emitter.addListener, | ||
off: emitter.removeListener, | ||
removeAllListeners: emitter.removeAllListeners, | ||
}; |
@@ -11,2 +11,3 @@ var install = require('../src/install'), | ||
semver = require('semver'), | ||
events = require('../src/events'), | ||
temp = __dirname, | ||
@@ -54,3 +55,3 @@ dummyplugin = 'DummyPlugin', | ||
it('should notify if plugin is already installed into project', function() { | ||
var spy = spyOn(console, 'log'); | ||
var spy = spyOn(events, 'emit'); | ||
get_json.andReturn({ | ||
@@ -63,3 +64,3 @@ installed_plugins:{ | ||
install('android', temp, dummyplugin, plugins_dir, {}); | ||
expect(spy).toHaveBeenCalledWith('Plugin "'+dummy_id+'" already installed, \'sall good.'); | ||
expect(spy).toHaveBeenCalledWith('log', 'Plugin "'+dummy_id+'" already installed, \'sall good.'); | ||
}); | ||
@@ -66,0 +67,0 @@ it('should check version if plugin has engine tag', function(){ |
@@ -136,3 +136,3 @@ var ios = require('../../src/platforms/ios'), | ||
ios['source-file'].install(source[0], dummyplugin, temp, dummy_id, proj_files); | ||
expect(spy).toHaveBeenCalledWith(path.join('Plugins', dummy_id, 'DummyPluginCommand.m')); | ||
expect(spy).toHaveBeenCalledWith(path.join('Plugins', dummy_id, 'DummyPluginCommand.m'), {}); | ||
}); | ||
@@ -143,3 +143,3 @@ it('should call into xcodeproj\'s addSourceFile appropriately when element has a target-dir', function() { | ||
ios['source-file'].install(source[0], dummyplugin, temp, dummy_id, proj_files); | ||
expect(spy).toHaveBeenCalledWith(path.join('Plugins', dummy_id, 'targetDir', 'TargetDirTest.m')); | ||
expect(spy).toHaveBeenCalledWith(path.join('Plugins', dummy_id, 'targetDir', 'TargetDirTest.m'), {}); | ||
}); | ||
@@ -146,0 +146,0 @@ it('should cp the file to the right target location when element has no target-dir', function() { |
@@ -31,18 +31,20 @@ #!/usr/bin/env node | ||
describe('clonePluginGitRepo', function(){ | ||
it('should shell out to git clone with correct arguments', function(){ | ||
var execSpy = spyOn(shell, 'exec').andReturn({ | ||
code: 0, | ||
output: 'git output' | ||
var fake_id = 'VillageDrunkard'; | ||
var execSpy, cp_spy, xml_spy; | ||
beforeEach(function() { | ||
execSpy = spyOn(shell, 'exec').andCallFake(function(cmd, opts, cb) { | ||
cb(0, 'git output'); | ||
}); | ||
var fake_id = 'fake.plugin.id'; | ||
var xml = { | ||
getroot: function() { | ||
return { attrib: { id: fake_id } }; | ||
spyOn(shell, 'which').andReturn(true); | ||
cp_spy = spyOn(shell, 'cp'); | ||
xml_spy = spyOn(xml_helpers, 'parseElementtreeSync').andReturn({ | ||
getroot:function() { | ||
return { | ||
attrib:{id:fake_id} | ||
}; | ||
} | ||
}; | ||
spyOn(xml_helpers, 'parseElementtreeSync').andReturn(xml); | ||
spyOn(shell, 'cp'); | ||
}); | ||
}); | ||
it('should shell out to git clone with correct arguments', function(){ | ||
var plugin_git_url = 'https://github.com/imhotep/ChildBrowser' | ||
var callback = jasmine.createSpy(); | ||
@@ -61,12 +63,2 @@ | ||
it('should take into account subdirectory argument when copying over final repository into plugins+plugin_id directory', function() { | ||
var exec_spy = spyOn(shell, 'exec').andReturn({ code: 0, output: 'git clone output' }); | ||
var cp_spy = spyOn(shell, 'cp'); | ||
var fake_id = 'VillageDrunkard'; | ||
var xml_spy = spyOn(xml_helpers, 'parseElementtreeSync').andReturn({ | ||
getroot:function() { | ||
return { | ||
attrib:{id:fake_id} | ||
}; | ||
} | ||
}); | ||
var plugin_git_url = 'https://github.com/imhotep/ChildBrowser' | ||
@@ -73,0 +65,0 @@ |
@@ -7,2 +7,3 @@ var shell = require('shelljs'), | ||
metadata = require('./util/metadata'), | ||
events = require('./events'), | ||
path = require('path'); | ||
@@ -12,2 +13,3 @@ | ||
module.exports = function fetchPlugin(plugin_dir, plugins_dir, options, callback) { | ||
events.emit('log', 'Fetching plugin from location "' + plugin_dir + '"...'); | ||
// Ensure the containing directory exists. | ||
@@ -37,3 +39,6 @@ shell.mkdir('-p', plugins_dir); | ||
plugins.clonePluginGitRepo(plugin_dir, plugins_dir, options.subdir, options.git_ref, function(err, dir) { | ||
if (!err) { | ||
if (err) { | ||
if (callback) callback(err); | ||
else throw err; | ||
} else { | ||
metadata.save_fetch_metadata(dir, data); | ||
@@ -49,3 +54,5 @@ if (callback) callback(null, dir); | ||
plugin_dir = path.join(uri.path, options.subdir); | ||
var xml = xml_helpers.parseElementtreeSync(path.join(plugin_dir, 'plugin.xml')); | ||
var plugin_xml_path = path.join(plugin_dir, 'plugin.xml'); | ||
events.emit('log', 'Fetch is reading plugin.xml from location "' + plugin_xml_path + '"...'); | ||
var xml = xml_helpers.parseElementtreeSync(plugin_xml_path); | ||
var plugin_id = xml.getroot().attrib.id; | ||
@@ -57,6 +64,8 @@ | ||
if (options.link) { | ||
events.emit('log', 'Symlinking from location "' + plugin_dir + '" to location "' + dest + '"'); | ||
fs.symlinkSync(plugin_dir, dest, 'dir'); | ||
} else { | ||
shell.mkdir('-p', dest); | ||
shell.cp('-R', path.join(plugin_dir, '*') , dest); | ||
events.emit('log', 'Copying from location "' + plugin_dir + '" to location "' + dest + '"'); | ||
shell.cp('-R', path.join(plugin_dir, '*'), dest); | ||
} | ||
@@ -63,0 +72,0 @@ |
@@ -6,3 +6,3 @@ var fs = require('fs'), | ||
module.exports = function help() { | ||
console.log(fs.readFileSync(doc_txt, 'utf-8')); | ||
return fs.readFileSync(doc_txt, 'utf-8'); | ||
}; |
@@ -5,2 +5,3 @@ var path = require('path'), | ||
n = require('ncallbacks'), | ||
events = require('./events'), | ||
action_stack = require('./util/action-stack'), | ||
@@ -26,3 +27,3 @@ shell = require('shelljs'), | ||
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 | ||
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) | ||
@@ -39,5 +40,4 @@ 4. handleInstall | ||
var err = new Error(platform + " not supported."); | ||
if (callback) callback(err); | ||
if (callback) return callback(err); | ||
else throw err; | ||
return; | ||
} | ||
@@ -78,2 +78,3 @@ | ||
var plugin_id = plugin_et.getroot().attrib['id']; | ||
events.emit('log', 'Starting installation of "' + plugin_id + '"...'); | ||
@@ -95,3 +96,3 @@ // check if platform has plugin installed already. | ||
if (is_installed) { | ||
console.log('Plugin "' + plugin_id + '" already installed, \'sall good.'); | ||
events.emit('log', 'Plugin "' + plugin_id + '" already installed, \'sall good.'); | ||
if (callback) callback(); | ||
@@ -130,3 +131,3 @@ return; | ||
} else { | ||
console.log('Warning: cordova version not detected. installing anyway.'); | ||
events.emit('log', 'Cordova project version not detected (lacks a ./cordova/version script), continuing.'); | ||
} | ||
@@ -156,2 +157,3 @@ | ||
if (dependencies && dependencies.length) { | ||
events.emit('log', 'Dependencies detected, iterating through them...'); | ||
var end = n(dependencies.length, function() { | ||
@@ -212,3 +214,3 @@ handleInstall(actions, plugin_id, plugin_et, platform, project_dir, plugins_dir, plugin_basename, plugin_dir, filtered_variables, options.www_dir, options.is_top_level, callback); | ||
if (fs.existsSync(dep_plugin_dir)) { | ||
console.log('Dependent plugin ' + dep_plugin_id + ' already fetched, using that version.'); | ||
events.emit('log', 'Dependent plugin "' + dep_plugin_id + '" already fetched, using that version.'); | ||
var opts = { | ||
@@ -221,3 +223,3 @@ cli_variables: filtered_variables, | ||
} else { | ||
console.log('Dependent plugin ' + dep_plugin_id + ' not fetched, retrieving then installing.'); | ||
events.emit('log', 'Dependent plugin "' + dep_plugin_id + '" not fetched, retrieving then installing.'); | ||
var opts = { | ||
@@ -245,3 +247,3 @@ cli_variables: filtered_variables, | ||
function handleInstall(actions, plugin_id, plugin_et, platform, project_dir, plugins_dir, plugin_basename, plugin_dir, filtered_variables, www_dir, is_top_level, callback) { | ||
console.log('Installing plugin ' + plugin_id + '...'); | ||
events.emit('log', 'Installing plugin ' + plugin_id + '...'); | ||
var handler = platform_modules[platform]; | ||
@@ -306,3 +308,3 @@ www_dir = www_dir || handler.www_dir(project_dir); | ||
console.log(plugin_id + ' installed.'); | ||
events.emit('log', plugin_id + ' installed.'); | ||
if (callback) callback(); | ||
@@ -309,0 +311,0 @@ } |
@@ -42,2 +42,3 @@ /* | ||
var is_framework = source_el.attrib['framework'] && (source_el.attrib['framework'] == 'true' || source_el.attrib['framework'] == true); | ||
var has_flags = source_el.attrib['compiler-flags'] && source_el.attrib['compiler-flags'].length ? true : false ; | ||
@@ -47,3 +48,3 @@ if (!fs.existsSync(srcFile)) throw new Error('cannot find "' + srcFile + '" ios <source-file>'); | ||
var project_ref = path.join('Plugins', path.relative(project.plugins_dir, destFile)); | ||
project.xcode.addSourceFile(project_ref); | ||
project.xcode.addSourceFile(project_ref, has_flags ? {compilerFlags:source_el.attrib['compiler-flags']} : {}); | ||
if (is_framework) { | ||
@@ -50,0 +51,0 @@ var weak = source_el.attrib['weak']; |
@@ -21,2 +21,3 @@ /** | ||
var platform_modules = require('./platforms'), | ||
events = require('./events'), | ||
path = require('path'), | ||
@@ -46,2 +47,3 @@ config_changes = require('./util/config-changes'), | ||
events.emit('log', 'Preparing ' + platform + ' project, starting with processing of config changes...'); | ||
config_changes.process(plugins_dir, project_dir, platform); | ||
@@ -57,2 +59,3 @@ | ||
var moduleObjects = []; | ||
events.emit('log', 'Iterating over installed plugins...'); | ||
@@ -127,2 +130,3 @@ plugins && plugins.forEach(function(plugin) { | ||
events.emit('log', 'Writing out cordova_plugins.json...'); | ||
// Write out moduleObjects as JSON to cordova_plugins.json | ||
@@ -135,3 +139,4 @@ fs.writeFileSync(path.join(wwwDir, 'cordova_plugins.json'), JSON.stringify(moduleObjects), 'utf-8'); | ||
final_contents += '});'; | ||
events.emit('log', 'Writing out cordova_plugins.js...'); | ||
fs.writeFileSync(path.join(wwwDir, 'cordova_plugins.js'), final_contents, 'utf-8'); | ||
}; |
@@ -9,2 +9,3 @@ var path = require('path'), | ||
n = require('ncallbacks'), | ||
events = require('./events'), | ||
dependencies = require('./util/dependencies'), | ||
@@ -18,5 +19,4 @@ underscore = require('underscore'), | ||
var err = new Error(platform + " not supported."); | ||
if (callback) callback(err); | ||
if (callback) return callback(err); | ||
else throw err; | ||
return; | ||
} | ||
@@ -28,5 +28,4 @@ | ||
var err = new Error('Plugin "' + id + '" not found. Already uninstalled?'); | ||
if (callback) callback(err); | ||
if (callback) return callback(err); | ||
else throw err; | ||
return; | ||
} | ||
@@ -59,5 +58,4 @@ | ||
var err = new Error('Another top-level plugin (' + tlp + ') relies on plugin ' + plugin_id + ', therefore aborting uninstallation.'); | ||
if (callback) callback(err); | ||
if (callback) return callback(err); | ||
else throw err; | ||
return; | ||
} | ||
@@ -72,2 +70,3 @@ diff_arr.push(ds); | ||
if (dependents.length && danglers && danglers.length) { | ||
events.emit('log', 'Uninstalling ' + danglers.length + ' dangling dependent plugins...'); | ||
var end = n(danglers.length, function() { | ||
@@ -96,2 +95,3 @@ handleUninstall(actions, platform, plugin_id, plugin_et, project_dir, options.www_dir, plugins_dir, plugin_dir, options.is_top_level, callback); | ||
www_dir = www_dir || handler.www_dir(project_dir); | ||
events.emit('log', 'Uninstalling ' + plugin_id + '...'); | ||
@@ -143,3 +143,3 @@ var assets = plugin_et.findall('./asset'); | ||
shell.rm('-rf', plugin_dir); | ||
console.log(plugin_id + ' uninstalled.'); | ||
events.emit('log', plugin_id + ' uninstalled.'); | ||
if (callback) callback(); | ||
@@ -146,0 +146,0 @@ } |
var ios = require('../platforms/ios'), | ||
wp7 = require('../platforms/wp7'), | ||
wp8 = require('../platforms/wp8'), | ||
events = require('../events'), | ||
fs = require('fs'); | ||
@@ -27,9 +29,16 @@ | ||
process:function(platform, project_dir, callback) { | ||
events.emit('log', 'Beginning processing of action stack for ' + platform + ' project...'); | ||
var project_files; | ||
// parse platform-specific project files once | ||
if (platform == 'ios') { | ||
events.emit('log', 'Parsing iOS project files...'); | ||
project_files = ios.parseIOSProjectFiles(project_dir); | ||
} | ||
if (platform == 'wp7') { | ||
events.emit('log', 'Parsing WP7 project files...'); | ||
project_files = wp7.parseWP7ProjectFile(project_dir); | ||
} | ||
if (platform == 'wp8') { | ||
events.emit('log', 'Parsing WP8 project files...'); | ||
project_files = wp8.parseWP8ProjectFile(project_dir); | ||
} | ||
@@ -40,6 +49,7 @@ while(this.stack.length) { | ||
var action_params = action.handler.params; | ||
if (platform == 'ios' || platform == 'wp7') action_params.push(project_files); | ||
if (platform == 'ios' || platform == 'wp7' || platform == 'wp8') action_params.push(project_files); | ||
try { | ||
handler.apply(null, action_params); | ||
} catch(e) { | ||
events.emit('warn', 'Error during processing of action! Attempting to revert...'); | ||
var incomplete = this.stack.unshift(action); | ||
@@ -52,22 +62,24 @@ var issue = 'Uh oh!\n'; | ||
var revert_params = undo.reverter.params; | ||
if (platform == 'ios' || platform == 'wp7') revert_params.push(project_files); | ||
if (platform == 'ios' || platform == 'wp7' || platform == 'wp8') revert_params.push(project_files); | ||
try { | ||
revert.apply(null, revert_params); | ||
} catch(err) { | ||
events.emit('warn', 'Error during reversion of action! We probably really messed up your project now, sorry! D:'); | ||
issue += 'A reversion action failed: ' + err.message + '\n'; | ||
} | ||
} | ||
console.log(e.stack); | ||
e.message = issue + e.message; | ||
if (callback) callback(e); | ||
if (callback) return callback(e); | ||
else throw e; | ||
return; | ||
} | ||
this.completed.push(action); | ||
} | ||
events.emit('log', 'Action stack processing complete.'); | ||
if (platform == 'ios') { | ||
// write out xcodeproj file | ||
events.emit('log', 'Writing out iOS pbxproj file...'); | ||
fs.writeFileSync(project_files.pbx, project_files.xcode.writeSync()); | ||
} | ||
if (platform == 'wp7') { | ||
if (platform == 'wp7' || platform == 'wp8') { | ||
events.emit('log', 'Writing out ' + platform + ' project files...'); | ||
project_files.write(); | ||
@@ -74,0 +86,0 @@ } |
@@ -26,2 +26,3 @@ /* | ||
et = require('elementtree'), | ||
events = require('../events'), | ||
xml_helpers = require('./../util/xml-helpers'), | ||
@@ -28,0 +29,0 @@ plist_helpers = require('./../util/plist-helpers'); |
@@ -1,2 +0,1 @@ | ||
#!/usr/bin/env node | ||
/* | ||
@@ -28,2 +27,3 @@ * | ||
xml_helpers = require('./xml-helpers'), | ||
events = require('../events'), | ||
tmp_dir = path.join(os.tmpdir(), 'plugman-tmp'); | ||
@@ -37,3 +37,3 @@ | ||
var err = new Error('git command line is not installed'); | ||
if (callback) callback(err); | ||
if (callback) return callback(err); | ||
else throw err; | ||
@@ -46,37 +46,41 @@ } | ||
var cmd = util.format('git clone "%s" "%s"', plugin_git_url, path.basename(tmp_dir)); | ||
var result = shell.exec(cmd, {silent: true, async:false}); | ||
if (result.code > 0) { | ||
var err = new Error('failed to get the plugin via git from URL '+ plugin_git_url + ', output: ' + result.output); | ||
if (callback) callback(err) | ||
else throw err; | ||
} else { | ||
console.log('Plugin "' + plugin_git_url + '" fetched.'); | ||
// Check out the specified revision, if provided. | ||
if (git_ref) { | ||
var cmd = util.format('cd "%s" && git checkout "%s"', tmp_dir, git_ref); | ||
var result = shell.exec(cmd, { silent: true, async:false }); | ||
if (result.code > 0) { | ||
var err = new Error('failed to checkout git ref "' + git_ref + '" for plugin at git url "' + plugin_git_url + '", output: ' + result.output); | ||
if (callback) callback(err); | ||
else throw err; | ||
events.emit('log', 'Fetching plugin via git-clone command: ' + cmd); | ||
shell.exec(cmd, {silent: true, async:true}, function(code, output) { | ||
if (code > 0) { | ||
var err = new Error('failed to get the plugin via git from URL '+ plugin_git_url + ', output: ' + output); | ||
if (callback) return callback(err) | ||
else throw err; | ||
} else { | ||
events.emit('log', 'Plugin "' + plugin_git_url + '" fetched.'); | ||
// Check out the specified revision, if provided. | ||
if (git_ref) { | ||
var cmd = util.format('cd "%s" && git checkout "%s"', tmp_dir, git_ref); | ||
var result = shell.exec(cmd, { silent: true, async:false }); | ||
if (result.code > 0) { | ||
var err = new Error('failed to checkout git ref "' + git_ref + '" for plugin at git url "' + plugin_git_url + '", output: ' + result.output); | ||
if (callback) return callback(err); | ||
else throw err; | ||
} | ||
events.emit('log', 'Plugin "' + plugin_git_url + '" checked out to git ref "' + git_ref + '".'); | ||
} | ||
console.log('Checked out ' + git_ref); | ||
} | ||
// Read the plugin.xml file and extract the plugin's ID. | ||
tmp_dir = path.join(tmp_dir, subdir); | ||
// TODO: what if plugin.xml does not exist? | ||
var xml_file = path.join(tmp_dir, 'plugin.xml'); | ||
var xml = xml_helpers.parseElementtreeSync(xml_file); | ||
var plugin_id = xml.getroot().attrib.id; | ||
// Read the plugin.xml file and extract the plugin's ID. | ||
tmp_dir = path.join(tmp_dir, subdir); | ||
// TODO: what if plugin.xml does not exist? | ||
var xml_file = path.join(tmp_dir, 'plugin.xml'); | ||
var xml = xml_helpers.parseElementtreeSync(xml_file); | ||
var plugin_id = xml.getroot().attrib.id; | ||
// TODO: what if a plugin dependended on different subdirectories of the same plugin? this would fail. | ||
// should probably copy over entire plugin git repo contents into plugins_dir and handle subdir seperately during install. | ||
var plugin_dir = path.join(plugins_dir, plugin_id); | ||
shell.cp('-R', path.join(tmp_dir, '*'), plugin_dir); | ||
// TODO: what if a plugin dependended on different subdirectories of the same plugin? this would fail. | ||
// should probably copy over entire plugin git repo contents into plugins_dir and handle subdir seperately during install. | ||
events.emit('log', 'Copying fetched plugin over "' + plugin_dir + '"...'); | ||
var plugin_dir = path.join(plugins_dir, plugin_id); | ||
shell.cp('-R', path.join(tmp_dir, '*'), plugin_dir); | ||
if (callback) callback(null, plugin_dir); | ||
} | ||
events.emit('log', 'Plugin "' + plugin_id + '" fetched.'); | ||
if (callback) callback(null, plugin_dir); | ||
} | ||
}); | ||
} | ||
}; | ||
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Git dependency
Supply chain riskContains a dependency which resolves to a remote git URL. Dependencies fetched from git URLs are not immutable can be used to inject untrusted code or reduce the likelihood of a reproducible install.
Found 1 instance in 1 package
2651833
10002
0
+ Addednode-uuid@1.3.3(transitive)
+ Addedpegjs@0.6.2(transitive)
+ Addedxcode@0.6.1(transitive)
Updatedxcode@0.6.1