1
0
mirror of https://gitcode.com/github-mirrors/react-native-update-cli.git synced 2025-09-16 01:41:37 +08:00
Code Issues Packages Projects Releases Wiki Activity GitHub Gitee

initial commit

This commit is contained in:
sunnylqm
2020-02-19 00:18:43 +08:00
parent db7ad264da
commit d46cb5b46a
22 changed files with 6759 additions and 0 deletions

14
.babelrc Normal file
View File

@@ -0,0 +1,14 @@
{
"plugins": [
"syntax-object-rest-spread",
"syntax-async-functions",
"transform-es2015-arrow-functions",
"transform-async-to-generator",
"transform-es2015-modules-commonjs",
"transform-es2015-destructuring",
"transform-es2015-spread",
"transform-object-rest-spread",
"transform-es2015-parameters",
"transform-strict-mode"
]
}

4
.prettierrc.js Normal file
View File

@@ -0,0 +1,4 @@
module.exports = {
trailingComma: 'all',
singleQuote: true,
};

162
cli.json Normal file
View File

@@ -0,0 +1,162 @@
{
"useCommand": true,
"defaultCommand": "help",
"commands": {
"help": {
},
"login":{
},
"logout": {
},
"me": {
},
"createApp": {
"options": {
"name": {
"hasValue": true
},
"platform": {
"hasValue": true
}
}
},
"apps": {
"options": {
"platform": {
"hasValue": true
}
}
},
"deleteApp": {
},
"selectApp": {
"options": {
"platform": {
"hasValue": true
}
}
},
"uploadIpa": {
},
"uploadApk": {
},
"packages": {
"options": {
"platform": {
"hasValue": true
}
}
},
"publish": {
"options": {
"platform": {
"hasValue": true
},
"name": {
"hasValue": true
},
"description": {
"hasValue": true
},
"metaInfo": {
"hasValue": true
}
}
},
"versions": {
"options": {
"platform": {
"hasValue": true
}
}
},
"update": {
"options": {
"platform": {
"hasValue": true
},
"versionId": {
"hasValue": true
},
"packageId": {
"hasValue": true
}
}
},
"build": {
"description": "Bundle javascript and copy assets."
},
"bundle": {
"description": "Bundle javascript code only.",
"options": {
"dev": {
"default": "false",
"hasValue": true
},
"platform": {
"hasValue": true
},
"bundleName":{
"default": "index.bundlejs",
"hasValue": true
},
"entryFile": {
"default": "index.js",
"hasValue": true
},
"intermediaDir": {
"default": "build/intermedia/${platform}",
"hasValue": true
},
"output": {
"default": "build/output/${platform}.${time}.ppk",
"hasValue": true
},
"verbose": {
}
}
},
"release": {
"description": "Push builded file to server."
},
"diff": {
"description": "Create diff patch",
"options": {
"output": {
"default": "build/output/diff",
"hasValue": true
}
}
},
"diffFromApk": {
"description": "Create diff patch from a Android package(.apk)",
"options": {
"output": {
"default": "build/output/diff-${time}.apk-patch",
"hasValue": true
}
}
},
"diffFromIpa": {
"description": "Create diff patch from a iOS package(.ipa)",
"options": {
"output": {
"default": "build/output/diff-${time}.ipa-patch",
"hasValue": true
}
}
}
},
"globalOptions":{
"no-interactive": {
"default": false
}
}
}

4
index.js Normal file
View File

@@ -0,0 +1,4 @@
/**
* Created by tdzl2003 on 2/22/16.
*/
module.exports = require('./lib');

158
lib/api.js Normal file
View File

@@ -0,0 +1,158 @@
'use strict';
let query = function () {
var _ref2 = _asyncToGenerator(function* (url, options) {
const resp = yield fetch(url, options);
const json = yield resp.json();
if (resp.status !== 200) {
throw Object.assign(new Error(json.message || json.error), { status: resp.status });
}
return json;
});
return function query(_x, _x2) {
return _ref2.apply(this, arguments);
};
}();
let uploadFile = function () {
var _ref3 = _asyncToGenerator(function* (fn) {
var _ref4 = yield exports.post('/upload', {});
const url = _ref4.url,
fieldName = _ref4.fieldName,
formData = _ref4.formData;
let realUrl = url;
if (!/^https?\:\/\//.test(url)) {
realUrl = host + url;
}
const fileSize = fs.statSync(fn).size;
const bar = new _progress2.default(' Uploading [:bar] :percent :etas', {
complete: '=',
incomplete: ' ',
total: fileSize
});
const info = yield new Promise(function (resolve, reject) {
formData.file = fs.createReadStream(fn);
formData.file.on('data', function (data) {
bar.tick(data.length);
});
_request2.default.post(realUrl, {
formData
}, function (err, resp, body) {
if (err) {
return reject(err);
}
if (resp.statusCode > 299) {
return reject(Object.assign(new Error(body), { status: resp.statusCode }));
}
resolve(JSON.parse(body));
});
});
return info;
});
return function uploadFile(_x3) {
return _ref3.apply(this, arguments);
};
}();
var _request = require('request');
var _request2 = _interopRequireDefault(_request);
var _progress = require('progress');
var _progress2 = _interopRequireDefault(_progress);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
/**
* Created by tdzl2003 on 2/13/16.
*/
const fetch = require('isomorphic-fetch');
let host = process.env.PUSHY_REGISTRY || 'https://update.reactnative.cn/api';
const fs = require('fs-extra');
let session = undefined;
let savedSession = undefined;
exports.loadSession = _asyncToGenerator(function* () {
if (fs.existsSync('.update')) {
try {
exports.replaceSession(JSON.parse(fs.readFileSync('.update', 'utf8')));
savedSession = session;
} catch (e) {
console.error('Failed to parse file `.update`. Try to remove it manually.');
throw e;
}
}
});
exports.getSession = function () {
return session;
};
exports.replaceSession = function (newSession) {
session = newSession;
};
exports.saveSession = function () {
// Only save on change.
if (session !== savedSession) {
const current = session;
const data = JSON.stringify(current, null, 4);
fs.writeFileSync('.update', data, 'utf8');
savedSession = current;
}
};
exports.closeSession = function () {
if (fs.existsSync('.update')) {
fs.unlinkSync('.update');
savedSession = undefined;
}
session = undefined;
host = process.env.PUSHY_REGISTRY || 'https://update.reactnative.cn';
};
function queryWithoutBody(method) {
return function (api) {
return query(host + api, {
method,
headers: {
'X-AccessToken': session ? session.token : ''
}
});
};
}
function queryWithBody(method) {
return function (api, body) {
return query(host + api, {
method,
headers: {
'Content-Type': 'application/json',
'X-AccessToken': session ? session.token : ''
},
body: JSON.stringify(body)
});
};
}
exports.get = queryWithoutBody('GET');
exports.post = queryWithBody('POST');
exports.put = queryWithBody('PUT');
exports.doDelete = queryWithBody('DELETE');
exports.uploadFile = uploadFile;

188
lib/app.js Normal file
View File

