1 | var chalk = require('chalk');
|
2 | var async = require('async');
|
3 | var _ = require('lodash');
|
4 | var fs = require('fs');
|
5 | var urljoin = require('url-join');
|
6 | var path = require('path');
|
7 | var inquirer = require('inquirer');
|
8 | var spawn = require('../lib/spawn');
|
9 | var api = require('../lib/api');
|
10 | var log = require('../lib/log');
|
11 | var manifest = require('../lib/manifest');
|
12 | var template = require('../lib/template');
|
13 | var helper = require('../lib/helper');
|
14 | var debug = require('debug')('4front:cli');
|
15 |
|
16 | require("simple-errors");
|
17 |
|
18 | module.exports = function(program, done) {
|
19 | program = _.defaults(program || {}, {
|
20 | baseDir: process.cwd()
|
21 | });
|
22 |
|
23 | log.messageBox("Create a new 4front app");
|
24 |
|
25 | collectInput(function(err, answers) {
|
26 | if (err) return done(err);
|
27 |
|
28 |
|
29 | log.blankLine();
|
30 |
|
31 | if (answers.confirmExistingDir === false)
|
32 | return done("Please re-run '4front create-app' from the root of the " +
|
33 | "directory where your existing app code resides.");
|
34 |
|
35 | var tasks = [], appDir;
|
36 | if (answers.startingMode === 'scratch' || program.templateUrl) {
|
37 |
|
38 | appDir = path.join(program.baseDir, answers.appName);
|
39 | tasks.push(function(cb) {
|
40 | fs.exists(appDir, function(exists) {
|
41 | debugger;
|
42 | if (exists === true)
|
43 | return cb("Directory " + appDir + " already exists.");
|
44 |
|
45 | log.info("Making app directory %s", appDir);
|
46 | fs.mkdir(appDir, cb);
|
47 | });
|
48 | });
|
49 | }
|
50 | else
|
51 | appDir = program.baseDir;
|
52 |
|
53 | debug("Setting appDir to %s", appDir);
|
54 |
|
55 | if (answers.templateUrl === 'blank') {
|
56 |
|
57 | tasks.push(function(cb) {
|
58 | createBlankStart(answers, appDir, cb);
|
59 | });
|
60 | }
|
61 | else if (answers.templateUrl) {
|
62 | tasks.push(function(cb) {
|
63 | unpackTemplate(answers.templateUrl, appDir, cb);
|
64 | });
|
65 | }
|
66 | else if (program.templateUrl) {
|
67 | tasks.push(function(cb) {
|
68 | unpackTemplate(program.templateUrl, appDir, cb);
|
69 | });
|
70 | }
|
71 |
|
72 | tasks.push(function(cb) {
|
73 | npmInstall(appDir, cb);
|
74 | });
|
75 |
|
76 | tasks.push(function(cb) {
|
77 | bowerInstall(appDir, cb);
|
78 | });
|
79 |
|
80 | var createdApp = null;
|
81 |
|
82 | tasks.push(function(cb) {
|
83 | invokeCreateAppApi(answers, function(_err, app) {
|
84 | if (err) return cb(_err);
|
85 | createdApp = app;
|
86 | cb(null);
|
87 | });
|
88 | });
|
89 |
|
90 |
|
91 | tasks.push(function(cb) {
|
92 | manifest.update(appDir, createdApp, cb);
|
93 | });
|
94 |
|
95 | async.series(tasks, function(_err) {
|
96 | if (_err) return done(_err);
|
97 |
|
98 | var message = "App created successfully at:\n" + createdApp.url +
|
99 | "\n\n";
|
100 |
|
101 | if (answers.startingMode === 'existing')
|
102 | message += "To start developing run:\n$ 4front dev";
|
103 | else
|
104 | message += "To start developing run:\n$ cd " + createdApp.name +
|
105 | "\n\nThen:\n$ 4front dev";
|
106 |
|
107 | message +=
|
108 | "\n\nWhen you are ready to deploy, simply run:\n$ 4front deploy";
|
109 | log.messageBox(message);
|
110 |
|
111 | done(null, createdApp);
|
112 | });
|
113 | });
|
114 |
|
115 | function collectInput(callback) {
|
116 | async.parallel({
|
117 | templates: loadStarterTemplates,
|
118 | organizations: loadOrganizations
|
119 | }, function(err, results) {
|
120 | if (err) return callback(err);
|
121 |
|
122 | if (results.organizations.length === 0) {
|
123 | return callback(
|
124 | "You need to belong to an organization to create a new app. Visit " +
|
125 | urljoin(program.profile.endpoint, '/portal/orgs/create') + " to get started.");
|
126 | }
|
127 |
|
128 | promptQuestions(results, callback);
|
129 | });
|
130 | }
|
131 |
|
132 | function loadStarterTemplates(callback) {
|
133 | debug("fetching app templates");
|
134 | api(program, {
|
135 | method: 'GET',
|
136 | path: '/platform/starter-templates'
|
137 | }, function(err, templates) {
|
138 | if (err) return callback(err);
|
139 | callback(null, templates);
|
140 | });
|
141 | }
|
142 |
|
143 | function loadOrganizations(callback) {
|
144 |
|
145 | debug("fetching organizations");
|
146 | api(program, {
|
147 | method: 'GET',
|
148 | path: '/profile/orgs'
|
149 | }, function(err, orgs) {
|
150 | if (err) return callback(err);
|
151 | callback(null, orgs);
|
152 | });
|
153 | }
|
154 |
|
155 | function promptQuestions(lookups, callback) {
|
156 | var questions = [];
|
157 |
|
158 |
|
159 | if (_.isArray(lookups.organizations) && lookups.organizations.length > 0) {
|
160 | questions.push(helper.pickOrgQuestion(lookups.organizations,
|
161 | "Which organization does this app belong?"));
|
162 | }
|
163 |
|
164 | questions.push({
|
165 | type: 'list',
|
166 | name: 'startingMode',
|
167 | choices: [{
|
168 | name: 'Starting from scratch',
|
169 | value: 'scratch'
|
170 | }, {
|
171 | name: 'Existing code',
|
172 | value: 'existing'
|
173 | }],
|
174 | default: null,
|
175 | when: function() {
|
176 |
|
177 |
|
178 | return !program.templateUrl;
|
179 | },
|
180 | message: "Are you starting this app from existing code or from scratch?"
|
181 | });
|
182 |
|
183 |
|
184 |
|
185 | questions.push({
|
186 | type: 'confirm',
|
187 | name: 'confirmExistingDir',
|
188 | message: 'Is this directory ' + program.baseDir +
|
189 | ' the location of your existing code?',
|
190 | when: function(answers) {
|
191 | return answers.startingMode === 'existing';
|
192 | }
|
193 | });
|
194 |
|
195 |
|
196 | questions.push({
|
197 | type: 'list',
|
198 | message: 'Select app template to start from',
|
199 | name: 'templateUrl',
|
200 | when: function(answers) {
|
201 | return answers.startingMode === 'scratch';
|
202 | },
|
203 | choices: buildTemplateChoices(lookups.templates)
|
204 | });
|
205 |
|
206 | inquirer.prompt(questions, function(answers) {
|
207 | if (answers.confirmExistingDir === false)
|
208 | return callback(answers);
|
209 |
|
210 | collectAppName(function(err, appName) {
|
211 | if (err) return callback(err);
|
212 |
|
213 | answers.appName = appName;
|
214 | callback(null, answers);
|
215 | });
|
216 | });
|
217 | }
|
218 |
|
219 | function collectAppName(callback) {
|
220 | log.messageBox(
|
221 | "Please choose a name for your app which will be used as\nthe URL, " +
|
222 | "i.e. http://<app_name>." + program.virtualHost + ".\nNames may only " +
|
223 | "contain lowercase letters, numbers,\nand dashes."
|
224 | );
|
225 |
|
226 | var question = {
|
227 | type: 'input',
|
228 | message: 'App name',
|
229 | name: 'appName',
|
230 | validate: function(input) {
|
231 | if (!/^[a-z0-9\-]+$/.test(input))
|
232 | return "Invalid app name";
|
233 |
|
234 | return true;
|
235 | }
|
236 | };
|
237 |
|
238 | var appName = null;
|
239 |
|
240 |
|
241 | async.until(function() {
|
242 | return appName !== null;
|
243 | }, function(cb) {
|
244 | inquirer.prompt([question], function(answers) {
|
245 | appNameExists(answers.appName, function(err, exists) {
|
246 | if (exists)
|
247 | log.writeln(chalk.red(">>") + " App name \"" + answers.appName +
|
248 | "\" is already taken. Please choose another name.");
|
249 | else
|
250 | appName = answers.appName;
|
251 |
|
252 | cb();
|
253 | });
|
254 | });
|
255 | }, function(err) {
|
256 | if (err) return callback(err);
|
257 | callback(null, appName);
|
258 | });
|
259 | }
|
260 |
|
261 | function buildTemplateChoices(templates) {
|
262 | var choices = [];
|
263 | choices.push({
|
264 | name: 'Blank',
|
265 | value: 'blank'
|
266 | });
|
267 |
|
268 | templates.forEach(function(_template) {
|
269 | if (_template.published !== false)
|
270 | choices.push({
|
271 | name: _template.name,
|
272 | value: _template.url
|
273 | });
|
274 | });
|
275 |
|
276 | return choices;
|
277 | }
|
278 |
|
279 | function npmInstall(appDir, callback) {
|
280 | fs.exists(path.join(appDir, 'package.json'), function(exists) {
|
281 | if (!exists)
|
282 | return callback();
|
283 |
|
284 |
|
285 | if (fs.exists(path.join(appDir, 'node_modules')))
|
286 | return callback();
|
287 |
|
288 | log.info("Installing npm dependencies in %s", appDir);
|
289 | spawn('npm', ['install'], {
|
290 | cwd: appDir,
|
291 | inheritStdio: true
|
292 | }, callback);
|
293 | });
|
294 | }
|
295 |
|
296 | function bowerInstall(appDir, callback) {
|
297 | fs.exists(path.join(appDir, 'bower.json'), function(exists) {
|
298 | if (!exists) {
|
299 | debug("No bower.json file exists in app directory");
|
300 | return callback();
|
301 | }
|
302 |
|
303 | log.info("Installing bower dependencies");
|
304 | spawn('bower', ['install'], {
|
305 | cwd: appDir,
|
306 | inheritStdio: true
|
307 | }, callback);
|
308 | });
|
309 | }
|
310 |
|
311 | function unpackTemplate(templateUrl, appDir, callback) {
|
312 |
|
313 | log.info("Unpacking template %s to %s", templateUrl, appDir);
|
314 |
|
315 | template.extract(templateUrl, appDir, callback);
|
316 | }
|
317 |
|
318 | function createBlankStart(answers, appDir, callback) {
|
319 |
|
320 |
|
321 | var blankHtml = "<!DOCTYPE html>\n" +
|
322 | "<html>\n" +
|
323 | "\t<head>\n" +
|
324 | "\t\t<title>Blank 4front App</title>\n" +
|
325 | "\t</head>\n" +
|
326 | "\t<body>\n" +
|
327 | "\t\t<h1>Blank 4front App</h1>\n" +
|
328 | "\t</body>\n" +
|
329 | "</html>";
|
330 |
|
331 | fs.writeFile(path.join(appDir, 'index.html'), blankHtml, callback);
|
332 | }
|
333 |
|
334 | function invokeCreateAppApi(answers, callback) {
|
335 | var appData = {
|
336 | name: answers.appName
|
337 | };
|
338 |
|
339 | if (answers.orgId)
|
340 | appData.orgId = answers.orgId;
|
341 |
|
342 | var options = {
|
343 | method: 'POST',
|
344 | path: '/orgs/' + answers.orgId + '/apps',
|
345 | json: appData
|
346 | };
|
347 |
|
348 | log.info("Invoking 4front API to create app");
|
349 | api(program, options, function(err, app) {
|
350 | debug("done invoking api");
|
351 | if (err) return callback(err);
|
352 |
|
353 | debug("api post to /api/apps succeeded");
|
354 | callback(null, app);
|
355 | });
|
356 | }
|
357 |
|
358 |
|
359 | function appNameExists(appName, callback) {
|
360 | var options = {
|
361 | method: 'HEAD',
|
362 | path: '/apps/' + appName
|
363 | };
|
364 |
|
365 | debug("checking if app name exists");
|
366 | api(program, options, function(err, body, statusCode) {
|
367 | if (err) return callback(err);
|
368 |
|
369 | return callback(null, statusCode === 200);
|
370 | });
|
371 | }
|
372 | };
|