Skip to content
This repository was archived by the owner on May 17, 2024. It is now read-only.

Commit 92ae274

Browse files
authored
Merge pull request #6 from stencila/feature/enter
feat: add `enter` command
2 parents 13c04c8 + 2870f56 commit 92ae274

File tree

5 files changed

+158
-22
lines changed

5 files changed

+158
-22
lines changed

package-lock.json

+22-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"@types/mkdirp": "^0.5.2",
4343
"@types/sprintf-js": "^1.1.1",
4444
"@types/stream-to-promise": "^2.2.0",
45+
"@types/tmp": "0.0.33",
4546
"@types/yargs": "^12.0.5",
4647
"all-contributors-cli": "^5.7.0",
4748
"jest": "^23.6.0",
@@ -62,7 +63,9 @@
6263
"glob": "^7.1.3",
6364
"js-yaml": "^3.12.1",
6465
"mkdirp": "^0.5.1",
66+
"node-pty": "^0.8.0",
6567
"sprintf-js": "^1.1.2",
68+
"tmp": "0.0.33",
6669
"yargs": "^12.0.5"
6770
}
6871
}

src/Environment.ts

+104-14
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import fs from 'fs'
2+
import os from 'os'
23
import path from 'path'
34

4-
// @ts-ignore
5-
import spawn from 'await-spawn'
5+
import chalk from 'chalk'
66
import del from 'del'
77
import glob from 'glob'
88
import mkdirp from 'mkdirp'
9+
import * as pty from 'node-pty'
10+
// @ts-ignore
11+
import spawn from 'await-spawn'
12+
import tmp from 'tmp'
913
import yaml from 'js-yaml'
1014

1115
import * as nix from './nix'
@@ -289,28 +293,114 @@ export default class Environment {
289293
}
290294