@@ -0,0 +1,188 @@
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.commands = exports.chooseApp = exports.listApp = undefined;
let listApp = exports.listApp = function () {
var _ref = _asyncToGenerator(function* (platform) {
var _ref2 = yield get('/app/list');
const data = _ref2.data;
const list = platform ? data.filter(function (v) {
return v.platform === platform;
}) : data;
for (const app of list) {
console.log(`${app.id}) ${app.name}(${app.platform})`);
}
if (platform) {
console.log(`\nTotal ${list.length} ${platform} apps`);
} else {
console.log(`\nTotal ${list.length} apps`);
}
return list;
});
return function listApp(_x) {
return _ref.apply(this, arguments);
};
}();
let chooseApp = exports.chooseApp = function () {
var _ref3 = _asyncToGenerator(function* (platform) {
const list = yield listApp(platform);
while (true) {
const id = yield (0, _utils.question)('Enter appId:');
const app = list.find(function (v) {
return v.id === (id | 0);
});
if (app) {
return app;
}
}
});
return function chooseApp(_x2) {
return _ref3.apply(this, arguments);
};
}();
exports.checkPlatform = checkPlatform;
exports.getSelectedApp = getSelectedApp;
var _utils = require('./utils');
var _fsExtra = require('fs-extra');
var fs = _interopRequireWildcard(_fsExtra);
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } /**
* Created by tdzl2003 on 2/13/16.
*/
var _require = require('./api');
const post = _require.post,
get = _require.get,
doDelete = _require.doDelete;
const validPlatforms = {
ios: 1,
android: 1
};
function checkPlatform(platform) {
if (!validPlatforms[platform]) {
throw new Error(`Invalid platform '${platform}'`);
}
return platform;
}
function getSelectedApp(platform) {
checkPlatform(platform);
if (!fs.existsSync('update.json')) {
throw new Error(`App not selected. run 'pushy selectApp --platform ${platform}' first!`);
}
const updateInfo = JSON.parse(fs.readFileSync('update.json', 'utf8'));
if (!updateInfo[platform]) {
throw new Error(`App not selected. run 'pushy selectApp --platform ${platform}' first!`);
}
return updateInfo[platform];
}
const commands = exports.commands = {
createApp: function () {
var _ref4 = _asyncToGenerator(function* (_ref5) {
let options = _ref5.options;
const name = options.name || (yield (0, _utils.question)('App Name:'));
const downloadUrl = options.downloadUrl;
const platform = checkPlatform(options.platform || (yield (0, _utils.question)('Platform(ios/android):')));
var _ref6 = yield post('/app/create', { name, platform });
const id = _ref6.id;
console.log(`Created app ${id}`);
yield this.selectApp({
args: [id],
options: { platform, downloadUrl }
});
});
return function createApp(_x3) {
return _ref4.apply(this, arguments);
};
}(),
deleteApp: function () {
var _ref7 = _asyncToGenerator(function* (_ref8) {
let args = _ref8.args,
options = _ref8.options;
const platform = options.platform;
const id = args[0] || chooseApp(platform);
if (!id) {
console.log('Canceled');
}
yield doDelete(`/app/${id}`);
console.log('Ok.');
});
return function deleteApp(_x4) {
return _ref7.apply(this, arguments);
};
}(),
apps: function () {
var _ref9 = _asyncToGenerator(function* (_ref10) {
let options = _ref10.options;
const platform = options.platform;
listApp(platform);
});
return function apps(_x5) {
return _ref9.apply(this, arguments);
};
}(),
selectApp: function () {
var _ref11 = _asyncToGenerator(function* (_ref12) {
let args = _ref12.args,
options = _ref12.options;
const platform = checkPlatform(options.platform || (yield (0, _utils.question)('Platform(ios/android):')));
const id = args[0] || (yield chooseApp(platform)).id;
let updateInfo = {};
if (fs.existsSync('update.json')) {
try {
updateInfo = JSON.parse(fs.readFileSync('update.json', 'utf8'));
} catch (e) {
console.error('Failed to parse file `update.json`. Try to remove it manually.');
throw e;
}
}
var _ref13 = yield get(`/app/${id}`);
const appKey = _ref13.appKey;
updateInfo[platform] = {
appId: id,
appKey
};
fs.writeFileSync('update.json', JSON.stringify(updateInfo, null, 4), 'utf8');
});
return function selectApp(_x6) {
return _ref11.apply(this, arguments);
};
}()
};

597
lib/bundle.js Normal file
View File

