1 | import {client} from '@haiku/sdk-client';
|
2 | import {inkstone} from '@haiku/sdk-inkstone';
|
3 | import {ErrorCode} from '@haiku/sdk-inkstone/lib/errors';
|
4 |
|
5 | import {
|
6 | DEFAULT_BRANCH_NAME,
|
7 | fetchProjectConfigInfo,
|
8 | getHaikuComponentInitialVersion,
|
9 | storeConfigValues,
|
10 | } from '@haiku/sdk-client/lib/ProjectDefinitions';
|
11 |
|
12 | import {bootstrapSceneFilesSync} from '@haiku/sdk-client/lib/bootstrapSceneFilesSync';
|
13 | import {createProjectFiles} from '@haiku/sdk-client/lib/createProjectFiles';
|
14 |
|
15 | import * as chalk from 'chalk';
|
16 | import {execSync} from 'child_process';
|
17 | import * as dedent from 'dedent';
|
18 | import * as fs from 'fs';
|
19 |
|
20 | import * as hasbin from 'hasbin';
|
21 | import * as inquirer from 'inquirer';
|
22 | import * as _ from 'lodash';
|
23 | import * as path from 'path';
|
24 |
|
25 | import * as prependFile from 'prepend-file';
|
26 |
|
27 | import {IContext, Nib} from './nib';
|
28 |
|
29 |
|
30 | const pkg = require('./../package.json');
|
31 |
|
32 | const cli = new Nib({
|
33 | name: 'haiku',
|
34 | version: pkg.version,
|
35 | description: 'The Haiku CLI — developer utilities for automating Haiku actions and performing local and' +
|
36 | ' server-enabled actions without requiring the desktop app.',
|
37 | preAction (context: IContext) {
|
38 | client.config.getenv();
|
39 | },
|
40 | commands: [
|
41 | {
|
42 | name: 'list',
|
43 | action: doList,
|
44 | flags: [
|
45 | {
|
46 | name: 'organizations',
|
47 | defaultValue: undefined,
|
48 | description: 'include to list organizations your account is a member of instead of projects',
|
49 | },
|
50 | ],
|
51 | description: 'Lists your team projects',
|
52 | },
|
53 | {
|
54 | name: 'change-password',
|
55 | action: doChangePassword,
|
56 | description: 'Changes your Haiku account password (interactive)',
|
57 | },
|
58 | {
|
59 | name: 'clone',
|
60 | action: doClone,
|
61 | description: 'Clone a Haiku project to your filesystem, passing through to git clone',
|
62 | args: [
|
63 | {
|
64 | name: 'project-name',
|
65 | required: true,
|
66 | usage: 'Clone a Haiku project to your filesystem, passing through to git clone',
|
67 | },
|
68 | {
|
69 | name: 'destination',
|
70 | required: false,
|
71 | usage: 'Optional: location on the file system where the project should be cloned',
|
72 | },
|
73 | ],
|
74 | },
|
75 | {
|
76 | name: 'delete',
|
77 | action: doDelete,
|
78 | description: 'Deletes a Haiku project for your entire team. Cannot be undone.',
|
79 | args: [
|
80 | {
|
81 | name: 'project-name',
|
82 | required: false,
|
83 | usage: 'Specifies the name of the project to delete (case-sensitive.) If this isn\'t provided, the action' +
|
84 | ' will be interactive.',
|
85 | },
|
86 | ],
|
87 | },
|
88 | {
|
89 | name: 'init',
|
90 | action: doInit,
|
91 | description: 'Inits a project for installing @haiku modules. ' +
|
92 | 'Will write or append to a .npmrc in this directory.',
|
93 | },
|
94 | {
|
95 | name: 'install',
|
96 | action: doInstall,
|
97 | description: 'Install a Haiku project as an npm module, requires a package.json',
|
98 | args: [
|
99 | {
|
100 | name: 'project-name',
|
101 | required: true,
|
102 | usage: 'Specifies the name of the project to install as a dependency. Case-sensitive.',
|
103 | },
|
104 | ],
|
105 | },
|
106 | {
|
107 | name: 'login',
|
108 | action: doLogin,
|
109 | description: 'Logs into Haiku services. (interactive)',
|
110 | },
|
111 | {
|
112 | name: 'logout',
|
113 | action: doLogout,
|
114 | description: 'Logs out of Haiku services.',
|
115 | },
|
116 | {
|
117 | name: 'update',
|
118 | aliases: ['upgrade'],
|
119 | args: [
|
120 | {
|
121 | name: 'project-name',
|
122 | required: false,
|
123 | usage: 'Specifies the name of the project to update as a dependency. Case-sensitive. If not provided,' +
|
124 | ' will update all detected Haiku projects.',
|
125 | },
|
126 | {
|
127 | name: 'version',
|
128 | required: false,
|
129 | usage: 'Specifies the version to update specified dependency to. If not provided, will update to the' +
|
130 | ' latest available version.',
|
131 | },
|
132 | ],
|
133 | action: doUpdate,
|
134 | description: 'Updates dependencies',
|
135 | },
|
136 | {
|
137 | name: 'generate',
|
138 | aliases: ['g'],
|
139 | args: [
|
140 | {
|
141 | name: 'component-name',
|
142 | required: true,
|
143 | usage: 'Specifies the name of new component to be generated. Case-sensitive and must be unique.',
|
144 | },
|
145 | ],
|
146 | action: generateComponent,
|
147 | description: 'Generate new component',
|
148 | },
|
149 | ],
|
150 | });
|
151 |
|
152 | export {cli};
|
153 |
|
154 | function ensureAuth (context: IContext, cb: (authToken: string) => void) {
|
155 | const authToken: string = client.config.getAuthToken();
|
156 | if (authToken) {
|
157 | inkstone.setConfig({authToken});
|
158 | cb(authToken);
|
159 | return;
|
160 | }
|
161 |
|
162 | context.writeLine('You must be authenticated to do that.');
|
163 | doLogin(context, () => {
|
164 | const newToken: string = client.config.getAuthToken();
|
165 | if (newToken) {
|
166 | inkstone.setConfig({authToken: newToken});
|
167 | cb(newToken);
|
168 | return;
|
169 | }
|
170 |
|
171 | context.writeLine('Hm, that didn\'t work. Let\'s try again.');
|
172 | ensureAuth(context, cb);
|
173 | });
|
174 | }
|
175 |
|
176 | function doChangePassword (context: IContext) {
|
177 | ensureAuth(context, (token) => {
|
178 | inquirer.prompt([
|
179 | {
|
180 | type: 'password',
|
181 | name: 'OldPassword',
|
182 | message: 'Old Password:',
|
183 | },
|
184 | {
|
185 | type: 'password',
|
186 | name: 'NewPassword',
|
187 | message: 'New Password:',
|
188 | },
|
189 | {
|
190 | type: 'password',
|
191 | name: 'NewPassword2',
|
192 | message: 'New Password (confirm):',
|
193 | },
|
194 | ]).then((answers: inquirer.Answers) => {
|
195 | if (answers.NewPassword !== answers.NewPassword2) {
|
196 | context.writeLine(chalk.red('New passwords do not match.'));
|
197 | process.exit(1);
|
198 | }
|
199 |
|
200 | const params: inkstone.user.ChangePasswordParams = {
|
201 | OldPassword: answers.OldPassword,
|
202 | NewPassword: answers.NewPassword,
|
203 | };
|
204 |
|
205 | inkstone.user.changePassword(token, params, (err, responseBody, response) => {
|
206 | if (err) {
|
207 | context.writeLine(chalk.bold(`Unabled to change password: `) + err);
|
208 | process.exit(1);
|
209 | } else {
|
210 | context.writeLine(chalk.green('Password updated.'));
|
211 | }
|
212 | });
|
213 | });
|
214 | });
|
215 | }
|
216 |
|
217 | function doClone (context: IContext) {
|
218 | const projectName = context.args['project-name'];
|
219 | let destination = context.args.destination || projectName;
|
220 | if (destination.charAt(destination.length - 1) !== '/') {
|
221 | destination += '/';
|
222 | }
|
223 |
|
224 | ensureAuth(context, (token) => {
|
225 | context.writeLine('Cloning project...');
|
226 | inkstone.project.get({Name: projectName}, (getByNameErr, projectAndCredentials) => {
|
227 | if (getByNameErr) {
|
228 | switch (getByNameErr.message) {
|
229 | case ErrorCode.ErrorCodeProjectNotFound:
|
230 | context.writeLine(chalk.bold(`Project ${projectName} not found.`));
|
231 | break;
|
232 | case ErrorCode.ErrorCodeProjectNameRequired:
|
233 | context.writeLine(chalk.bold(`Project name is required.`));
|
234 | break;
|
235 | }
|
236 | process.exit(1);
|
237 | }
|
238 |
|
239 | client.git.cloneRepo(projectAndCredentials.RepositoryUrl, destination, (cloneErr) => {
|
240 | if (cloneErr) {
|
241 | context.writeLine(chalk.red('Error cloning project. Use the --verbose flag for more information.'));
|
242 | process.exit(1);
|
243 | } else {
|
244 | context.writeLine(`Project ${chalk.bold(projectName)} cloned to ${chalk.bold(destination)}`);
|
245 | process.exit(0);
|
246 | }
|
247 | });
|
248 | });
|
249 | });
|
250 | }
|
251 |
|
252 | function doDelete (context: IContext) {
|
253 | ensureAuth(context, (token: string) => {
|
254 | context.writeLine(chalk.bold('Please note that deleting this project will delete it for your entire team.'));
|
255 | context.writeLine(chalk.red('Deleting a project cannot be undone!'));
|
256 |
|
257 | const actuallyDelete = (finalProjectName: string) => {
|
258 | inkstone.project.deleteByName({Name: finalProjectName}, (err) => {
|
259 | if (err) {
|
260 | context.writeLine(chalk.red('Error deleting project. Does this project exist?'));
|
261 | process.exit(1);
|
262 | } else {
|
263 | context.writeLine(chalk.green('Project deleted!'));
|
264 | process.exit(0);
|
265 | }
|
266 | });
|
267 | };
|
268 |
|
269 | let projectName = context.args['project-name'];
|
270 |
|
271 | if (projectName) {
|
272 | actuallyDelete(projectName);
|
273 | } else {
|
274 | inquirer
|
275 | .prompt([
|
276 | {
|
277 | type: 'input',
|
278 | name: 'name',
|
279 | message: 'Project Name:',
|
280 | },
|
281 | ])
|
282 | .then((answers: inquirer.Answers) => {
|
283 | projectName = answers.name;
|
284 | context.writeLine('Deleting project...');
|
285 | actuallyDelete(projectName);
|
286 | });
|
287 | }
|
288 | });
|
289 | }
|
290 |
|
291 | function doInit (context: IContext) {
|
292 |
|
293 | let npmrc = '';
|
294 | try {
|
295 | npmrc = fs.readFileSync('.npmrc').toString();
|
296 | } catch (exception) {
|
297 | if (exception.code === 'ENOENT') {
|
298 |
|
299 | } else {
|
300 |
|
301 | throw (exception);
|
302 | }
|
303 | }
|
304 | if (npmrc.indexOf('@haiku') === -1) {
|
305 | prependFile.sync('.npmrc', dedent`
|
306 | //reservoir.haiku.ai:8910/:_authToken=
|
307 | @haiku:registry=https://reservoir.haiku.ai:8910/\n
|
308 | `);
|
309 | }
|
310 | }
|
311 |
|
312 | function doInstall (context: IContext) {
|
313 | const projectName = context.args['project-name'];
|
314 | ensureAuth(context, () => {
|
315 |
|
316 | hasbin('npm', (result: boolean) => {
|
317 | if (result) {
|
318 |
|
319 | if (fs.existsSync(process.cwd() + '/package.json')) {
|
320 | context.writeLine('Installing ' + projectName + '...');
|
321 |
|
322 | const packageJson = client.npm.readPackageJson();
|
323 |
|
324 | if (!packageJson.dependencies) {
|
325 | packageJson.dependencies = {};
|
326 | }
|
327 |
|
328 |
|
329 | let projectString = '@haiku/';
|
330 | inkstone.organization.list((listErr, orgs) => {
|
331 | if (listErr) {
|
332 | context.writeLine(
|
333 | chalk.red('There was an error retrieving your account information.') +
|
334 | ' Please ensure that you have internet access.' +
|
335 | ' If this problem persists, please contact support@haiku.ai and tell us that you don\'t have an' +
|
336 | ' organization associated with your account.',
|
337 | );
|
338 | process.exit(1);
|
339 | }
|
340 |
|
341 |
|
342 | projectString += orgs[0].Name.toLowerCase() + '-';
|
343 |
|
344 | inkstone.project.get({Name: projectName}, (getByNameErr, projectAndCredentials) => {
|
345 | if (getByNameErr) {
|
346 | context.writeLine(
|
347 | chalk.red('That project wasn\'t found.') +
|
348 | ' Note that project names are CaseSensitive. ' +
|
349 | 'Please ensure that you have the correct project name, that you\'re logged into the correct' +
|
350 | ' account, and that you have internet access.',
|
351 | );
|
352 | process.exit(1);
|
353 | }
|
354 |
|
355 | projectString += projectAndCredentials.Name.toLowerCase();
|
356 |
|
357 |
|
358 | packageJson.dependencies[projectString] = 'latest';
|
359 |
|
360 |
|
361 | doInit(context);
|
362 |
|
363 | client.npm.writePackageJson(packageJson);
|
364 | try {
|
365 | execSync('npm install');
|
366 | } catch (e) {
|
367 | context.writeLine(`${chalk.red('npm install failed.')} Your Haiku packages have been injected` +
|
368 | ' into package.json, but npm install failed. Please try again.');
|
369 | process.exit(1);
|
370 | }
|
371 |
|
372 | context.writeLine(chalk.green('Haiku project installed successfully.'));
|
373 | process.exit(0);
|
374 | });
|
375 |
|
376 | });
|
377 |
|
378 | } else {
|
379 | context.writeLine(chalk.red('haiku install can only be used at the root of a project with a package.json.'));
|
380 | context.writeLine('You can use ' + chalk.bold('haiku clone ProjectName [/Optional/Destination]') +
|
381 | ' to clone the project\'s git repo directly.');
|
382 | process.exit(1);
|
383 | }
|
384 | } else {
|
385 | context.writeLine(chalk.red('npm was not found on this machine. ') +
|
386 | ' We recommend installing it with nvm: https://github.com/creationix/nvm');
|
387 | process.exit(1);
|
388 | }
|
389 | });
|
390 |
|
391 | });
|
392 | }
|
393 |
|
394 | function doList (context: IContext) {
|
395 |
|
396 | ensureAuth(context, () => {
|
397 | if (context.flags.organizations) {
|
398 | inkstone.organization.list((err, organizations, resp) => {
|
399 | if (organizations === undefined || organizations.length === 0) {
|
400 | context.writeLine('You are not a member of any organizations.');
|
401 | } else {
|
402 | context.writeLine(chalk.cyan('Your Organizations:'));
|
403 | _.forEach(organizations, (org) => {
|
404 | context.writeLine(' ' + org.Name);
|
405 | });
|
406 | }
|
407 | process.exit(0);
|
408 | });
|
409 | } else {
|
410 | inkstone.project.list((err, projects) => {
|
411 | if (!projects || projects.length === 0) {
|
412 | context.writeLine('No existing projects. Use ' + chalk.bold('haiku generate') + ' to make a new one!');
|
413 | process.exit(0);
|
414 | } else {
|
415 | context.writeLine(chalk.cyan('Your team\'s Haiku projects:'));
|
416 | context.writeLine('(To work with one, call ' + chalk.bold('haiku clone project_name') + ' or ' +
|
417 | chalk.bold('haiku install project_name'));
|
418 | _.forEach(projects, (project) => {
|
419 | context.writeLine(' ' + project.Name);
|
420 | });
|
421 | process.exit(0);
|
422 | }
|
423 | });
|
424 | }
|
425 | });
|
426 | }
|
427 |
|
428 | function doLogin (context: IContext, cb?: () => void) {
|
429 | context.writeLine('Enter your Haiku credentials.');
|
430 | let username = '';
|
431 | let password = '';
|
432 |
|
433 | inquirer.prompt([
|
434 | {
|
435 | type: 'input',
|
436 | name: 'username',
|
437 | message: 'Email:',
|
438 | },
|
439 | {
|
440 | type: 'password',
|
441 | name: 'password',
|
442 | message: 'Password:',
|
443 | },
|
444 | ]).then((answers: inquirer.Answers) => {
|
445 | username = answers.username;
|
446 | password = answers.password;
|
447 |
|
448 | inkstone.user.authenticate(username, password, (err, authResponse, httpResponse) => {
|
449 | if (err !== undefined) {
|
450 | if (httpResponse && httpResponse.statusCode === 403) {
|
451 | context.writeLine(chalk.bold.yellow('You must verify your email address before logging in.'));
|
452 | } else {
|
453 | context.writeLine(chalk.bold.red('Username or password incorrect.'));
|
454 | }
|
455 | if (context.flags.verbose) {
|
456 | context.writeLine(err.toString());
|
457 | }
|
458 | } else {
|
459 | client.config.setAuthToken(authResponse.Token);
|
460 | context.writeLine(chalk.bold.green(`Welcome ${username}!`));
|
461 | }
|
462 | if (cb) {
|
463 | cb();
|
464 | } else {
|
465 | process.exit(0);
|
466 | }
|
467 | });
|
468 | });
|
469 | }
|
470 |
|
471 | function doLogout () {
|
472 |
|
473 | client.config.setAuthToken('');
|
474 | process.exit(0);
|
475 | }
|
476 |
|
477 |
|
478 | function doUpdate (context: IContext) {
|
479 | hasbin('npm', (result: boolean) => {
|
480 | if (result) {
|
481 | try {
|
482 | context.writeLine('Updating packages...');
|
483 | execSync('npm update');
|
484 | context.writeLine(chalk.green('Haiku packages updated successfully.'));
|
485 | process.exit(0);
|
486 | } catch (e) {
|
487 | context.writeLine(chalk.red('npm update failed.') +
|
488 | ' This may be a configuration issue with npm. Try running npm install and then running haiku update again.');
|
489 | process.exit(1);
|
490 | }
|
491 | } else {
|
492 | context.writeLine(chalk.red('npm was not found on this machine. ') +
|
493 | ' We recommend installing it with nvm: https://github.com/creationix/nvm');
|
494 | process.exit(1);
|
495 | }
|
496 | });
|
497 | }
|
498 |
|
499 | function generateComponent (context: IContext) {
|
500 | const componentName = context.args['component-name'];
|
501 |
|
502 | context.writeLine('Creating component...');
|
503 |
|
504 | const projectPath = path.join(process.cwd(), componentName);
|
505 | const projectName = componentName;
|
506 |
|
507 | const authorName: string = null;
|
508 | const organizationName: string = null;
|
509 |
|
510 | storeConfigValues(projectPath, {
|
511 | username: authorName,
|
512 | branch: DEFAULT_BRANCH_NAME,
|
513 | version: getHaikuComponentInitialVersion(),
|
514 | organization: organizationName,
|
515 | project: projectName,
|
516 | });
|
517 |
|
518 | const projectOptions = {
|
519 | organizationName,
|
520 | projectName,
|
521 | projectPath,
|
522 | authorName,
|
523 | skipContentCreation: false,
|
524 | };
|
525 |
|
526 | createProjectFiles(projectOptions, () => {
|
527 | context.writeLine('Created initial project files');
|
528 | fetchProjectConfigInfo(projectPath, (err: Error|null, userconfig: any) => {
|
529 | if (err) {
|
530 | throw err;
|
531 | }
|
532 | bootstrapSceneFilesSync(projectPath, 'main', userconfig);
|
533 | context.writeLine('Created main component');
|
534 | });
|
535 | });
|
536 |
|
537 | context.writeLine('Project created');
|
538 | process.exit(0);
|
539 |
|
540 | }
|
541 |
|
542 |
|