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

Commit dcdb25b

Browse files
committed
feat: add enter command
1 parent 13c04c8 commit dcdb25b

File tree

4 files changed

+125
-14
lines changed

4 files changed

+125
-14
lines changed

package-lock.json

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

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"glob": "^7.1.3",
6363
"js-yaml": "^3.12.1",
6464
"mkdirp": "^0.5.1",
65+
"node-pty": "^0.8.0",
6566
"sprintf-js": "^1.1.2",
6667
"yargs": "^12.0.5"
6768
}

src/Environment.ts

+85-14
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
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'
912
import yaml from 'js-yaml'
1013

1114
import * as nix from './nix'
@@ -289,28 +292,96 @@ export default class Environment {
289292
}
290293

291294
/**
292-
* Execute a bash command within the environment
295+
* Create variables for an environment.
296+
*
297+
* This method is used in several other metho
298+
* e.g. `within`, `enter`
293299
*
294-
* A 'pure' shell will only hav available the executables that
300+
* A 'pure' environment will only have available the executables that
295301
* were exlicitly installed into the environment
296302
*
303+
* @param pure Should the shell that this command is executed in be 'pure'?
304+
*/
305+
async vars (pure: boolean = false) {
306+
const location = await nix.location(this.name)
307+
308+
let PATH = `${location}/bin:${location}/sbin`
309+
if (!pure) PATH += ':' + process.env.PATH
310+
311+
const R_LIBS_SITE = `${location}/library`
312+
313+
return {
314+
PATH,
315+
R_LIBS_SITE
316+
}
317+
}
318+
319+
/**
320+
* Execute a bash command within the environment
321+
*
297322
* @param command The command to execute
298323
* @param pure Should the shell that this command is executed in be 'pure'?
299324
*/
300325
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], {
326+
// Get the path to bash because it may not be available in
327+
// the PATH of a pure shell
328+
let shell = await spawn('which', ['bash'])
329+
shell = shell.toString().trim()
330+
await spawn(shell, ['-c', command], {
308331
stdio: 'inherit',
309-
env: {
310-
PATH: path,
311-
R_LIBS_SITE: `${location}/library`
332+
env: await this.vars()
333+
})
334+
}
335+
336+
/**
337+
* Enter the a shell within the environment
338+
*
339+
* @param command An initial command to execute in the shell e.g. R or python
340+
* @param pure Should the shell be 'pure'?
341+
*/
342+
async enter (command: string = '', pure: boolean = true) {
343+
const shellName = os.platform() === 'win32' ? 'powershell.exe' : 'bash'
344+
const shellArgs = ['--noprofile', '--norc']
345+
346+
let shellPath = await spawn('which', [shellName])
347+
shellPath = shellPath.toString().trim()
348+
349+
let vars = await this.vars(pure)
350+
vars = Object.assign(vars, {
351+
PS1: '☆ ' + chalk.green.bold(this.name) + ':' + chalk.blue('\\w') + '$ '
352+
})
353+
354+
const shellProcess = pty.spawn(shellPath, shellArgs, {
355+
name: 'xterm-color',
356+
cols: 120,
357+
rows: 30,
358+
env: vars
359+
})
360+
shellProcess.on('data', data => {
361+
process.stdout.write(data)
362+
})
363+
364+
// To prevent echoing of input set stdin to raw mode (see https://github.com/Microsoft/node-pty/issues/78)
365+
// https://nodejs.org/api/tty.html: "When in raw mode, input is always available character-by-character,
366+
// not including modifiers. Additionally, all special processing of characters
367+
// by the terminal is disabled, including echoing input characters. Note that CTRL+C
368+
// will no longer cause a SIGINT when in this mode."
369+
// @ts-ignore
370+
process.stdin.setRawMode(true)
371+
372+
// Write the result through to the shell process
373+
// Capture Ctrl+D for special handling:
374+
// - if in the top level shell process then exit this process
375+
// - otherwise, pass on the process e.g. node, Rrm
376+
const ctrlD = Buffer.from([4])
377+
process.stdin.on('data', data => {
378+
if (data.equals(ctrlD) && shellProcess.process == shellPath) {
379+
process.exit(1)
312380
}
381+
shellProcess.write(data)
313382
})
383+
384+
if (command) shellProcess.write(command + '\r')
314385
}
315386

316387
/**

src/cli.ts

+24
Original file line numberDiff line numberDiff line change
@@ -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

0 commit comments

Comments
 (0)