@@ -0,0 +1,597 @@
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.commands = undefined;
var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
let runReactNativeBundleCommand = function () {
var _ref = _asyncToGenerator(function* (bundleName, development, entryFile, outputFolder, platform, sourcemapOutput, config) {
let reactNativeBundleArgs = [];
let envArgs = process.env.PUSHY_ENV_ARGS;
if (envArgs) {
Array.prototype.push.apply(reactNativeBundleArgs, envArgs.trim().split(/\s+/));
}
fs.emptyDirSync(outputFolder);
Array.prototype.push.apply(reactNativeBundleArgs, [path.join("node_modules", "react-native", "local-cli", "cli.js"), "bundle", '--assets-dest', outputFolder, '--bundle-output', path.join(outputFolder, bundleName), '--dev', development, '--entry-file', entryFile, '--platform', platform, '--reset-cache']);
if (sourcemapOutput) {
reactNativeBundleArgs.push('--sourcemap-output', sourcemapOutput);
}
if (config) {
reactNativeBundleArgs.push('--config', config);
}
const reactNativeBundleProcess = spawn('node', reactNativeBundleArgs);
console.log(`Running bundle command: node ${reactNativeBundleArgs.join(' ')}`);
return new Promise(function (resolve, reject) {
reactNativeBundleProcess.stdout.on('data', function (data) {
console.log(data.toString().trim());
});
reactNativeBundleProcess.stderr.on('data', function (data) {
console.error(data.toString().trim());
});
reactNativeBundleProcess.on('close', function () {
var _ref2 = _asyncToGenerator(function* (exitCode) {
if (exitCode) {
reject(new Error(`"react-native bundle" command exited with code ${exitCode}.`));
} else {
if (platform === 'android') {
yield compileHermesByteCode(bundleName, outputFolder);
}
resolve(null);
}
});
return function (_x8) {
return _ref2.apply(this, arguments);
};
}());
});
});
return function runReactNativeBundleCommand(_x, _x2, _x3, _x4, _x5, _x6, _x7) {
return _ref.apply(this, arguments);
};
}();
let compileHermesByteCode = function () {
var _ref3 = _asyncToGenerator(function* (bundleName, outputFolder) {
let enableHermes = false;
try {
const gradleConfig = yield g2js.parseFile('android/app/build.gradle');
const projectConfig = gradleConfig['project.ext.react'];
for (const packagerConfig of projectConfig) {
if (packagerConfig.includes('enableHermes') && packagerConfig.includes('true')) {
enableHermes = true;
break;
}
}
} catch (e) {}
if (enableHermes) {
console.log(`Hermes enabled, now compiling to hermes bytecode:\n`);
const hermesPath = fs.existsSync('node_modules/hermes-engine') ? 'node_modules/hermes-engine' : 'node_modules/hermesvm';
execSync(`${hermesPath}/${getHermesOSBin()}/hermes -emit-binary -out ${outputFolder}/${bundleName} ${outputFolder}/${bundleName} -O`, { stdio: 'ignore' });
}
});
return function compileHermesByteCode(_x9, _x10) {
return _ref3.apply(this, arguments);
};
}();
let pack = function () {
var _ref4 = _asyncToGenerator(function* (dir, output) {
console.log('Packing');
fs.ensureDirSync(path.dirname(output));
yield new Promise(function (resolve, reject) {
var zipfile = new _yazl.ZipFile();
function addDirectory(root, rel) {
if (rel) {
zipfile.addEmptyDirectory(rel);
}
const childs = fs.readdirSync(root);
for (const name of childs) {
if (name === '.' || name === '..') {
continue;
}
const fullPath = path.join(root, name);
const stat = fs.statSync(fullPath);
if (stat.isFile()) {
//console.log('adding: ' + rel+name);
zipfile.addFile(fullPath, rel + name);
} else if (stat.isDirectory()) {
//console.log('adding: ' + rel+name+'/');
addDirectory(fullPath, rel + name + '/');
}
}
}
addDirectory(dir, '');
zipfile.outputStream.on('error', function (err) {
return reject(err);
});
zipfile.outputStream.pipe(fs.createWriteStream(output)).on('close', function () {
resolve();
});
zipfile.end();
});
console.log('Bundled saved to: ' + output);
});
return function pack(_x11, _x12) {
return _ref4.apply(this, arguments);
};
}();
let diffFromPPK = function () {
var _ref5 = _asyncToGenerator(function* (origin, next, output) {
fs.ensureDirSync(path.dirname(output));
const originEntries = {};
const originMap = {};
let originSource;
yield enumZipEntries(origin, function (entry, zipFile) {
originEntries[entry.fileName] = entry;
if (!/\/$/.test(entry.fileName)) {
// isFile
originMap[entry.crc32] = entry.fileName;
if (entry.fileName === 'index.bundlejs') {
// This is source.
return readEntire(entry, zipFile).then(function (v) {
return originSource = v;
});
}
}
});
originSource = originSource || new Buffer(0);
const copies = {};
var zipfile = new _yazl.ZipFile();
const writePromise = new Promise(function (resolve, reject) {
zipfile.outputStream.on('error', function (err) {
throw err;
});
zipfile.outputStream.pipe(fs.createWriteStream(output)).on('close', function () {
resolve();
});
});
const addedEntry = {};
function addEntry(fn) {
//console.log(fn);
if (!fn || addedEntry[fn]) {
return;
}
const base = basename(fn);
if (base) {
addEntry(base);
}
zipfile.addEmptyDirectory(fn);
}
const newEntries = {};
yield enumZipEntries(next, function (entry, nextZipfile) {
newEntries[entry.fileName] = entry;
if (/\/$/.test(entry.fileName)) {
// Directory
if (!originEntries[entry.fileName]) {
addEntry(entry.fileName);
}
} else if (entry.fileName === 'index.bundlejs') {
//console.log('Found bundle');
return readEntire(entry, nextZipfile).then(function (newSource) {
//console.log('Begin diff');
zipfile.addBuffer(diff(originSource, newSource), 'index.bundlejs.patch');
//console.log('End diff');
});
} else {
// If same file.
const originEntry = originEntries[entry.fileName];
if (originEntry && originEntry.crc32 === entry.crc32) {
// ignore
return;
}
// If moved from other place
if (originMap[entry.crc32]) {
const base = basename(entry.fileName);
if (!originEntries[base]) {
addEntry(base);
}
copies[entry.fileName] = originMap[entry.crc32];
return;
}
// New file.
addEntry(basename(entry.fileName));
return new Promise(function (resolve, reject) {
nextZipfile.openReadStream(entry, function (err, readStream) {
if (err) {
return reject(err);
}
zipfile.addReadStream(readStream, entry.fileName);
readStream.on('end', function () {
//console.log('add finished');
resolve();
});
});
});
}
});
const deletes = {};
for (var k in originEntries) {
if (!newEntries[k]) {
console.log('Delete ' + k);
deletes[k] = 1;
}
}
//console.log({copies, deletes});
zipfile.addBuffer(new Buffer(JSON.stringify({ copies, deletes })), '__diff.json');
zipfile.end();
yield writePromise;
});
return function diffFromPPK(_x13, _x14, _x15) {
return _ref5.apply(this, arguments);
};
}();
let diffFromPackage = function () {
var _ref6 = _asyncToGenerator(function* (origin, next, output, originBundleName) {
let transformPackagePath = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : function (v) {
return v;
};
fs.ensureDirSync(path.dirname(output));
const originEntries = {};
const originMap = {};
let originSource;
yield enumZipEntries(origin, function (entry, zipFile) {
if (!/\/$/.test(entry.fileName)) {
const fn = transformPackagePath(entry.fileName);
if (!fn) {
return;
}
//console.log(fn);
// isFile
originEntries[fn] = entry.crc32;
originMap[entry.crc32] = fn;
if (fn === originBundleName) {
// This is source.
return readEntire(entry, zipFile).then(function (v) {
return originSource = v;
});
}
}
});
originSource = originSource || new Buffer(0);
const copies = {};
var zipfile = new _yazl.ZipFile();
const writePromise = new Promise(function (resolve, reject) {
zipfile.outputStream.on('error', function (err) {
throw err;
});
zipfile.outputStream.pipe(fs.createWriteStream(output)).on('close', function () {
resolve();
});
});
yield enumZipEntries(next, function (entry, nextZipfile) {
if (/\/$/.test(entry.fileName)) {
// Directory
zipfile.addEmptyDirectory(entry.fileName);
} else if (entry.fileName === 'index.bundlejs') {
//console.log('Found bundle');
return readEntire(entry, nextZipfile).then(function (newSource) {
//console.log('Begin diff');
zipfile.addBuffer(diff(originSource, newSource), 'index.bundlejs.patch');
//console.log('End diff');
});
} else {
// If same file.
if (originEntries[entry.fileName] === entry.crc32) {
copies[entry.fileName] = '';
return;
}
// If moved from other place
if (originMap[entry.crc32]) {
copies[entry.fileName] = originMap[entry.crc32];
return;
}
return new Promise(function (resolve, reject) {
nextZipfile.openReadStream(entry, function (err, readStream) {
if (err) {
return reject(err);
}
zipfile.addReadStream(readStream, entry.fileName);
readStream.on('end', function () {
//console.log('add finished');
resolve();
});
});
});
}
});
zipfile.addBuffer(new Buffer(JSON.stringify({ copies })), '__diff.json');
zipfile.end();
yield writePromise;
});
return function diffFromPackage(_x16, _x17, _x18, _x19) {
return _ref6.apply(this, arguments);
};
}();
var _utils = require('./utils');
var _fsExtra = require('fs-extra');
var fs = _interopRequireWildcard(_fsExtra);
var _yazl = require('yazl');
var _yauzl = require('yauzl');
var _app = require('./app');
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
/**
* Created by tdzl2003 on 2/22/16.
*/
const path = require('path');
var _require = require('child_process');
const spawn = _require.spawn,
spawnSync = _require.spawnSync,
execSync = _require.execSync;
const g2js = require('gradle-to-js/lib/parser');
const os = require('os');
var diff;
try {
var bsdiff = require('node-bsdiff');
diff = typeof bsdiff != 'function' ? bsdiff.diff : bsdiff;
} catch (e) {
diff = function () {
console.warn('This function needs "node-bsdiff". Please run "npm i node-bsdiff" from your project directory first!');
throw new Error('This function needs module "node-bsdiff". Please install it first.');
};
}
function exec(command) {
const commandResult = spawnSync(command, {
shell: true,
stdio: 'inherit'
});
if (commandResult.error) {
throw commandResult.error;
}
}
function getHermesOSBin() {
if (os.platform() === 'win32') return 'win64-bin';
if (os.platform() === 'darwin') return 'osx-bin';
if (os.platform() === 'linux') return 'linux64-bin';
}
function readEntire(entry, zipFile) {
const buffers = [];
return new Promise(function (resolve, reject) {
zipFile.openReadStream(entry, function (err, stream) {
stream.pipe({
write(chunk) {
buffers.push(chunk);
},
end() {
resolve(Buffer.concat(buffers));
},
prependListener() {},
on() {},
once() {},
emit() {}
});
});
});
}
function basename(fn) {
const m = /^(.+\/)[^\/]+\/?$/.exec(fn);
return m && m[1];
}
function enumZipEntries(zipFn, callback) {
return new Promise(function (resolve, reject) {
(0, _yauzl.open)(zipFn, { lazyEntries: true }, function (err, zipfile) {
if (err) {
return reject(err);
}
zipfile.on('end', resolve);
zipfile.on('error', reject);
zipfile.on('entry', function (entry) {
const result = callback(entry, zipfile);
if (result && typeof result.then === 'function') {
result.then(function () {
return zipfile.readEntry();
});
} else {
zipfile.readEntry();
}
});
zipfile.readEntry();
});
});
}
const commands = exports.commands = {
bundle: function () {
var _ref7 = _asyncToGenerator(function* (_ref8) {
let options = _ref8.options;
const platform = (0, _app.checkPlatform)(options.platform || (yield (0, _utils.question)('Platform(ios/android):')));
var _translateOptions = (0, _utils.translateOptions)(_extends({}, options, {
platform
}));
let bundleName = _translateOptions.bundleName,
entryFile = _translateOptions.entryFile,
intermediaDir = _translateOptions.intermediaDir,
output = _translateOptions.output,
dev = _translateOptions.dev,
verbose = _translateOptions.verbose;
// const sourcemapOutput = path.join(intermediaDir, bundleName + ".map");
const realOutput = output.replace(/\$\{time\}/g, '' + Date.now());
if (!platform) {
throw new Error('Platform must be specified.');
}
var _getRNVersion = (0, _utils.getRNVersion)();
const version = _getRNVersion.version,
major = _getRNVersion.major,
minor = _getRNVersion.minor;
console.log('Bundling with React Native version: ', version);
yield runReactNativeBundleCommand(bundleName, dev, entryFile, intermediaDir, platform);
yield pack(path.resolve(intermediaDir), realOutput);
const v = yield (0, _utils.question)('Would you like to publish it?(Y/N)');
if (v.toLowerCase() === 'y') {
yield this.publish({
args: [realOutput],
options: {
platform
}
});
}
});
return function bundle(_x21) {
return _ref7.apply(this, arguments);
};
}(),
diff(_ref9) {
let args = _ref9.args,
options = _ref9.options;
return _asyncToGenerator(function* () {
var _args = _slicedToArray(args, 2);
const origin = _args[0],
next = _args[1];
const output = options.output;
const realOutput = output.replace(/\$\{time\}/g, '' + Date.now());
if (!origin || !next) {
console.error('pushy diff <origin> <next>');
process.exit(1);
}
yield diffFromPPK(origin, next, realOutput, 'index.bundlejs');
console.log(`${realOutput} generated.`);
})();
},
diffFromApk(_ref10) {
let args = _ref10.args,
options = _ref10.options;
return _asyncToGenerator(function* () {
var _args2 = _slicedToArray(args, 2);
const origin = _args2[0],
next = _args2[1];
const output = options.output;
const realOutput = output.replace(/\$\{time\}/g, '' + Date.now());
if (!origin || !next) {
console.error('pushy diffFromApk <origin> <next>');
process.exit(1);
}
yield diffFromPackage(origin, next, realOutput, 'assets/index.android.bundle');
console.log(`${realOutput} generated.`);
})();
},
diffFromIpa(_ref11) {
let args = _ref11.args,
options = _ref11.options;
return _asyncToGenerator(function* () {
var _args3 = _slicedToArray(args, 2);
const origin = _args3[0],
next = _args3[1];
const output = options.output;
const realOutput = output.replace(/\$\{time\}/g, '' + Date.now());
if (!origin || !next) {
console.error('pushy diffFromIpa <origin> <next>');
process.exit(1);
}
yield diffFromPackage(origin, next, realOutput, 'main.jsbundle', function (v) {
const m = /^Payload\/[^/]+\/(.+)$/.exec(v);
return m && m[1];
});
console.log(`${realOutput} generated.`);
})();
}
};

65
lib/index.js Executable file
View File

