define(function(require, exports, module) { main.consumes = ["Plugin", "cli_commands", "proc", "api", "auth"]; main.provides = ["cli.publish"]; return main; function main(options, imports, register) { var Plugin = imports.Plugin; var cmd = imports.cli_commands; var proc = imports.proc; var auth = imports.auth; var api = imports.api; var SHELLSCRIPT = require("text!./publish.git.sh").toString("utf8"); var TAR = "tar"; var APIHOST = options.apiHost; var BASICAUTH = process.env.C9_TEST_AUTH; var SCM = { "git": { binary: "git", clone: "clone" }, "mercurial": { binary: "hg", clone: "clone" }, "hg": { binary: "hg", clone: "clone" } }; var fs = require("fs"); var join = require("path").join; var os = require("os"); var FormData = require("form-data"); var http = require(APIHOST.indexOf("localhost") > -1 ? "http" : "https"); var basename = require("path").basename; var verbose = false; var force = false; // Set up basic auth for api if needed if (BASICAUTH) api.basicAuth = BASICAUTH; /***** Initialization *****/ var plugin = new Plugin("Ajax.org", main.consumes); // var emit = plugin.getEmitter(); var loaded; function load(){ if (loaded) return; loaded = true; cmd.addCommand({ name: "publish", info: " Publishes a cloud9 package.", usage: "[--verbose] [--force] [ | major | minor | patch | build]", options: { "verbose" : { "description": "Output more information", "alias": "v", "default": false, "boolean": true }, "force" : { "description": "Ignore warnings", "alias": "f", "default": false, "boolean": true } }, check: function(argv) { if (argv._.length < 2 && !argv["newversion"]) throw new Error("Missing version"); }, exec: function(argv) { verbose = argv["verbose"]; force = argv["force"]; publish( argv._[1], function(err, data){ if (err) { if (err.message || typeof err == "string") console.error(err.message || err); if (!verbose) console.error("\nTry running with --verbose flag for more information"); process.exit(1); } else { console.log("Succesfully published version", data.version); process.exit(0); } }); } }); cmd.addCommand({ name: "unpublish", info: "Disables a cloud9 package.", usage: "[--verbose]", options: { "verbose" : { "description": "Output more information", "alias": "v", "default": false, "boolean": true } }, check: function(argv) {}, exec: function(argv) { verbose = argv["verbose"]; unpublish( function(err, data){ if (err) { console.error(err.message || err || "Terminated."); process.exit(1); } else { console.log("Succesfully disabled package"); process.exit(0); } }); } }); cmd.addCommand({ name: "install", info: " Installs a cloud9 package.", usage: "[--verbose] [--force] [--global] [--local] [--debug] [@]", // @TODO --global, --debug, --local options: { "local": { description: "", "default": false, "boolean": true }, "global": { description: "", "default": false, "boolean": true }, "debug": { description: "", "default": false, "boolean": true }, "package" : { description: "", "default": false }, "verbose" : { "description": "Output more information", "alias": "v", "default": false, "boolean": true }, "force" : { "description": "Ignore warnings", "alias": "f", "default": false, "boolean": true } }, check: function(argv) { if (argv._.length < 2 && !argv["package"]) throw new Error("package"); }, exec: function(argv) { verbose = argv["verbose"]; force = argv["force"]; if (argv.accessToken) auth.accessToken = argv.accessToken; if (!argv.local && !argv.debug) { if (!process.env.C9_PID) { console.warn("It looks like you are not running on c9.io. Will default to local installation of the package"); argv.local = true; } } var name = argv._[1]; install( name, { global: argv.global, local: argv.local, debug: argv.debug }, function(err, data){ if (err) { console.error(err.message || "Terminated."); process.exit(1); } else { console.log("Succesfully installed", name + (argv.debug ? "" : "@" + data.version)); process.exit(0); } }); } }); cmd.addCommand({ name: "remove", info: " Removes a cloud9 package.", usage: "[--verbose] [--global] [--local] ", // @TODO --global options: { "local": { description: "", "default": false, "boolean": true }, "global": { description: "", "default": false, "boolean": true }, "package" : { description: "" }, "verbose" : { "description": "Output more information", "alias": "v", "default": false, "boolean": true } }, check: function(argv) { if (argv._.length < 2 && !argv["package"]) throw new Error("package"); }, exec: function(argv) { verbose = argv["verbose"]; if (argv.accessToken) auth.accessToken = argv.accessToken; var name = argv._[1]; uninstall( name, { global: argv.global, local: argv.local }, function(err, data){ if (err) { console.error(err.message || "Terminated."); process.exit(1); } else { console.log("Succesfully removed", name); process.exit(0); } }); } }); cmd.addCommand({ name: "list", info: " Lists all available packages.", usage: "[--json]", options: { "json": { description: "", "default": false, "boolean": true }, }, check: function(argv) {}, exec: function(argv) { verbose = argv["verbose"]; list(argv.json); } }); } /***** Methods *****/ function stringifyError(err){ return (verbose ? JSON.stringify(err, 4, " ") : (typeof err == "string" ? err : err.message)); } function list(asJson, callback){ callback = callback || function(){}; api.packages.get("", function(err, list){ if (err) { console.error("ERROR: Could not get list: ", stringifyError(err)); return callback(err); } if (asJson) { console.log(JSON.stringify(list, 4, " ")); return callback(null, list); } else { list.forEach(function(item){ console.log(item.name, "https://c9.io/packages/" + item.name); }); return callback(null, list); } }); } function publish(version, callback) { var cwd = process.cwd(); var packagePath = cwd + "/package.json"; fs.readFile(packagePath, function(err, data){ if (err) return callback(new Error("ERROR: Could not find package.json in " + cwd)); var json; try { json = JSON.parse(data); } catch(e) { return callback(new Error("ERROR: Could not parse package.json: ", e.message)); } // Basic Validation if (!json.name) return callback(new Error("ERROR: Missing name property in package.json")); if (basename(cwd) != json.name) { console.warn("WARNING: The name property in package.json is not equal to the directory name, which is " + basename(cwd)); if (!force) return callback(new Error("Use --force to ignore this warning.")); } if (!json.description) return callback(new Error("ERROR: Missing description property in package.json")); if (!json.repository) return callback(new Error("ERROR: Missing repository property in package.json")); if (!json.categories || json.categories.length == 0) return callback(new Error("ERROR: At least one category is required in package.json")); // Validate README.md if (!fs.existsSync(join(cwd, "README.md"))) { console.warn("WARNING: README.md is missing."); if (!force) return callback(new Error("Use --force to ignore these warnings.")); } // Validate plugins var plugins = {}; fs.readdirSync(cwd).map(function(filename) { if (/_test\.js$/.test(filename) || !/.js/.test(filename)) return; var val = fs.readFileSync(cwd + "/" + filename); if (!/\(options,\s*imports,\s*register\)/.test(val)) return; if (!/consumes\s*=/.test(val)) return; if (!/provides\s*=/.test(val)) return; plugins[filename] = {}; }); var warned, failed; Object.keys(plugins).forEach(function(name){ if (!json.plugins[name.replace(/\.js$/, "")]) { console.warn("WARNING: Plugin '" + name + "' is not listed in package.json."); warned = true; } // @TODO temporarily disabled the requirement for tests while tests cannot actually run yet // else if (!fs.existsSync(join(cwd, name.replace(/\.js$/, "_test.js")))) { // console.warn("ERROR: Plugin '" + name + "' has no test associated with it."); // failed = true; // } }); if (failed) return callback(new Error()); if (warned && !force) return callback(new Error("Use --force to ignore these warnings.")); var v = (json.version || "0.0.1").split("."); // Update the version field in the package.json file if (version == "major") { v[0]++; v[1] = 0; v[2] = 0; } else if (version == "minor") { v[1]++; v[2] = 0; } else if (version == "patch" || version == "build") v[2]++; else if (version.match(/^\d+\.\d+\.\d+$/)) v = version.split("."); else return callback(new Error("Invalid version. Semver required: " + version)); json.version = v.join("."); // Write the package.json file fs.writeFile(packagePath, JSON.stringify(json, 1, " "), function(err){ if (err) return callback(err); SHELLSCRIPT = SHELLSCRIPT .replace(/\$1/, packagePath) .replace(/\$2/, json.version); proc.spawn("bash", { args: ["-c", SHELLSCRIPT] }, function(err, p){ if (err) return callback(err); if (verbose) { p.stdout.on("data", function(c){ process.stdout.write(c.toString("utf8")); }); p.stderr.on("data", function(c){ process.stderr.write(c.toString("utf8")); }); } p.on("exit", function(code, stderr, stdout){ if (code !== 0) return callback(new Error("ERROR: publish failed with exit code " + code)); console.log("Created tag and updated package.json to version", json.version); build(); }); }); }); // Build the package // @TODO use a proper package tool // @TODO add a .c9exclude file that excludes files var zipFilePath; function build(){ zipFilePath = join(os.tmpDir(), json.name + "@" + json.version); var tarArgs = ["-zcvf", zipFilePath, "."]; var c9ignore = process.env.HOME + "/.c9/.c9ignore"; fs.exists(c9ignore, function (exists) { if (exists) { tarArgs.push("--exclude-from=" + c9ignore); } proc.spawn(TAR, { args: tarArgs }, function(err, p){ if (err) return callback(err); if (verbose) { p.stdout.on("data", function(c){ process.stdout.write(c.toString("utf8")); }); p.stderr.on("data", function(c){ process.stderr.write(c.toString("utf8")); }); } p.on("exit", function(code){ if (code !== 0) return callback(new Error("ERROR: Could not package directory")); console.log("Built package", json.name + "@" + json.version); upload(); }); }); }); } // Update c9.io with the new version being published. function upload(){ // Check if the plugin is already registered if (verbose) console.log("Uploading package " + json.name); api.packages.get(json.name, function(err, pkg){ if (err) {} // Ignore error, if we don't get a response it means this package hasn't been published yet if (!pkg || pkg.error) { if (verbose) console.log("Package not registered, creating new."); // Registers the package name on c9.io if it is being published for the first time. api.user.get("", function(err, user){ if (err) return callback(new Error("ERROR: Failed to get user details from API - " + stringifyError(err))); api.packages.post("", { contentType: "application/json", body: { name: json.name, description: json.description, owner_type: "user", // @TODO implement this when adding orgs owner_id: parseInt(user.id), permissions: json.permissions || "world", categories: json.categories, repository: json.repository, longname: json.longname, website: json.website, screenshots: json.screenshots || [], pricing: json.pricing || {} } }, function(err, pkg){ if (err) return callback(new Error("ERROR: Failed to upload new package to API - " + stringifyError(err))); next(pkg); }); }); } else { if (verbose) console.log("Plugin already registered, updating."); api.packages.put(json.name, { contentType: "application/json", body: { permissions: json.permissions, categories: json.categories, repository: json.repository, longname: json.longname, website: json.website, description: json.description, screenshots: json.screenshots, pricing: json.pricing, enabled: true } }, function(err, pkg){ if (err) return callback(new Error("ERROR: Failed to update existing package - " + stringifyError(err))); if (verbose) console.log("Successfully updated existing package"); next(pkg); }); } function next(pkg){ // Create Version if (verbose) console.log("Sending new version ", json.version); var form = new FormData(); form.append('version', json.version); form.append('options', JSON.stringify(json.plugins)); form.append('package', fs.createReadStream(zipFilePath)); var path = "/packages/" + json.name + "/versions?access_token=" + encodeURIComponent(auth.accessToken); var host = APIHOST.split(":")[0] var port = parseInt(APIHOST.split(":")[1]) || null; var request = http.request({ agent: false, method: "post", host: host, port: port, path: path, auth: BASICAUTH, headers: form.getHeaders() }); form.pipe(request); request.on('response', function(res) { if (res.statusCode != 200) return callback(new Error("ERROR: Unknown Error:" + res.statusCode)); // Create Version Complete callback(null, json); }); } }); } }); } function unpublish(callback){ var packagePath = process.cwd() + "/package.json"; fs.readFile(packagePath, function(err, data){ if (err) return callback(err); // @TODO package.json not found var json; try { json = JSON.parse(data); } catch(e) { return callback(new Error("ERROR: Could not parse package.json: ", e.message)); } if (!json.name) return callback(new Error("ERROR: Missing name property in package.json")); api.packages.put(json.name + "/disable", {}, callback); }); } function install(packageName, options, callback){ // Call install url var parts = packageName.split("@"); var name = parts[0]; var version = parts[1]; var repository; if (!version || options.debug) { if (verbose) console.log("Retrieving package info"); api.packages.get(name, function (err, info) { if (err) return callback(err); if (verbose) console.log("Found:", info); version = info.latest; repository = info.repository; installPackage(); }); } else { installPackage(); } function prepareDirectory(callback){ // Create package dir var packagePath = process.env.HOME + "/.c9/plugins/" + name; var exists = fs.existsSync(packagePath) ; if (exists) { if (!force) return callback(new Error("WARNING: Directory not empty: " + packagePath + ". Use --force to overwrite.")); proc.execFile("rm", { args: ["-Rf", packagePath] }, function(){ mkdirP(packagePath); callback(null, packagePath); }); } else { mkdirP(packagePath); callback(null, packagePath); } } function installPackage(){ if (!version) return callback(new Error("No version found for this package")); if (options.local) { if (verbose) console.log("Installing package locally"); prepareDirectory(function(err, packagePath){ if (err) return callback(err); // Download package var gzPath = join(os.tmpDir(), name + "@" + version + ".tar.gz"); var file = fs.createWriteStream(gzPath); var path = "/packages/" + name + "/versions/" + version + "/download?access_token=" + encodeURIComponent(auth.accessToken); var host = APIHOST.split(":")[0]; var port = parseInt(APIHOST.split(":")[1]) || null; var request = http.get({ agent: false, method: "get", host: host, port: port, auth: BASICAUTH, path: path }, function(response){ response.pipe(file); }); if (verbose) console.log("Downloading package to", gzPath); request.on('response', function(res) { if (res.statusCode != 200) return callback(new Error("Unknown Error:" + res.statusCode)); if (verbose) console.log("Unpacking", gzPath, "to", packagePath); // Untargz package proc.spawn(TAR, { args: ["-C", packagePath, "-zxvf", gzPath] }, function(err, p){ if (err) return callback(err); if (verbose) { p.stdout.on("data", function(c){ process.stdout.write(c.toString("utf8")); }); p.stderr.on("data", function(c){ process.stderr.write(c.toString("utf8")); }); } p.on("exit", function(code){ var err = code !== 0 ? new Error("Failed to unpack package") : null; // Done callback(err, { version: version }); }); }); }); }); } else if (options.debug) { if (verbose) console.log("Installing debug version of package"); prepareDirectory(function(err, packagePath){ if (err) return callback(err); if (verbose) console.log("Cloning repository: ", repository); // Git clone repository var scm = SCM[repository.type]; proc.spawn(scm.binary, { args: [scm.clone, repository.url, packagePath] }, function(err, p){ if (err) return callback(err); if (verbose) { p.stdout.on("data", function(c){ process.stdout.write(c.toString("utf8")); }); p.stderr.on("data", function(c){ process.stderr.write(c.toString("utf8")); }); } p.on("exit", function(code){ var err = code !== 0 ? new Error("Failed to clone package from repository. Do you have access?") : null; // Done callback(err); }); }); }); } else { if (verbose) console.log("Notifying c9.io that packages needs to be installed"); var endpoint = options.global ? api.user : api.project; var url = "install/" + packageName + "/" + version; endpoint.post(url, function(err, info){ callback(err, info); }); } } } function uninstall(packageName, options, callback){ // Call uninstall url var parts = packageName.split("@"); var name = parts[0]; var version = parts[1]; if (!version) { api.packages.get(name, function (err, info) { if (err) return callback(err); version = info.latest; uninstallPackage(); }); } else { uninstallPackage(); } function uninstallPackage(){ if (options.local || options.debug) { // rm -Rf var packagePath = process.env.HOME + "/.c9/plugins/" + name; proc.spawn("rm", { args: ["-rf", packagePath] }, function(err, p){ if (err) return callback(err); if (verbose) { p.stdout.on("data", function(c){ process.stdout.write(c.toString("utf8")); }); p.stderr.on("data", function(c){ process.stderr.write(c.toString("utf8")); }); } p.on("exit", function(code){ var err = code !== 0 ? new Error("Failed to remove package.") : null; // if debug > see if should be installed and put back original // @TODO // Done callback(err); }); }); } else { var endpoint = options.global ? api.user : api.project; var url = "uninstall/" + packageName; endpoint.post(url, function(err, info){ callback(err, info); }); } } } function mkdirP(path){ var dirs = path.split('/'); var prevDir = dirs.splice(0,1) + "/"; while (dirs.length > 0) { var curDir = prevDir + dirs.splice(0,1); if (! fs.existsSync(curDir) ) { fs.mkdirSync(curDir); } prevDir = curDir + '/'; } } /***** Lifecycle *****/ plugin.on("load", function(){ load(); }); plugin.on("enable", function(){ }); plugin.on("disable", function(){ }); plugin.on("unload", function(){ loaded = false; verbose = false; force = false; }); /***** Register and define API *****/ /** * **/ plugin.freezePublicAPI({ /** * */ publish: publish, /** * */ unpublish: unpublish, /** * */ install: install, /** * */ uninstall: uninstall, /** * */ list: list }); register(null, { "cli.publish": plugin }); } });