UNPKG

17 kBPlain TextView Raw
1import {client} from '@haiku/sdk-client';
2import {inkstone} from '@haiku/sdk-inkstone';
3import {ErrorCode} from '@haiku/sdk-inkstone/lib/errors';
4
5import {
6 DEFAULT_BRANCH_NAME,
7 fetchProjectConfigInfo,
8 getHaikuComponentInitialVersion,
9 storeConfigValues,
10} from '@haiku/sdk-client/lib/ProjectDefinitions';
11
12import {bootstrapSceneFilesSync} from '@haiku/sdk-client/lib/bootstrapSceneFilesSync';
13import {createProjectFiles} from '@haiku/sdk-client/lib/createProjectFiles';
14
15import * as chalk from 'chalk';
16import {execSync} from 'child_process';
17import * as dedent from 'dedent';
18import * as fs from 'fs';
19// @ts-ignore
20import * as hasbin from 'hasbin';
21import * as inquirer from 'inquirer';
22import * as _ from 'lodash';
23import * as path from 'path';
24// @ts-ignore
25import * as prependFile from 'prepend-file';
26
27import {IContext, Nib} from './nib';
28
29// tslint:disable-next-line:no-var-requires
30const pkg = require('./../package.json');
31
32const 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
152export {cli};
153
154function 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
176function 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
217function 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
252function 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
291function doInit (context: IContext) {
292 // Set up @haiku scope for this project if it doesn't exist
293 let npmrc = '';
294 try {
295 npmrc = fs.readFileSync('.npmrc').toString();
296 } catch (exception) {
297 if (exception.code === 'ENOENT') {
298 // file not found, this is fine
299 } else {
300 // different error, should throw
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
312function doInstall (context: IContext) {
313 const projectName = context.args['project-name'];
314 ensureAuth(context, () => {
315 // ensure that npm is installed
316 hasbin('npm', (result: boolean) => {
317 if (result) {
318 // ensure that there's a package.json in this directory
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 // construct project string: @haiku/org-project#latest
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 // TODO: for multi-org support, get the org name more intelligently than this
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 // now projectString should be @haiku/org-project
358 packageJson.dependencies[projectString] = 'latest';
359
360 // Set up @haiku scope for this project if it doesn't exist
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
394function 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
428function 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
471function doLogout () {
472 // TODO: expire auth token on inkstone?
473 client.config.setAuthToken('');
474 process.exit(0);
475}
476
477// TODO: update only @haiku packages, instead of all updatable packages in package.json
478function 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
499function 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// see ./unimplemented.txt for incomplete player upgrade logic