@@ -0,0 +1,65 @@
#!/usr/bin/env node
'use strict';
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
/**
* Created by tdzl2003 on 2/13/16.
*/
var _require = require('./api');
const loadSession = _require.loadSession;
const updateNotifier = require('update-notifier');
const pkg = require('../package.json');
updateNotifier({ pkg }).notify();
function printUsage(_ref) {
let args = _ref.args;
// const commandName = args[0];
// TODO: print usage of commandName, or print global usage.
console.log('Usage is under development now.');
console.log('Visit `https://github.com/reactnativecn/react-native-pushy` for early document.');
process.exit(1);
}
function printVersionCommand() {
if (process.argv.indexOf('-v') >= 0 || process.argv[2] === 'version') {
console.log('react-native-update-cli: ' + pkg.version);
try {
const PACKAGE_JSON_PATH = path.resolve(process.cwd(), 'node_modules', 'react-native-update', 'package.json');
console.log('react-native-update: ' + require(PACKAGE_JSON_PATH).version);
} catch (e) {
console.log('react-native-update: n/a - not inside a React Native project directory');
}
process.exit();
}
}
const commands = _extends({}, require('./user').commands, require('./bundle').commands, require('./app').commands, require('./package').commands, require('./versions').commands, {
help: printUsage
});
function run() {
printVersionCommand();
const argv = require('cli-arguments').parse(require('../cli.json'));
global.NO_INTERACTIVE = argv.options['no-interactive'];
loadSession().then(function () {
return commands[argv.command](argv);
}).catch(function (err) {
if (err.status === 401) {
console.log('Not loggined.\nRun `pushy login` at your project directory to login.');
return;
}
console.error(err.stack);
process.exit(-1);
});
};
run();

170
lib/package.js Normal file
View File

@@ -0,0 +1,170 @@
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.commands = exports.choosePackage = exports.listPackage = undefined;
let listPackage = exports.listPackage = function () {
var _ref = _asyncToGenerator(function* (appId) {
var _ref2 = yield get(`/app/${appId}/package/list?limit=1000`);
const data = _ref2.data;
const header = [{ value: 'Package Id' }, { value: 'Version' }];
const rows = [];
for (const pkg of data) {
const version = pkg.version;
let versionInfo = '';
if (version) {
versionInfo = ` - ${version.id} ${version.hash.slice(0, 8)} ${version.name}`;
} else {
versionInfo = ' (newest)';
}
rows.push([pkg.id, `${pkg.name}(${pkg.status})${versionInfo}`]);
}
console.log(Table(header, rows).render());
console.log(`\nTotal ${data.length} package(s).`);
return data;
});
return function listPackage(_x) {
return _ref.apply(this, arguments);
};
}();
let choosePackage = exports.choosePackage = function () {
var _ref3 = _asyncToGenerator(function* (appId) {
const list = yield listPackage(appId);
while (true) {
const id = yield (0, _utils.question)('Enter Package Id:');
const app = list.find(function (v) {
return v.id === (id | 0);
});
if (app) {
return app;
}
}
});
return function choosePackage(_x2) {
return _ref3.apply(this, arguments);
};
}();
var _utils = require('./utils');
var _app = require('./app');
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
/**
* Created by tdzl2003 on 4/2/16.
*/
var _require = require('./api');
const get = _require.get,
post = _require.post,
uploadFile = _require.uploadFile;
const Table = require('tty-table');
const commands = exports.commands = {
uploadIpa: function () {
var _ref4 = _asyncToGenerator(function* (_ref5) {
let args = _ref5.args;
const fn = args[0];
if (!fn) {
throw new Error('Usage: pushy uploadIpa <ipaFile>');
}
var _ref6 = yield (0, _utils.getIpaInfo)(fn);
const versionName = _ref6.versionName,
buildTime = _ref6.buildTime;
var _ref7 = yield (0, _app.getSelectedApp)('ios');
const appId = _ref7.appId;
var _ref8 = yield uploadFile(fn);
const hash = _ref8.hash;
var _ref9 = yield post(`/app/${appId}/package/create`, {
name: versionName,
hash,
buildTime
});
const id = _ref9.id;
console.log(`Ipa uploaded: ${id}`);
});
return function uploadIpa(_x3) {
return _ref4.apply(this, arguments);
};
}(),
uploadApk: function () {
var _ref10 = _asyncToGenerator(function* (_ref11) {
let args = _ref11.args;
const fn = args[0];
if (!fn) {
throw new Error('Usage: pushy uploadApk <apkFile>');
}
var _ref12 = yield (0, _utils.getApkInfo)(fn);
const versionName = _ref12.versionName,
buildTime = _ref12.buildTime;
var _ref13 = yield (0, _app.getSelectedApp)('android');
const appId = _ref13.appId;
var _ref14 = yield uploadFile(fn);
const hash = _ref14.hash;
var _ref15 = yield post(`/app/${appId}/package/create`, {
name: versionName,
hash,
buildTime
});
const id = _ref15.id;
console.log(`Apk uploaded: ${id}`);
});
return function uploadApk(_x4) {
return _ref10.apply(this, arguments);
};
}(),
packages: function () {
var _ref16 = _asyncToGenerator(function* (_ref17) {
let options = _ref17.options;
const platform = (0, _app.checkPlatform)(options.platform || (yield (0, _utils.question)('Platform(ios/android):')));
var _ref18 = yield (0, _app.getSelectedApp)(platform);
const appId = _ref18.appId;
yield listPackage(appId);
});
return function packages(_x5) {
return _ref16.apply(this, arguments);
};
}()
};

72
lib/user.js Normal file
View File

@@ -0,0 +1,72 @@
'use strict';
var _utils = require('./utils');
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } /**
* Created by tdzl2003 on 2/13/16.
*/
var _require = require('./api');
const post = _require.post,
get = _require.get,
replaceSession = _require.replaceSession,
saveSession = _require.saveSession,
closeSession = _require.closeSession;
const crypto = require('crypto');
function md5(str) {
return crypto.createHash('md5').update(str).digest('hex');
}
exports.commands = {
login: function () {
var _ref = _asyncToGenerator(function* (_ref2) {
let args = _ref2.args;
const email = args[0] || (yield (0, _utils.question)('email:'));
const pwd = args[1] || (yield (0, _utils.question)('password:', true));
var _ref3 = yield post('/user/login', {
email,
pwd: md5(pwd)
});
const token = _ref3.token,
info = _ref3.info;
replaceSession({ token });
yield saveSession();
console.log(`Welcome, ${info.name}.`);
});
return function login(_x) {
return _ref.apply(this, arguments);
};
}(),
logout: function () {
var _ref4 = _asyncToGenerator(function* () {
yield closeSession();
console.log('Logged out.');
});
return function logout() {
return _ref4.apply(this, arguments);
};
}(),
me: function () {
var _ref5 = _asyncToGenerator(function* () {
const me = yield get('/user/me');
for (const k in me) {
if (k !== 'ok') {
console.log(`${k}: ${me[k]}`);
}
}
});
return function me() {
return _ref5.apply(this, arguments);
};
}()
};

123
lib/utils/index.js Normal file
View File

@@ -0,0 +1,123 @@
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.getIpaInfo = exports.getApkInfo = undefined;
let getApkInfo = exports.getApkInfo = function () {
var _ref = _asyncToGenerator(function* (fn) {
const appInfoParser = new AppInfoParser(fn);
var _ref2 = yield appInfoParser.parse();
const versionName = _ref2.versionName,
application = _ref2.application;
let buildTime = 0;
if (Array.isArray(application.metaData)) {
for (const meta of application.metaData) {
if (meta.name === 'pushy_build_time') {
buildTime = meta.value[0];
}
}
}
if (buildTime == 0) {
throw new Error('Can not get build time for this app.');
}
return { versionName, buildTime };
});
return function getApkInfo(_x) {
return _ref.apply(this, arguments);
};
}();
let getIpaInfo = exports.getIpaInfo = function () {
var _ref3 = _asyncToGenerator(function* (fn) {
const appInfoParser = new AppInfoParser(fn);
var _ref4 = yield appInfoParser.parse();
const versionName = _ref4.CFBundleShortVersionString;
let buildTimeTxtBuffer = yield appInfoParser.parser.getEntry(/payload\/.+?\.app\/pushy_build_time.txt/);
if (!buildTimeTxtBuffer) {
// Not in root bundle when use `use_frameworks`
buildTimeTxtBuffer = yield appInfoParser.parser.getEntry(/payload\/.+?\.app\/frameworks\/react_native_update.framework\/pushy_build_time.txt/);
}
if (!buildTimeTxtBuffer) {
throw new Error('Can not get build time for this app.');
}
const buildTime = buildTimeTxtBuffer.toString().replace('\n', '');
return { versionName, buildTime };
});
return function getIpaInfo(_x2) {
return _ref3.apply(this, arguments);
};
}();
exports.question = question;
exports.translateOptions = translateOptions;
exports.getRNVersion = getRNVersion;
var _path = require('path');
var path = _interopRequireWildcard(_path);
var _fsExtra = require('fs-extra');
var fs = _interopRequireWildcard(_fsExtra);
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } /**
* Created by tdzl2003 on 2/13/16.
*/
const AppInfoParser = require('app-info-parser');
var read = require('read');
function question(query, password) {
if (NO_INTERACTIVE) {
return Promise.resolve('');
}
return new Promise(function (resolve, reject) {
return read({
prompt: query,
silent: password,
replace: password ? '*' : undefined
}, function (err, result) {
return err ? reject(err) : resolve(result);
});
});
}
function translateOptions(options) {
const ret = {};
for (let key in options) {
const v = options[key];
if (typeof v === 'string') {
ret[key] = v.replace(/\$\{(\w+)\}/g, function (v, n) {
return options[n] || process.env[n] || v;
});
} else {
ret[key] = v;
}
}
return ret;
}
function getRNVersion() {
const version = JSON.parse(fs.readFileSync(path.resolve('node_modules/react-native/package.json'))).version;
// We only care about major and minor version.
const match = /^(\d+)\.(\d+)\./.exec(version);
return {
version,
major: match[1] | 0,
minor: match[2] | 0
};
}

