Skip to content
This repository was archived by the owner on Jul 30, 2025. It is now read-only.

Commit b66a2fb

Browse files
myan9starpit
authored andcommitted
feat: cd command handles VFS mounts
Fixes #6988
1 parent 8e5a053 commit b66a2fb

File tree

16 files changed

+236
-167
lines changed

16 files changed

+236
-167
lines changed

packages/core/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ export {
248248

249249
// Util
250250
export { findFileWithViewer, findFile, isSpecialDirectory } from './core/find-file'
251-
export { expandHomeDir } from './util/home'
251+
export { expandHomeDir, cwd } from './util/home'
252252
export { flatten } from './core/utility'
253253
export { promiseEach } from './util/async'
254254
export { isHTML, isPromise } from './util/types'

packages/core/src/models/tab-state.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import Debug from 'debug'
1818

1919
import { WatchableJob } from '../core/jobs/job'
2020
import { inBrowser } from '../core/capabilities'
21+
import { cwd as kuiCwd } from '../util/home'
2122
import { eventBus, StatusStripeChangeEvent } from '../core/events'
2223

2324
const debug = Debug('core/models/TabState')
@@ -81,7 +82,7 @@ export default class TabState {
8182
/** Capture contextual global state */
8283
public capture() {
8384
this._env = Object.assign({}, process.env)
84-
this._cwd = inBrowser() ? process.env.PWD : process.cwd().slice(0) // just in case, copy the string
85+
this._cwd = kuiCwd()
8586

8687
debug('captured tab state', this.uuid, this.cwd)
8788
}
@@ -196,7 +197,7 @@ export default class TabState {
196197
process.env = this._env
197198

198199
if (this._cwd !== undefined) {
199-
if (inBrowser()) {
200+
if (inBrowser() || process.env.VIRTUAL_CWD) {
200201
debug('changing cwd', process.env.PWD, this._cwd)
201202
process.env.PWD = this._cwd
202203
} else {

packages/core/src/util/home.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
import { join } from 'path'
1818
import { homedir as home } from 'os'
1919

20+
import { inBrowser } from '../core/capabilities'
21+
2022
const homedir = home()
2123

2224
export const expandHomeDir = function(path: string): string {
@@ -32,3 +34,5 @@ export const expandHomeDir = function(path: string): string {
3234
}
3335

3436
export default expandHomeDir
37+
38+
export const cwd = () => process.env.VIRTUAL_CWD || (inBrowser() ? process.env.PWD || '/' : process.cwd().slice(0))
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*
2+
* Copyright 2017-21 IBM Corporation
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* This plugin introduces commands that dispatch to a local bash-like
19+
* shell
20+
*
21+
*/
22+
23+
import Debug from 'debug'
24+
import { join } from 'path'
25+
26+
import { cwd, inBrowser, Arguments, Registrar, i18n, expandHomeDir } from '@kui-shell/core'
27+
import { localFilepath } from './usage-helpers'
28+
import { findMount } from '../vfs'
29+
30+
const strings = i18n('plugin-bash-like')
31+
const debug = Debug('plugins/bash-like/cmds/general')
32+
33+
const usage = {
34+
cd: {
35+
strict: 'cd',
36+
command: 'cd',
37+
title: strings('cdUsageTitle'),
38+
header: strings('cdUsageHeader'),
39+
optional: localFilepath
40+
}
41+
}
42+
43+
/**
44+
* cd command
45+
*
46+
*/
47+
const cd = async (args: Arguments) => {
48+
const dirAsProvided = args.REPL.split(args.command, true, true)[1] || ''
49+
if (dirAsProvided === '-' && !process.env.OLDPWD) {
50+
throw new Error(`cd: not a directory: ${dirAsProvided}`)
51+
}
52+
53+
const dir = !dirAsProvided ? expandHomeDir('~') : dirAsProvided === '-' ? process.env.OLDPWD : dirAsProvided
54+
55+
const mount = findMount(dir)
56+
try {
57+
const resolveDir =
58+
mount.isLocal || dirAsProvided === '-' ? dir : process.env.VIRTUAL_CWD ? join(process.env.VIRTUAL_CWD, dir) : dir
59+
debug('cd dir', resolveDir)
60+
const stat = await mount.fstat(args, resolveDir)
61+
62+
if (stat.isDirectory) {
63+
if (process.env.OLDPWD === undefined) {
64+
process.env.OLDPWD = ''
65+
}
66+
67+
const OLDPWD = cwd() // remember it for when we're done
68+
const newDir = expandHomeDir(stat.fullpath)
69+
70+
if (mount.isLocal && !inBrowser()) {
71+
process.chdir(newDir)
72+
}
73+
74+
process.env.OLDPWD = OLDPWD
75+
process.env.PWD = newDir
76+
if (mount.isLocal) {
77+
delete process.env.VIRTUAL_CWD
78+
} else {
79+
process.env.VIRTUAL_CWD = newDir
80+
}
81+
82+
if (args.tab.state) {
83+
args.tab.state.capture()
84+
}
85+
return newDir
86+
} else {
87+
throw new Error(`cd: not a directory: ${dirAsProvided}`)
88+
}
89+
} catch (err) {
90+
if (err.message && err.message.includes('no such file or directory')) {
91+
throw new Error(`cd: no such file or directory: ${dirAsProvided}`)
92+
} else {
93+
throw err
94+
}
95+
}
96+
}
97+
98+
const bcd = async ({ command, execOptions, REPL }: Arguments) => {
99+
const pwd: string = await REPL.qexec(command.replace(/^cd/, 'kuicd'), undefined, undefined, execOptions)
100+
process.env.PWD = pwd
101+
return pwd
102+
}
103+
104+
/**
105+
* Register command handlers
106+
*
107+
*/
108+
export default (commandTree: Registrar) => {
109+
commandTree.listen('/kuicd', cd, {
110+
requiresLocal: true
111+
})
112+
113+
if (!inBrowser()) {
114+
commandTree.listen('/cd', cd, {
115+
usage: usage.cd
116+
})
117+
} else {
118+
commandTree.listen('/cd', bcd, {
119+
usage: usage.cd
120+
})
121+
}
122+
}

plugins/plugin-bash-like/fs/src/plugin.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17+
import cd from './lib/cd'
1718
import ls from './lib/ls'
1819
import edit from './lib/edit'
1920
import open from './lib/open'
@@ -30,6 +31,7 @@ import { Registrar } from '@kui-shell/core'
3031
*/
3132
export default async (registrar: Registrar) => {
3233
await Promise.all([
34+
cd(registrar),
3335
ls(registrar),
3436
edit(registrar),
3537
open(registrar),

plugins/plugin-bash-like/fs/src/vfs/index.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import Debug from 'debug'
1818
import slash from 'slash'
1919
import { isAbsolute, join } from 'path'
20-
import { Arguments, ParsedOptions, REPL, Table, eventBus, getCurrentTab, inBrowser } from '@kui-shell/core'
20+
import { cwd, Arguments, ParsedOptions, REPL, Table, eventBus, getCurrentTab, inBrowser } from '@kui-shell/core'
2121

2222
import { FStat } from '../lib/fstat'
2323
import { KuiGlobOptions, GlobStats } from '../lib/glob'
@@ -181,11 +181,6 @@ export async function mount(vfs: VFS | VFSProducingFunction) {
181181
}
182182
}
183183

184-
/** @return current working directory */
185-
function cwd() {
186-
return inBrowser() ? process.env.PWD || '/' : process.cwd()
187-
}
188-
189184
/** @return the absolute path to `filepath` */
190185
function absolute(filepath: string): string {
191186
return isAbsolute(filepath) ? filepath : join(cwd(), filepath)

plugins/plugin-bash-like/src/lib/cmds/bash-like.ts

Lines changed: 2 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,12 @@
2323
import Debug from 'debug'
2424
import { exec, ExecOptions as ChildProcessExecOptions } from 'child_process'
2525

26-
import { inBrowser, Arguments, ExecOptions, ExecType, Registrar, i18n } from '@kui-shell/core'
26+
import { Arguments, ExecOptions, ExecType, Registrar } from '@kui-shell/core'
2727

2828
import { handleNonZeroExitCode } from '../util/exec'
2929
import { extractJSON } from '../util/json'
30-
import { localFilepath } from '../util/usage-helpers'
3130
import { dispatchToShell } from './catchall'
3231

33-
const strings = i18n('plugin-bash-like')
3432
const debug = Debug('plugins/bash-like/cmds/general')
3533

3634
export const doExec = (
@@ -121,128 +119,6 @@ export const doExec = (
121119
})
122120
})
123121

124-
const doShell = (argv: string[], args: Arguments) =>
125-
// eslint-disable-next-line no-async-promise-executor
126-
new Promise(async (resolve, reject) => {
127-
const { execOptions, REPL } = args
128-
129-
if (inBrowser()) {
130-
reject(new Error('Local file access not supported when running in a browser'))
131-
}
132-
133-
// purposefully imported lazily, so that we don't spoil browser mode (where shell is not available)
134-
const shell = await import('shelljs')
135-
136-
if (argv.length < 2) {
137-
reject(new Error('Please provide a bash command'))
138-
}
139-
140-
const cmd = argv[1]
141-
debug('argv', argv)
142-
debug('cmd', cmd)
143-
144-
// shell.echo prints the the outer console, which we don't want
145-
if (shell[cmd] && (inBrowser() || (cmd !== 'mkdir' && cmd !== 'echo'))) {
146-
const args = argv.slice(2)
147-
148-
// remember OLDPWD, so that `cd -` works (shell issue #78)
149-
if (process.env.OLDPWD === undefined) {
150-
process.env.OLDPWD = ''
151-
}
152-
const OLDPWD = shell.pwd() // remember it for when we're done
153-
if (cmd === 'cd' && args[0] === '-') {
154-
// special case for "cd -"
155-
args[0] = process.env.OLDPWD
156-
}
157-
158-
// see if we should use the built-in shelljs support
159-
if (
160-
!args.find(arg => arg.charAt(0) === '-') && // any options? then no
161-
!args.find(arg => arg === '>') && // redirection? then no
162-
cmd !== 'ls'
163-
) {
164-
// shelljs doesn't like dash args
165-
// otherwise, shelljs has a built-in handler for this
166-
167-
debug('using internal shelljs', cmd, args)
168-
169-
const output = shell[cmd](args)
170-
if (cmd === 'cd') {
171-
const newDir = shell.pwd().toString()
172-
process.env.OLDPWD = OLDPWD
173-
process.env.PWD = newDir
174-
175-
if (output.code === 0) {
176-
// special case: if the user asked to change working
177-
// directory, respond with the new working directory
178-
resolve(shell.pwd().toString())
179-
} else {
180-
reject(new Error(output.stderr))
181-
}
182-
} else {
183-
// otherwise, respond with the output of the command;
184-
if (output && output.length > 0) {
185-
if (execOptions && execOptions['json']) {
186-
resolve(JSON.parse(output))
187-
} else {
188-
resolve(output.toString())
189-
}
190-
} else {
191-
resolve(true)
192-
}
193-
}
194-
}
195-
}
196-
197-
//
198-
// otherwise, we use exec to implement the shell command; here, we
199-
// cross our fingers that the platform implements the requested
200-
// command
201-
//
202-
const rest = argv.slice(1) // skip over '!'
203-
const cmdLine = rest.map(_ => REPL.encodeComponent(_)).join(' ')
204-
debug('cmdline', cmdLine, rest)
205-
206-
doExec(cmdLine, execOptions).then(resolve, reject)
207-
})
208-
209-
const usage = {
210-
cd: {
211-
strict: 'cd',
212-
command: 'cd',
213-
title: strings('cdUsageTitle'),
214-
header: strings('cdUsageHeader'),
215-
optional: localFilepath
216-
}
217-
}
218-
219-
/**
220-
* cd command
221-
*
222-
*/
223-
const cd = (args: Arguments) => {
224-
const dir = args.REPL.split(args.command, true, true)[1] || ''
225-
debug('cd dir', dir)
226-
return doShell(['!', 'cd', dir], args)
227-
.then(newDir => {
228-
if (args.tab.state) {
229-
args.tab.state.capture()
230-
}
231-
return newDir
232-
})
233-
.catch(err => {
234-
err['code'] = 500
235-
throw err
236-
})
237-
}
238-
239-
const bcd = async ({ command, execOptions, REPL }: Arguments) => {
240-
const pwd: string = await REPL.qexec(command.replace(/^cd/, 'kuicd'), undefined, undefined, execOptions)
241-
debug('pwd', pwd)
242-
process.env.PWD = pwd
243-
return pwd
244-
}
245-
246122
const specialHandler = (args: Arguments) => {
247123
if (args.execOptions.type === ExecType.TopLevel) {
248124
throw new Error('this command is intended for internal consumption only')
@@ -265,17 +141,5 @@ export default (commandTree: Registrar) => {
265141
hidden: true
266142
})
267143

268-
commandTree.listen('/kuicd', cd, {
269-
requiresLocal: true
270-
})
271-
272-
if (!inBrowser()) {
273-
commandTree.listen('/cd', cd, {
274-
usage: usage.cd
275-
})
276-
} else {
277-
commandTree.listen('/cd', bcd, {
278-
usage: usage.cd
279-
})
280-
}
144+
commandTree.listen('/pwd', () => process.env.PWD)
281145
}

plugins/plugin-bash-like/src/test/bash-like/cd.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ describe(`bash-like cd ${process.env.MOCHA_RUN_TARGET || ''}`, function(this: Co
3535
CLI.command('pwd', this.app)
3636
.then(ReplExpect.okWithAny)
3737
.then(async () => {
38-
initialDirectory = await this.app.client.$(Selectors.OUTPUT_LAST_PTY).then(_ => _.getText())
38+
initialDirectory = await this.app.client.$(Selectors.OUTPUT_LAST).then(_ => _.getText())
3939
})
4040
.catch(Common.oops(this, true))
4141
)

plugins/plugin-bash-like/src/test/bash-like/export.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ describe('export command', function(this: Common.ISuite) {
4242

4343
return CLI.command('cd', this.app)
4444
.then(ReplExpect.okWithAny)
45-
.then(() => CLI.command('pwd', this.app).then(ReplExpect.okWithPtyOutput(process.env.HOME)))
45+
.then(() => CLI.command('pwd', this.app).then(ReplExpect.okWithString(process.env.HOME)))
4646
.then(() => CLI.command('printenv HOME', this.app).then(ReplExpect.okWithPtyOutput(home)))
4747
.catch(Common.oops(this))
4848
})

0 commit comments

Comments
 (0)