291295
/**
292-
* Execute a bash command within the environment
296+
* Create variables for an environment.
297+
*
298+
* This method is used in several other metho
299+
* e.g. `within`, `enter`
293300
*
294-
* A 'pure' shell will only hav available the executables that
301+
* A 'pure' environment will only have available the executables that
295302
* were exlicitly installed into the environment
296303
*
304+
* @param pure Should the shell that this command is executed in be 'pure'?
305+
*/
306+
async vars (pure: boolean = false) {
307+
const location = await nix.location(this.name)
308+
309+
let PATH = `${location}/bin:${location}/sbin`
310+
if (!pure) PATH += ':' + process.env.PATH
311+
312+
const R_LIBS_SITE = `${location}/library`
313+
314+
return {
315+
PATH,
316+
R_LIBS_SITE
317+
}
318+
}
319+
320+
/**
321+
* Execute a bash command within the environment
322+
*
297323
* @param command The command to execute
298324
* @param pure Should the shell that this command is executed in be 'pure'?
299325
*/
300326
async within (command: string, pure: boolean = false) {
301-
const location = await nix.location(this.name)
302-
let path = `${location}/bin:${location}/sbin`
303-
if (!pure) path += ':' + process.env.PATH
304-
// Get the path to bash because it may not be available in the PATH of
305-
// a pure shell
306-
let bash = await spawn('which', ['bash'])
307-
await spawn(bash.toString().trim(), ['-c', command], {
327+
// Get the path to bash because it may not be available in
328+
// the PATH of a pure shell
329+
let shell = await spawn('which', ['bash'])
330+
shell = shell.toString().trim()
331+
await spawn(shell, ['-c', command], {
308332
stdio: 'inherit',
309-
env: {
310-
PATH: path,
311-
R_LIBS_SITE: `${location}/library`
333+
env: await this.vars()
334+
})
335+
}
336+
337+
/**
338+
* Enter the a shell within the environment
339+
*
340+
* @param command An initial command to execute in the shell e.g. R or python
341+
* @param pure Should the shell be 'pure'?
342+
*/
343+
async enter (command: string = '', pure: boolean = true) {
344+
const shellName = os.platform() === 'win32' ? 'powershell.exe' : 'bash'
345+
const shellArgs = ['--noprofile']
346+
347+
// Path to the shell executable. We need to do this
348+
// because the environment may not actually have any shell
349+
// in it, in which case, when using `pure` a shell won't be available.
350+
let shellPath = await spawn('which', [shellName])
351+
shellPath = shellPath.toString().trim()
352+
353+
// Inject Nixster into the environment as an alias so we can use it
354+
// there without polluting the environment with additional binaries.
355+
// During development you'll need to use ---pure=false so that
356+
// node is available to run Nixster. In production, when a user
357+
// has installed a binary, this shouldn't be necessary
358+
let nixsterPath = await spawn('which', ['nixster'])
359+
const tempRcFile = tmp.fileSync()
360+
fs.writeFileSync(tempRcFile.name, `alias nixster="${nixsterPath.toString().trim()}"\n`)
361+
shellArgs.push('--rcfile', tempRcFile.name)
362+
363+
// Environment variables
364+
let vars = await this.vars(pure)
365+
vars = Object.assign(vars, {
366+
// Let Nixster know which environment we're in.
367+
NIXSTER_ENV: this.name,
368+
// Customise the bash prompt so that the user know that they are in
369+
// a Nixster environment and which one.
370+
PS1: '☆ ' + chalk.green.bold(this.name) + ':' + chalk.blue('\\w') + '$ '
371+
})
372+
373+
const shellProcess = pty.spawn(shellPath, shellArgs, {
374+
name: 'xterm-color',
375+
cols: 120,
376+
rows: 30,
377+
env: vars
378+
})
379+
shellProcess.on('data', data => {
380+
process.stdout.write(data)
381+
})
382+
383+
// To prevent echoing of input set stdin to raw mode (see https://github.com/Microsoft/node-pty/issues/78)
384+
// https://nodejs.org/api/tty.html: "When in raw mode, input is always available character-by-character,
385+
// not including modifiers. Additionally, all special processing of characters
386+
// by the terminal is disabled, including echoing input characters. Note that CTRL+C
387+
// will no longer cause a SIGINT when in this mode."
388+
// @ts-ignore
389+
process.stdin.setRawMode(true)
390+
391+
// Write the result through to the shell process
392+
// Capture Ctrl+D for special handling:
393+
// - if in the top level shell process then exit this process
394+
// - otherwise, pass on the process e.g. node, Rrm
395+
const ctrlD = Buffer.from([4])
396+
process.stdin.on('data', data => {
397+
if (data.equals(ctrlD) && shellProcess.process === shellPath) {
398+
process.exit(1)
312399
}
400+
shellProcess.write(data)
313401
})
402+
403+
if (command) shellProcess.write(command + '\r')
314404
}
315405

316406
/**

src/cli.ts

+28-4
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ yargs
5151
output(envs, argv, (envs: any) => {
5252
const layout = '%-15s %-40s %-80s'
5353
const header = sprintf(layout, chalk.gray('Ready'), chalk.gray('Name'), chalk.gray('Description')) + '\n'
54-
return header +
54+
return header +
5555
envs.map((env: any) => {
5656
const icon = env.built ? chalk.green('✓') : chalk.yellow('⚪')
5757
const name = chalk.blue(env.name)
@@ -91,7 +91,7 @@ yargs
9191
type: 'array'
9292
})
9393
.option('removes', {
94-
describe: 'Remove packages from environment',
94+
describe: 'Remove packages to environment',
9595
alias: 'r',
9696
type: 'array'
9797
})
@@ -201,6 +201,30 @@ yargs
201201
}
202202
})
203203

204+
.command('enter <name> [command..]', 'Enter a shell within the environment', (yargs: any) => {
205+
yargs
206+
.positional('name', {
207+
describe: 'Name of the environment',
208+
type: 'string'
209+
})
210+
.positional('command', {
211+
describe: 'An initial command to execute in the shell e.g. `R` or `python`',
212+
type: 'string'
213+
})
214+
.option('pure', {
215+
describe: 'Should the environment be pure (no host executables available)?',
216+
alias: 'p',
217+
type: 'boolean',
218+
default: true
219+
})
220+
}, async (argv: any) => {
221+
try {
222+
await new Environment(argv.name).enter(argv.command.join(' '), argv.pure)
223+
} catch (err) {
224+
error(err)
225+
}
226+
})
227+
204228
// TODO instead of a sperate command, --docker should be an option for the `build`, `within` and `enter` commands
205229
.command('dockerize [name] [command]', 'Containerize the environment using Docker', (yargs: any) => {
206230
yargs
@@ -406,7 +430,7 @@ function output (value: any, argv: any = {}, prettifier?: (value: any) => string
406430
* Add output options to a command.
407431
*
408432
* The aim of this approach is to have a consistent
409-
* and convenient way for users to be able to specify
433+
* and convieient way for users to be able to specify
410434
* the output format across commands.
411435
*
412436
* Formats can be specified explicitly e.g.
@@ -459,7 +483,7 @@ function outputOptions (yargs: any) {
459483
* Create a diagnostic message for the user
460484
*
461485
* @param message Message to display
462-
* @param emoji Emoji to prefix the display
486+
* @param emoji Emjoi to prefix the display
463487
*/
464488
function diag (message: string, emoji: string) {
465489
if (process.stderr.isTTY && emoji) process.stderr.write(emoji + ' ')

src/nix.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export function semver (version: string): string {
3030
// Register function in the database
3131
try {
3232
db.function('semver', semver)
33-
} catch(error) {
33+
} catch (error) {
3434
// The following error get's thown sometimes (maybe if the function
3535
// has already been registered?)
3636
if (error.message !== 'Expected first argument to be a string') {

0 commit comments

Comments
 (0)