199
lib/versions.js Normal file
View File

@@ -0,0 +1,199 @@
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.commands = undefined;
let showVersion = function () {
var _ref = _asyncToGenerator(function* (appId, offset) {
var _ref2 = yield get(`/app/${appId}/version/list`);
const data = _ref2.data,
count = _ref2.count;
console.log(`Offset ${offset}`);
for (const version of data) {
let packageInfo = version.packages.slice(0, 3).map(function (v) {
return v.name;
}).join(', ');
const count = version.packages.length;
if (count > 3) {
packageInfo += `...and ${count - 3} more`;
}
if (count === 0) {
packageInfo = `(no package)`;
} else {
packageInfo = `[${packageInfo}]`;
}
console.log(`${version.id}) ${version.hash.slice(0, 8)} ${version.name} ${packageInfo}`);
}
return data;
});
return function showVersion(_x, _x2) {
return _ref.apply(this, arguments);
};
}();
let listVersions = function () {
var _ref3 = _asyncToGenerator(function* (appId) {
let offset = 0;
while (true) {
yield showVersion(appId, offset);
const cmd = yield (0, _utils.question)('page Up/page Down/Begin/Quit(U/D/B/Q)');
switch (cmd.toLowerCase()) {
case 'u':
offset = Math.max(0, offset - 10);break;
case 'd':
offset += 10;break;
case 'b':
offset = 0;break;
case 'q':
return;
}
}
});
return function listVersions(_x3) {
return _ref3.apply(this, arguments);
};
}();
let chooseVersion = function () {
var _ref4 = _asyncToGenerator(function* (appId) {
let offset = 0;
while (true) {
const data = yield showVersion(appId, offset);
const cmd = yield (0, _utils.question)('Enter versionId or page Up/page Down/Begin(U/D/B)');
switch (cmd.toLowerCase()) {
case 'U':
offset = Math.max(0, offset - 10);break;
case 'D':
offset += 10;break;
case 'B':
offset = 0;break;
default:
{
const v = data.find(function (v) {
return v.id === (cmd | 0);
});
if (v) {
return v;
}
}
}
}
});
return function chooseVersion(_x4) {
return _ref4.apply(this, arguments);
};
}();
var _utils = require('./utils');
var _app = require('./app');
var _package = require('./package');
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
/**
* Created by tdzl2003 on 4/2/16.
*/
var _require = require('./api');
const get = _require.get,
post = _require.post,
put = _require.put,
uploadFile = _require.uploadFile;
const commands = exports.commands = {
publish: function () {
var _ref5 = _asyncToGenerator(function* (_ref6) {
let args = _ref6.args,
options = _ref6.options;
const fn = args[0];
const name = options.name,
description = options.description,
metaInfo = options.metaInfo;
if (!fn || !fn.endsWith('.ppk')) {
throw new Error('Usage: pushy publish <ppkFile> --platform ios|android');
}
const platform = (0, _app.checkPlatform)(options.platform || (yield (0, _utils.question)('Platform(ios/android):')));
var _ref7 = yield (0, _app.getSelectedApp)(platform);
const appId = _ref7.appId;
var _ref8 = yield uploadFile(fn);
const hash = _ref8.hash;
var _ref9 = yield post(`/app/${appId}/version/create`, {
name: name || (yield (0, _utils.question)('Enter version name:')) || '(未命名)',
hash,
description: description || (yield (0, _utils.question)('Enter description:')),
metaInfo: metaInfo || (yield (0, _utils.question)('Enter meta info:'))
});
const id = _ref9.id;
console.log(`Version published: ${id}`);
const v = yield (0, _utils.question)('Would you like to bind packages to this version?(Y/N)');
if (v.toLowerCase() === 'y') {
yield this.update({ args: [], options: { versionId: id, platform } });
}
});
return function publish(_x5) {
return _ref5.apply(this, arguments);
};
}(),
versions: function () {
var _ref10 = _asyncToGenerator(function* (_ref11) {
let options = _ref11.options;
const platform = (0, _app.checkPlatform)(options.platform || (yield (0, _utils.question)('Platform(ios/android):')));
var _ref12 = yield (0, _app.getSelectedApp)(platform);
const appId = _ref12.appId;
yield listVersions(appId);
});
return function versions(_x6) {
return _ref10.apply(this, arguments);
};
}(),
update: function () {
var _ref13 = _asyncToGenerator(function* (_ref14) {
let args = _ref14.args,
options = _ref14.options;
const platform = (0, _app.checkPlatform)(options.platform || (yield (0, _utils.question)('Platform(ios/android):')));
var _ref15 = yield (0, _app.getSelectedApp)(platform);
const appId = _ref15.appId;
const versionId = options.versionId || (yield chooseVersion(appId)).id;
const pkgId = options.packageId || (yield (0, _package.choosePackage)(appId)).id;
yield put(`/app/${appId}/package/${pkgId}`, {
versionId
});
console.log('Ok.');
});
return function update(_x7) {
return _ref13.apply(this, arguments);
};
}()
};

57
package.json Normal file
View File

@@ -0,0 +1,57 @@
{
"name": "react-native-update-cli",
"version": "1.0.0",
"description": "Command tools for javaScript updater with `pushy` service for react native apps.",
"main": "index.js",
"bin": {
"pushy": "lib/index.js"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"prepublish": "babel src --out-dir lib"
},
"repository": {
"type": "git",
"url": "git+https://github.com/reactnativecn/react-native-pushy-cli.git"
},
"keywords": [
"react-native",
"ios",
"android",
"update"
],
"author": "reactnativecn",
"license": "BSD-3-Clause",
"bugs": {
"url": "https://github.com/reactnativecn/react-native-pushy/issues"
},
"homepage": "https://github.com/reactnativecn/react-native-pushy/tree/master/react-native-pushy-cli",
"dependencies": {
"app-info-parser": "^0.3.8",
"cli-arguments": "^0.2.1",
"fs-extra": "^8.1.0",
"gradle-to-js": "^2.0.0",
"isomorphic-fetch": "^2.2.1",
"progress": "^1.1.8",
"read": "^1.0.7",
"request": "^2.69.0",
"tty-table": "^2.7.0",
"update-notifier": "^4.1.0",
"yauzl": "^2.10.0",
"yazl": "2.3.0"
},
"devDependencies": {
"babel-cli": "^6.5.1",
"babel-eslint": "^4.1.6",
"babel-plugin-syntax-async-functions": "^6.5.0",
"babel-plugin-syntax-object-rest-spread": "^6.5.0",
"babel-plugin-transform-async-to-generator": "^6.3.13",
"babel-plugin-transform-es2015-arrow-functions": "^6.5.2",
"babel-plugin-transform-es2015-destructuring": "^6.3.15",
"babel-plugin-transform-es2015-modules-commonjs": "^6.3.16",
"babel-plugin-transform-es2015-parameters": "^6.5.0",
"babel-plugin-transform-es2015-spread": "^6.5.2",
"babel-plugin-transform-object-rest-spread": "^6.5.0",
"babel-plugin-transform-strict-mode": "^6.5.2"
}
}

132
src/api.js Normal file
View File

@@ -0,0 +1,132 @@
/**
* Created by tdzl2003 on 2/13/16.
*/
const fetch = require('isomorphic-fetch');
let host = process.env.PUSHY_REGISTRY || 'https://update.reactnative.cn/api';
const fs = require('fs-extra');
import request from 'request';
import ProgressBar from 'progress';
let session = undefined;
let savedSession = undefined;
exports.loadSession = async function() {
if (fs.existsSync('.update')) {
try {
exports.replaceSession(JSON.parse(fs.readFileSync('.update', 'utf8')));
savedSession = session;
} catch (e) {
console.error('Failed to parse file `.update`. Try to remove it manually.');
throw e;
}
}
};
exports.getSession = function() {
return session;
};
exports.replaceSession = function(newSession) {
session = newSession;
};
exports.saveSession = function() {
// Only save on change.
if (session !== savedSession) {
const current = session;
const data = JSON.stringify(current, null, 4);
fs.writeFileSync('.update', data, 'utf8');
savedSession = current;
}
};
exports.closeSession = function() {
if (fs.existsSync('.update')) {
fs.unlinkSync('.update');
savedSession = undefined;
}
session = undefined;
host = process.env.PUSHY_REGISTRY || 'https://update.reactnative.cn';
};
async function query(url, options) {
const resp = await fetch(url, options);
const json = await resp.json();
if (resp.status !== 200) {
throw Object.assign(new Error(json.message || json.error), { status: resp.status });
}
return json;
}
function queryWithoutBody(method) {
return function(api) {
return query(host + api, {
method,
headers: {
'X-AccessToken': session ? session.token : '',
},
});
};
}
function queryWithBody(method) {
return function(api, body) {
return query(host + api, {
method,
headers: {
'Content-Type': 'application/json',
'X-AccessToken': session ? session.token : '',
},
body: JSON.stringify(body),
});
};
}
exports.get = queryWithoutBody('GET');
exports.post = queryWithBody('POST');
exports.put = queryWithBody('PUT');
exports.doDelete = queryWithBody('DELETE');
async function uploadFile(fn) {
const { url, fieldName, formData } = await exports.post('/upload', {});
let realUrl = url;
if (!/^https?\:\/\//.test(url)) {
realUrl = host + url;
}
const fileSize = fs.statSync(fn).size;
const bar = new ProgressBar(' Uploading [:bar] :percent :etas', {
complete: '=',
incomplete: ' ',
total: fileSize,
});
const info = await new Promise((resolve, reject) => {
formData.file = fs.createReadStream(fn);
formData.file.on('data', function(data) {
bar.tick(data.length);
});
request.post(
realUrl,
{
formData,
},
(err, resp, body) => {
if (err) {
return reject(err);
}
if (resp.statusCode > 299) {
return reject(Object.assign(new Error(body), { status: resp.statusCode }));
}
resolve(JSON.parse(body));
},
);
});
return info;
}
exports.uploadFile = uploadFile;

110
src/app.js Normal file
View File

@@ -0,0 +1,110 @@
/**
* Created by tdzl2003 on 2/13/16.
*/
import {question} from './utils';
import * as fs from 'fs-extra';
const {
post,
get,
doDelete,
} = require('./api');
const validPlatforms = {
ios: 1,
android: 1,
};
export function checkPlatform(platform) {
if (!validPlatforms[platform]) {
throw new Error(`Invalid platform '${platform}'`);
}
return platform
}
export function getSelectedApp(platform) {
checkPlatform(platform);
if (!fs.existsSync('update.json')){
throw new Error(`App not selected. run 'pushy selectApp --platform ${platform}' first!`);
}
const updateInfo = JSON.parse(fs.readFileSync('update.json', 'utf8'));
if (!updateInfo[platform]) {
throw new Error(`App not selected. run 'pushy selectApp --platform ${platform}' first!`);
}
return updateInfo[platform];
}
export async function listApp(platform){
const {data} = await get('/app/list');
const list = platform?data.filter(v=>v.platform===platform):data;
for (const app of list) {
console.log(`${app.id}) ${app.name}(${app.platform})`);
}
if (platform) {
console.log(`\nTotal ${list.length} ${platform} apps`);
} else {
console.log(`\nTotal ${list.length} apps`);
}
return list;
}
export async function chooseApp(platform) {
const list = await listApp(platform);
while (true) {
const id = await question('Enter appId:');
const app = list.find(v=>v.id === (id|0));
if (app) {
return app;
}
}
}
export const commands = {
createApp: async function ({options}) {
const name = options.name || await question('App Name:');
const {downloadUrl} = options;
const platform = checkPlatform(options.platform || await question('Platform(ios/android):'));
const {id} = await post('/app/create', {name, platform});
console.log(`Created app ${id}`);
await this.selectApp({
args: [id],
options: {platform, downloadUrl},
});
},
deleteApp: async function ({args, options}) {
const {platform} = options;
const id = args[0] || chooseApp(platform);
if (!id) {
console.log('Canceled');
}
await doDelete(`/app/${id}`);
console.log('Ok.');
},
apps: async function ({options}){
const {platform} = options;
listApp(platform);
},
selectApp: async function({args, options}) {
const platform = checkPlatform(options.platform || await question('Platform(ios/android):'));
const id = args[0] || (await chooseApp(platform)).id;
let updateInfo = {};
if (fs.existsSync('update.json')) {
try {
updateInfo = JSON.parse(fs.readFileSync('update.json', 'utf8'));
} catch (e) {
console.error('Failed to parse file `update.json`. Try to remove it manually.');
throw e;
}
}
const {appKey} = await get(`/app/${id}`);
updateInfo[platform] = {
appId: id,
appKey,
};
fs.writeFileSync('update.json', JSON.stringify(updateInfo, null, 4), 'utf8');
},
}

508
src/bundle.js Normal file
View File

@@ -0,0 +1,508 @@
/**
* Created by tdzl2003 on 2/22/16.
*/
const path = require('path');
import { getRNVersion, translateOptions } from './utils';
import * as fs from 'fs-extra';
import { ZipFile } from 'yazl';
import { open as openZipFile } from 'yauzl';
import { question } from './utils';
import { checkPlatform } from './app';
const { spawn, spawnSync, execSync } = require('child_process');
const g2js = require('gradle-to-js/lib/parser');
const os = require('os');
var diff;
try {
var bsdiff = require('node-bsdiff');
diff = typeof bsdiff != 'function' ? bsdiff.diff : bsdiff;
} catch (e) {
diff = function() {
console.warn(
'This function needs "node-bsdiff". Please run "npm i node-bsdiff" from your project directory first!',
);
throw new Error('This function needs module "node-bsdiff". Please install it first.');
};
}
function exec(command) {
const commandResult = spawnSync(command, {
shell: true,
stdio: 'inherit',
});
if (commandResult.error) {
throw commandResult.error;
}
}
async function runReactNativeBundleCommand(
bundleName,
development,
entryFile,
outputFolder,
platform,
sourcemapOutput,
config,
) {
let reactNativeBundleArgs = [];
let envArgs = process.env.PUSHY_ENV_ARGS;
if (envArgs) {
Array.prototype.push.apply(reactNativeBundleArgs, envArgs.trim().split(/\s+/));
}
fs.emptyDirSync(outputFolder);
Array.prototype.push.apply(reactNativeBundleArgs, [
path.join("node_modules", "react-native", "local-cli", "cli.js"),
"bundle",
'--assets-dest',
outputFolder,
'--bundle-output',
path.join(outputFolder, bundleName),
'--dev',
development,
'--entry-file',
entryFile,
'--platform',
platform,
'--reset-cache',
]);
if (sourcemapOutput) {
reactNativeBundleArgs.push('--sourcemap-output', sourcemapOutput);
}
if (config) {
reactNativeBundleArgs.push('--config', config);
}
const reactNativeBundleProcess = spawn('node', reactNativeBundleArgs);
console.log(`Running bundle command: node ${reactNativeBundleArgs.join(' ')}`);
return new Promise((resolve, reject) => {
reactNativeBundleProcess.stdout.on('data', data => {
console.log(data.toString().trim());
});
reactNativeBundleProcess.stderr.on('data', data => {
console.error(data.toString().trim());
});
reactNativeBundleProcess.on('close', async exitCode => {
if (exitCode) {
reject(new Error(`"react-native bundle" command exited with code ${exitCode}.`));
} else {
if (platform === 'android') {
await compileHermesByteCode(bundleName, outputFolder);
}
resolve(null);
}
});
});
}
function getHermesOSBin() {
if (os.platform() === 'win32') return 'win64-bin';
if (os.platform() === 'darwin') return 'osx-bin';
if (os.platform() === 'linux') return 'linux64-bin';
}
async function compileHermesByteCode(bundleName, outputFolder) {
let enableHermes = false;
try {
const gradleConfig = await g2js.parseFile('android/app/build.gradle');
const projectConfig = gradleConfig['project.ext.react'];
for (const packagerConfig of projectConfig) {
if (packagerConfig.includes('enableHermes') && packagerConfig.includes('true')) {
enableHermes = true;
break;
}
}
} catch (e) {}
if (enableHermes) {
console.log(`Hermes enabled, now compiling to hermes bytecode:\n`);
const hermesPath = fs.existsSync('node_modules/hermes-engine')
? 'node_modules/hermes-engine'
: 'node_modules/hermesvm';
execSync(
`${hermesPath}/${getHermesOSBin()}/hermes -emit-binary -out ${outputFolder}/${bundleName} ${outputFolder}/${bundleName} -O`,
{ stdio: 'ignore' },
);
}
}
async function pack(dir, output) {
console.log('Packing');
fs.ensureDirSync(path.dirname(output));
await new Promise((resolve, reject) => {
var zipfile = new ZipFile();
function addDirectory(root, rel) {
if (rel) {
zipfile.addEmptyDirectory(rel);
}
const childs = fs.readdirSync(root);
for (const name of childs) {
if (name === '.' || name === '..') {
continue;
}
const fullPath = path.join(root, name);
const stat = fs.statSync(fullPath);
if (stat.isFile()) {
//console.log('adding: ' + rel+name);
zipfile.addFile(fullPath, rel + name);
} else if (stat.isDirectory()) {
//console.log('adding: ' + rel+name+'/');
addDirectory(fullPath, rel + name + '/');
}
}
}
addDirectory(dir, '');
zipfile.outputStream.on('error', err => reject(err));
zipfile.outputStream.pipe(fs.createWriteStream(output)).on('close', function() {
resolve();
});
zipfile.end();
});
console.log('Bundled saved to: ' + output);
}
function readEntire(entry, zipFile) {
const buffers = [];
return new Promise((resolve, reject) => {
zipFile.openReadStream(entry, (err, stream) => {
stream.pipe({
write(chunk) {
buffers.push(chunk);
},
end() {
resolve(Buffer.concat(buffers));
},
prependListener() {},
on() {},
once() {},
emit() {},
});
});
});
}
function basename(fn) {
const m = /^(.+\/)[^\/]+\/?$/.exec(fn);
return m && m[1];
}
async function diffFromPPK(origin, next, output) {
fs.ensureDirSync(path.dirname(output));
const originEntries = {};
const originMap = {};
let originSource;
await enumZipEntries(origin, (entry, zipFile) => {
originEntries[entry.fileName] = entry;
if (!/\/$/.test(entry.fileName)) {
// isFile
originMap[entry.crc32] = entry.fileName;
if (entry.fileName === 'index.bundlejs') {
// This is source.
return readEntire(entry, zipFile).then(v => (originSource = v));
}
}
});
originSource = originSource || new Buffer(0);
const copies = {};
var zipfile = new ZipFile();
const writePromise = new Promise((resolve, reject) => {
zipfile.outputStream.on('error', err => {
throw err;
});
zipfile.outputStream.pipe(fs.createWriteStream(output)).on('close', function() {
resolve();
});
});
const addedEntry = {};
function addEntry(fn) {
//console.log(fn);
if (!fn || addedEntry[fn]) {
return;
}
const base = basename(fn);
if (base) {
addEntry(base);
}
zipfile.addEmptyDirectory(fn);
}
const newEntries = {};
await enumZipEntries(next, (entry, nextZipfile) => {
newEntries[entry.fileName] = entry;
if (/\/$/.test(entry.fileName)) {
// Directory
if (!originEntries[entry.fileName]) {
addEntry(entry.fileName);
}
} else if (entry.fileName === 'index.bundlejs') {
//console.log('Found bundle');
return readEntire(entry, nextZipfile).then(newSource => {
//console.log('Begin diff');
zipfile.addBuffer(diff(originSource, newSource), 'index.bundlejs.patch');
//console.log('End diff');
});
} else {
// If same file.
const originEntry = originEntries[entry.fileName];
if (originEntry && originEntry.crc32 === entry.crc32) {
// ignore
return;
}
// If moved from other place
if (originMap[entry.crc32]) {
const base = basename(entry.fileName);
if (!originEntries[base]) {
addEntry(base);
}
copies[entry.fileName] = originMap[entry.crc32];
return;
}
// New file.
addEntry(basename(entry.fileName));
return new Promise((resolve, reject) => {
nextZipfile.openReadStream(entry, function(err, readStream) {
if (err) {
return reject(err);
}
zipfile.addReadStream(readStream, entry.fileName);
readStream.on('end', () => {
//console.log('add finished');
resolve();
});
});
});
}
});
const deletes = {};
for (var k in originEntries) {
if (!newEntries[k]) {
console.log('Delete ' + k);
deletes[k] = 1;
}
}
//console.log({copies, deletes});
zipfile.addBuffer(new Buffer(JSON.stringify({ copies, deletes })), '__diff.json');
zipfile.end();
await writePromise;
}
async function diffFromPackage(origin, next, output, originBundleName, transformPackagePath = v => v) {
fs.ensureDirSync(path.dirname(output));
const originEntries = {};
const originMap = {};
let originSource;
await enumZipEntries(origin, (entry, zipFile) => {
if (!/\/$/.test(entry.fileName)) {
const fn = transformPackagePath(entry.fileName);
if (!fn) {
return;
}
//console.log(fn);
// isFile
originEntries[fn] = entry.crc32;
originMap[entry.crc32] = fn;
if (fn === originBundleName) {
// This is source.
return readEntire(entry, zipFile).then(v => (originSource = v));
}
}
});
originSource = originSource || new Buffer(0);
const copies = {};
var zipfile = new ZipFile();
const writePromise = new Promise((resolve, reject) => {
zipfile.outputStream.on('error', err => {
throw err;
});
zipfile.outputStream.pipe(fs.createWriteStream(output)).on('close', function() {
resolve();
});
});
await enumZipEntries(next, (entry, nextZipfile) => {
if (/\/$/.test(entry.fileName)) {
// Directory
zipfile.addEmptyDirectory(entry.fileName);
} else if (entry.fileName === 'index.bundlejs') {
//console.log('Found bundle');
return readEntire(entry, nextZipfile).then(newSource => {
//console.log('Begin diff');
zipfile.addBuffer(diff(originSource, newSource), 'index.bundlejs.patch');
//console.log('End diff');
});
} else {
// If same file.
if (originEntries[entry.fileName] === entry.crc32) {
copies[entry.fileName] = '';
return;
}
// If moved from other place
if (originMap[entry.crc32]) {
copies[entry.fileName] = originMap[entry.crc32];
return;
}
return new Promise((resolve, reject) => {
nextZipfile.openReadStream(entry, function(err, readStream) {
if (err) {
return reject(err);
}
zipfile.addReadStream(readStream, entry.fileName);
readStream.on('end', () => {
//console.log('add finished');
resolve();
});
});
});
}
});
zipfile.addBuffer(new Buffer(JSON.stringify({ copies })), '__diff.json');
zipfile.end();
await writePromise;
}
function enumZipEntries(zipFn, callback) {
return new Promise((resolve, reject) => {
openZipFile(zipFn, { lazyEntries: true }, (err, zipfile) => {
if (err) {
return reject(err);
}
zipfile.on('end', resolve);
zipfile.on('error', reject);
zipfile.on('entry', entry => {
const result = callback(entry, zipfile);
if (result && typeof result.then === 'function') {
result.then(() => zipfile.readEntry());
} else {
zipfile.readEntry();
}
});
zipfile.readEntry();
});
});
}
export const commands = {
bundle: async function({ options }) {
const platform = checkPlatform(options.platform || (await question('Platform(ios/android):')));
let { bundleName, entryFile, intermediaDir, output, dev, verbose } = translateOptions({
...options,
platform,
});
// const sourcemapOutput = path.join(intermediaDir, bundleName + ".map");
const realOutput = output.replace(/\$\{time\}/g, '' + Date.now());
if (!platform) {
throw new Error('Platform must be specified.');
}
const { version, major, minor } = getRNVersion();
console.log('Bundling with React Native version: ', version);
await runReactNativeBundleCommand(bundleName, dev, entryFile, intermediaDir, platform);
await pack(path.resolve(intermediaDir), realOutput);
const v = await question('Would you like to publish it?(Y/N)');
if (v.toLowerCase() === 'y') {
await this.publish({
args: [realOutput],
options: {
platform,
},
});
}
},
async diff({ args, options }) {
const [origin, next] = args;
const { output } = options;
const realOutput = output.replace(/\$\{time\}/g, '' + Date.now());
if (!origin || !next) {
console.error('pushy diff <origin> <next>');
process.exit(1);
}
await diffFromPPK(origin, next, realOutput, 'index.bundlejs');
console.log(`${realOutput} generated.`);
},
async diffFromApk({ args, options }) {
const [origin, next] = args;
const { output } = options;
const realOutput = output.replace(/\$\{time\}/g, '' + Date.now());
if (!origin || !next) {
console.error('pushy diffFromApk <origin> <next>');
process.exit(1);
}
await diffFromPackage(origin, next, realOutput, 'assets/index.android.bundle');
console.log(`${realOutput} generated.`);
},
async diffFromIpa({ args, options }) {
const [origin, next] = args;
const { output } = options;
const realOutput = output.replace(/\$\{time\}/g, '' + Date.now());
if (!origin || !next) {
console.error('pushy diffFromIpa <origin> <next>');
process.exit(1);
}
await diffFromPackage(origin, next, realOutput, 'main.jsbundle', v => {
const m = /^Payload\/[^/]+\/(.+)$/.exec(v);
return m && m[1];
});
console.log(`${realOutput} generated.`);
},
};

67
src/index.js Normal file
View File

@@ -0,0 +1,67 @@
#!/usr/bin/env node
/**
* Created by tdzl2003 on 2/13/16.
*/
const {loadSession} = require('./api');
const updateNotifier = require('update-notifier');
const pkg = require('../package.json');
updateNotifier({pkg}).notify();
function printUsage({args}) {
// const commandName = args[0];
// TODO: print usage of commandName, or print global usage.
console.log('Usage is under development now.')
console.log('Visit `https://github.com/reactnativecn/react-native-pushy` for early document.');
process.exit(1);
}
function printVersionCommand() {
if (process.argv.indexOf('-v') >= 0 || process.argv[2] === 'version') {
console.log('react-native-update-cli: ' + pkg.version);
try {
const PACKAGE_JSON_PATH = path.resolve(
process.cwd(),
'node_modules',
'react-native-update',
'package.json'
);
console.log('react-native-update: ' + require(PACKAGE_JSON_PATH).version);
} catch (e) {
console.log('react-native-update: n/a - not inside a React Native project directory')
}
process.exit();
}
}
const commands = {
...require('./user').commands,
...require('./bundle').commands,
...require('./app').commands,
...require('./package').commands,
...require('./versions').commands,
help: printUsage,
};
function run() {
printVersionCommand();
const argv = require('cli-arguments').parse(require('../cli.json'));
global.NO_INTERACTIVE = argv.options['no-interactive'];
loadSession()
.then(()=>commands[argv.command](argv))
.catch(err=>{
if (err.status === 401) {
console.log('Not loggined.\nRun `pushy login` at your project directory to login.');
return;
}
console.error(err.stack);
process.exit(-1);
});
};
run();

87
src/package.js Normal file
View File

@@ -0,0 +1,87 @@
/**
* Created by tdzl2003 on 4/2/16.
*/
const { get, post, uploadFile } = require('./api');
import { question } from './utils';
import { checkPlatform, getSelectedApp } from './app';
import { getApkInfo, getIpaInfo } from './utils';
const Table = require('tty-table');
export async function listPackage(appId) {
const { data } = await get(`/app/${appId}/package/list?limit=1000`);
const header = [{ value: 'Package Id' }, { value: 'Version' }];
const rows = [];
for (const pkg of data) {
const { version } = pkg;
let versionInfo = '';
if (version) {
versionInfo = ` - ${version.id} ${version.hash.slice(0, 8)} ${version.name}`;
} else {
versionInfo = ' (newest)';
}
rows.push([pkg.id, `${pkg.name}(${pkg.status})${versionInfo}`]);
}
console.log(Table(header, rows).render());
console.log(`\nTotal ${data.length} package(s).`);
return data;
}
export async function choosePackage(appId) {
const list = await listPackage(appId);
while (true) {
const id = await question('Enter Package Id:');
const app = list.find(v => v.id === (id | 0));
if (app) {
return app;
}
}
}
export const commands = {
uploadIpa: async function({ args }) {
const fn = args[0];
if (!fn) {
throw new Error('Usage: pushy uploadIpa <ipaFile>');
}
const { versionName, buildTime } = await getIpaInfo(fn);
const { appId } = await getSelectedApp('ios');
const { hash } = await uploadFile(fn);
const { id } = await post(`/app/${appId}/package/create`, {
name: versionName,
hash,
buildTime,
});
console.log(`Ipa uploaded: ${id}`);
},
uploadApk: async function({ args }) {
const fn = args[0];
if (!fn) {
throw new Error('Usage: pushy uploadApk <apkFile>');
}
const { versionName, buildTime } = await getApkInfo(fn);
const { appId } = await getSelectedApp('android');
const { hash } = await uploadFile(fn);
const { id } = await post(`/app/${appId}/package/create`, {
name: versionName,
hash,
buildTime,
});
console.log(`Apk uploaded: ${id}`);
},
packages: async function({ options }) {
const platform = checkPlatform(options.platform || (await question('Platform(ios/android):')));
const { appId } = await getSelectedApp(platform);
await listPackage(appId);
},
};

43
src/user.js Normal file
View File

@@ -0,0 +1,43 @@
/**
* Created by tdzl2003 on 2/13/16.
*/
import {question} from './utils';
const {
post,
get,
replaceSession,
saveSession,
closeSession,
} = require('./api');
const crypto = require('crypto');
function md5(str) {
return crypto.createHash('md5').update(str).digest('hex');
}
exports.commands = {
login: async function ({args}){
const email = args[0] || await question('email:');
const pwd = args[1] || await question('password:', true);
const {token, info} = await post('/user/login', {
email,
pwd: md5(pwd),
});
replaceSession({token});
await saveSession();
console.log(`Welcome, ${info.name}.`);
},
logout: async function (){
await closeSession();
console.log('Logged out.');
},
me: async function (){
const me = await get('/user/me');
for (const k in me) {
if (k !== 'ok') {
console.log(`${k}: ${me[k]}`);
}
}
},
}

84
src/utils/index.js Normal file
View File

@@ -0,0 +1,84 @@
/**
* Created by tdzl2003 on 2/13/16.
*/
import * as path from 'path';
import * as fs from 'fs-extra';
const AppInfoParser = require('app-info-parser');
var read = require('read');
export function question(query, password) {
if (NO_INTERACTIVE) {
return Promise.resolve('');
}
return new Promise((resolve, reject) =>
read(
{
prompt: query,
silent: password,
replace: password ? '*' : undefined,
},
(err, result) => (err ? reject(err) : resolve(result)),
),
);
}
export function translateOptions(options) {
const ret = {};
for (let key in options) {
const v = options[key];
if (typeof v === 'string') {
ret[key] = v.replace(/\$\{(\w+)\}/g, function(v, n) {
return options[n] || process.env[n] || v;
});
} else {
ret[key] = v;
}
}
return ret;
}
export function getRNVersion() {
const version = JSON.parse(fs.readFileSync(path.resolve('node_modules/react-native/package.json'))).version;
// We only care about major and minor version.
const match = /^(\d+)\.(\d+)\./.exec(version);
return {
version,
major: match[1] | 0,
minor: match[2] | 0,
};
}
export async function getApkInfo(fn) {
const appInfoParser = new AppInfoParser(fn);
const { versionName, application } = await appInfoParser.parse();
let buildTime = 0;
if (Array.isArray(application.metaData)) {
for (const meta of application.metaData) {
if (meta.name === 'pushy_build_time') {
buildTime = meta.value[0];
}
}
}
if (buildTime == 0) {
throw new Error('Can not get build time for this app.');
}
return { versionName, buildTime };
}
export async function getIpaInfo(fn) {
const appInfoParser = new AppInfoParser(fn);
const { CFBundleShortVersionString: versionName } = await appInfoParser.parse();
let buildTimeTxtBuffer = await appInfoParser.parser.getEntry(/payload\/.+?\.app\/pushy_build_time.txt/);
if (!buildTimeTxtBuffer) {
// Not in root bundle when use `use_frameworks`
buildTimeTxtBuffer = await appInfoParser.parser.getEntry(/payload\/.+?\.app\/frameworks\/react_native_update.framework\/pushy_build_time.txt/);
}
if (!buildTimeTxtBuffer) {
throw new Error('Can not get build time for this app.');
}
const buildTime = buildTimeTxtBuffer.toString().replace('\n', '');
return { versionName, buildTime };
}

111
src/versions.js Normal file
View File

@@ -0,0 +1,111 @@
/**
* Created by tdzl2003 on 4/2/16.
*/
const {
get,
post,
put,
uploadFile,
} = require('./api');
import { question } from './utils';
import { checkPlatform, getSelectedApp } from './app';
import { choosePackage } from './package';
async function showVersion(appId, offset) {
const { data, count } = await get(`/app/${appId}/version/list`);
console.log(`Offset ${offset}`);
for (const version of data) {
let packageInfo = version.packages.slice(0, 3).map(v=>v.name).join(', ');
const count = version.packages.length;
if (count > 3) {
packageInfo += `...and ${count-3} more`;
}
if (count === 0) {
packageInfo = `(no package)`;
} else {
packageInfo = `[${packageInfo}]`;
}
console.log(`${version.id}) ${version.hash.slice(0, 8)} ${version.name} ${packageInfo}`);
}
return data;
}
async function listVersions(appId) {
let offset = 0;
while (true) {
await showVersion(appId, offset);
const cmd = await question('page Up/page Down/Begin/Quit(U/D/B/Q)');
switch (cmd.toLowerCase()) {
case 'u': offset = Math.max(0, offset - 10); break;
case 'd': offset += 10; break;
case 'b': offset = 0; break;
case 'q': return;
}
}
}
async function chooseVersion(appId) {
let offset = 0;
while (true) {
const data = await showVersion(appId, offset);
const cmd = await question('Enter versionId or page Up/page Down/Begin(U/D/B)');
switch (cmd.toLowerCase()) {
case 'U': offset = Math.max(0, offset - 10); break;
case 'D': offset += 10; break;
case 'B': offset = 0; break;
default:
{
const v = data.find(v=>v.id === (cmd | 0));
if (v) {
return v;
}
}
}
}
}
export const commands = {
publish: async function({args, options}) {
const fn = args[0];
const {name, description, metaInfo } = options;
if (!fn || !fn.endsWith('.ppk')) {
throw new Error('Usage: pushy publish <ppkFile> --platform ios|android');
}
const platform = checkPlatform(options.platform || await question('Platform(ios/android):'));
const { appId } = await getSelectedApp(platform);
const { hash } = await uploadFile(fn);
const { id } = await post(`/app/${appId}/version/create`, {
name: name || await question('Enter version name:') || '(未命名)',
hash,
description: description || await question('Enter description:'),
metaInfo: metaInfo || await question('Enter meta info:'),
});
console.log(`Version published: ${id}`);
const v = await question('Would you like to bind packages to this version?(Y/N)');
if (v.toLowerCase() === 'y') {
await this.update({args:[], options:{versionId: id, platform}});
}
},
versions: async function({options}) {
const platform = checkPlatform(options.platform || await question('Platform(ios/android):'));
const { appId } = await getSelectedApp(platform);
await listVersions(appId);
},
update: async function({args, options}) {
const platform = checkPlatform(options.platform || await question('Platform(ios/android):'));
const { appId } = await getSelectedApp(platform);
const versionId = options.versionId || (await chooseVersion(appId)).id;
const pkgId = options.packageId || (await choosePackage(appId)).id;
await put(`/app/${appId}/package/${pkgId}`, {
versionId,
});
console.log('Ok.');
}
};

3804
yarn.lock Normal file

File diff suppressed because it is too large Load Diff