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

Commit eb5bdba

Browse files
committed
fix for buggy disambiguation of commands, plus usage models for host commands
Fixes #746 usage models for auth commands Fixes #748
1 parent 337b9a7 commit eb5bdba

File tree

10 files changed

+171
-28
lines changed

10 files changed

+171
-28
lines changed

app/content/js/command-tree.js

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,29 @@ const util = require('util'),
2626
interior = () => undefined, // this will trigger a re-parse using Context.current as the path prefix
2727
newTree = () => ({ $: root(), key: '/', route: '/', children: {}}),
2828
model = newTree(), // this is the model of registered listeners, a tree
29-
intentions = newTree(), // this is the model of registered intentional listeners
30-
disambiguator = {} // map from command name to disambiguations
29+
intentions = newTree() // this is the model of registered intentional listeners
30+
let disambiguator = {} // map from command name to disambiguations
3131

3232
debug('finished loading modules')
3333

3434
// for plugins.js
35-
exports.disambiguator = () => {
35+
exports.startScan = () => {
36+
const state = disambiguator
37+
disambiguator = {}
38+
debug('startScan', disambiguator)
39+
return state
40+
}
41+
exports.endScan = state => {
42+
debug('finishing up', disambiguator)
3643
const map = {}
3744
for (let command in disambiguator) {
3845
map[command] = disambiguator[command].map(({route, options}) => ({
3946
route, plugin: options && options.plugin
4047
}))
4148
}
49+
if (state) {
50+
disambiguator = state
51+
}
4252
return map
4353
}
4454

@@ -505,7 +515,7 @@ const read = (model, argv) => {
505515
*/
506516
const disambiguate = (argv, noRetry=false) => {
507517
let idx
508-
const resolutions = ( (((idx=0)||true) && disambiguator[argv[idx]]) || (((idx=argv.length-1)||true) && disambiguator[argv[idx]]) || []).filter(isFileFilter)
518+
const resolutions = ( (((idx=0)||true) && resolver.disambiguate(argv[idx])) || (((idx=argv.length-1)||true) && resolver.disambiguate(argv[idx])) || [])
509519
debug('disambiguate', idx, argv, resolutions)
510520

511521
if (resolutions.length === 0 && !noRetry) {
@@ -516,11 +526,26 @@ const disambiguate = (argv, noRetry=false) => {
516526

517527
} else if (resolutions.length === 1) {
518528
// one unambiguous resolution! great, but we need to
519-
// double-check: if the resolution is a subtree, then it better have a child that matches
520-
const leaf = resolutions[0]
529+
// double-check: if the resolution is a subtree, then it better have a child that matches
530+
const argvForMatch = resolutions[0].route.split('/').slice(1)
531+
const cmdMatch = treeMatch(model, argvForMatch),
532+
leaf = cmdMatch && cmdMatch.$ ? cmdMatch : treeMatch(intentions, argvForMatch)
533+
534+
debug('disambiguate single match', idx, argv, cmdMatch, leaf)
535+
536+
if (!leaf || !leaf.$) {
537+
if (!noRetry && resolutions[0].plugin) {
538+
debug('disambiguate attempting to resolve plugins 2')
539+
resolver.resolve(`/${argv.join('/')}`)
540+
return disambiguate(argv, true)
541+
} else {
542+
debug('disambiguate nope', intentions)
543+
return
544+
}
521545

522-
if (idx < argv.length - 1 && leaf.children) {
546+
} else if (idx < argv.length - 1 && leaf.children) {
523547
// then the match is indeed a subtree
548+
debug('validating disambiguation')
524549
let foundMatch = false
525550
const next = argv[argv.length - 1]
526551
for (let cmd in leaf.children) {
@@ -535,7 +560,7 @@ const disambiguate = (argv, noRetry=false) => {
535560
}
536561
}
537562

538-
debug('disambiguate success', leaf)
563+
debug(`disambiguate success ${leaf.route}`, leaf)
539564
return withEvents(leaf.$, leaf)
540565
}
541566
}

app/content/js/plugins.js

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
const debug = require('debug')('plugins')
18-
debug('starting')
18+
debug('loading')
1919

2020
const fs = require('fs'),
2121
path = require('path'),
@@ -323,13 +323,19 @@ const loadPrescan = userDataDir => {
323323
debug('loading prescan %s', prescanned)
324324

325325
return new Promise((resolve, reject) => {
326-
fs.readFile(prescanned, (err, data) => {
326+
fs.readFile(prescanned, (err, _data) => {
327327
debug('read done, any errors in the read? %s', !!err)
328328

329329
if (err) {
330330
reject(err)
331331
} else {
332-
resolve(JSON.parse(data.toString()))
332+
const data = _data.toString()
333+
if (data.trim().length === 0) {
334+
// it was empty
335+
resolve({})
336+
} else {
337+
resolve(JSON.parse(data))
338+
}
333339
}
334340
})
335341
})
@@ -393,8 +399,14 @@ const makeResolver = prescan => {
393399
const resolver = {
394400
isOverridden: route => prescan.overrides[route],
395401

402+
disambiguate: command => {
403+
debug('attempting to disambiguate command', command)
404+
return prescan.disambiguator[command]
405+
},
406+
396407
/** given a partial command, do we have a disambiguation of it? e.g. "gr" => "grid" */
397408
disambiguatePartial: partial => {
409+
debug('attempting to disambiguate partial', partial)
398410
const matches = []
399411
if (prescan.disambiguator) {
400412
for (let command in prescan.disambiguator) {
@@ -434,7 +446,9 @@ const makeResolver = prescan => {
434446
*
435447
*/
436448
exports.scan = opts => {
437-
debug('scan')
449+
debug('scan', opts)
450+
451+
const state = opts.externalOnly && commandTree.startScan()
438452

439453
return resolveFromLocalFilesystem(opts).then(() => {
440454
const disambiguator = {}
@@ -465,7 +479,8 @@ exports.scan = opts => {
465479
}
466480
}
467481
}
468-
return { commandToPlugin, topological, flat, overrides, usage, disambiguator: commandTree.disambiguator() }
482+
483+
return { commandToPlugin, topological, flat, overrides, usage, disambiguator: commandTree.endScan(state) }
469484
})
470485
}
471486

app/content/js/usage-error.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ const wrap = div => {
9292
*/
9393
const usageFromCommand = command => repl.qexec(command)
9494
.then(_ => {
95-
console.error('Invalid usage model', _)
95+
console.error('Invalid usage model', command, _)
9696
throw new Error('Internal Error')
9797
})
9898
.catch(usageError => usageError.raw)

app/plugins/modules/plugin/lib/commands.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,15 @@ const doList = (_a, _b, fullArgv, modules, rawCommandString, _2, argvWithoutOpti
5757
name,
5858
onclick: () => repl.pexec(name)
5959
})))
60+
.catch(err => {
61+
if (err.code === 'ENOENT') {
62+
const error = new Error('This plugin is not installed')
63+
error.code = 404
64+
throw error
65+
} else {
66+
throw err
67+
}
68+
})
6069
//success(false, `offered by the ${plugin} plugin`, commands))
6170
}
6271

app/plugins/modules/plugin/lib/compile.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ const writeToFile = (dir, modules) => new Promise((resolve, reject) => {
6161
*
6262
*/
6363
const readFile = dir => new Promise((resolve, reject) => {
64-
fs.readFile(prescanned(dir), (err, data) => {
64+
fs.readFile(prescanned(dir), (err, _data) => {
6565
if (err) {
6666
console.error(err.code)
6767
if (err.code === 'ENOENT') {
@@ -70,7 +70,18 @@ const readFile = dir => new Promise((resolve, reject) => {
7070
reject(err)
7171
}
7272
} else {
73-
resolve(JSON.parse(data.toString()))
73+
try {
74+
const data = _data.toString()
75+
if (!data || data.trim().length === 0) {
76+
// it was empty
77+
resolve({})
78+
} else {
79+
resolve(JSON.parse(data))
80+
}
81+
} catch (err) {
82+
console.error(err)
83+
resolve({})
84+
}
7485
}
7586
})
7687
})

app/plugins/modules/plugin/usage.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
const parents = [{ command: 'plugin' }]
1919

2020
/** required parameter: name of installed plugin */
21-
const installedPlugin = [{ name: 'plugin', docs: 'the name of an installed plugin' }]
21+
const installedPlugin = [{ name: 'plugin', docs: 'the name of an installed plugin', entity: 'plugin' }]
2222

2323
/**
2424
* Usage model for plugin commands
@@ -68,6 +68,7 @@ exports.list = {
6868
breadcrumb: 'List plugins',
6969
docs: 'list installed Shell plugins',
7070
example: 'plugin list',
71+
optional: [{ name: '--limit', hidden: true }], // to make tab completion happy
7172
parents
7273
}
7374

app/plugins/ui/commands/auth.js

Lines changed: 85 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,81 @@ const fs = require('fs'),
2727
expandHomeDir = require('expand-home-dir'),
2828
wskpropsFile = expandHomeDir('~/.wskprops')
2929

30+
const usage = {
31+
auth: {
32+
toplevel: {
33+
commandPrefix: 'auth',
34+
title: 'Authorization Operations',
35+
header: 'Commands to switch, list, and remember OpenWhisk authorization keys',
36+
related: ['host']
37+
},
38+
list: {
39+
strict: 'list',
40+
command: 'list',
41+
title: 'List Auth Keys',
42+
header: 'List the OpenWhisk namespaces for which you have authorization keys',
43+
parents: ['auth'],
44+
related: ['host set']
45+
},
46+
switch: {
47+
strict: 'switch',
48+
command: 'switch',
49+
title: 'Switch Auth Keys',
50+
header: 'Switch to use an OpenWhisk namespace, by name',
51+
example: 'auth switch <namespace>',
52+
required: [{ name: 'namespace', docs: 'an OpenWhisk namespace', entity: 'namespace' }],
53+
parents: ['auth'],
54+
related: ['auth list', 'host set']
55+
},
56+
add: {
57+
strict: 'add',
58+
command: 'add',
59+
title: 'Add Auth Key',
60+
header: 'Install an OpenWhisk authorization key',
61+
example: 'auth add <auth>',
62+
required: [{ name: 'auth', docs: 'an OpenWhisk authorization key' }],
63+
parents: ['auth'],
64+
related: ['auth switch', 'auth list', 'host set']
65+
}
66+
},
67+
host: {
68+
toplevel: {
69+
commandPrefix: 'host',
70+
title: 'Host Operations',
71+
header: 'Commands to switch OpenWhisk API host',
72+
related: ['auth']
73+
},
74+
get: {
75+
strict: 'get',
76+
command: 'get',
77+
commandPrefix: 'host',
78+
title: 'Get API Host',
79+
header: 'Print the current OpenWhisk API host',
80+
example: 'host get',
81+
parents: ['host']
82+
},
83+
set: {
84+
strict: 'set',
85+
command: 'set',
86+
commandPrefix: 'host',
87+
title: 'Set API Host',
88+
header: 'Change the OpenWhisk API host to a chosen alternative',
89+
example: 'host set <which>',
90+
nRowsInViewport: 5,
91+
oneof: [
92+
{ name: 'local', docs: 'Use a local OpenWhisk installation' },
93+
{ name: 'us-south', docs: 'Use the IBM Cloud Dallas installation' },
94+
{ name: 'eu-gb', docs: 'Use the IBM Cloud London installation' },
95+
{ name: 'eu-de', docs: 'Use the IBM Cloud Frankrut installation' },
96+
{ name: 'hostname', docs: 'Use a given hostname or IP address' }
97+
],
98+
parents: ['host']
99+
}
100+
}
101+
}
102+
usage.auth.toplevel.available = [usage.auth.add, usage.auth.list, usage.auth.switch]
103+
usage.host.toplevel.available = [usage.host.get, usage.host.set]
104+
30105
/**
31106
* The message we will use to inform the user of a auth switch event
32107
*
@@ -142,8 +217,8 @@ const use = (wsk, commandTree) => verb => (_1, _2, _3, _4, _5, _6, argv) => name
142217
module.exports = (commandTree, prequire) => {
143218
const wsk = prequire('/ui/commands/openwhisk-core')
144219

145-
commandTree.subtree('/host', { docs: 'Commands to switch OpenWhisk API host' })
146-
commandTree.subtree('/auth', { docs: 'Commands to switch, list, and remember OpenWhisk authorization keys' })
220+
commandTree.subtree('/host', { usage: usage.host.toplevel })
221+
commandTree.subtree('/auth', { usage: usage.auth.toplevel })
147222

148223
const clicky = (parent, cmd, exec = repl.pexec) => {
149224
const dom = document.createElement('span')
@@ -185,22 +260,22 @@ module.exports = (commandTree, prequire) => {
185260
}
186261
const add = (_1, _2, _3, _4, _5, _6, argv) => addFn(firstArg(argv, 'add'))
187262

188-
const listCmd = commandTree.listen('/auth/list', list, { docs: 'List the OpenWhisk authorization keys you have installed' })
263+
const listCmd = commandTree.listen('/auth/list', list, { usage: usage.auth.list })
189264
commandTree.synonym('/auth/ls', list, listCmd)
190265

191266
const useFn = use(wsk, commandTree)
192-
const useCmd = commandTree.listen('/auth/use', useFn('use'), { docs: 'Switch to use an OpenWhisk namespace, by name (hint: try auth ls first)' })
193-
commandTree.synonym('/auth/switch', useFn('switch'), useCmd)
267+
const useCmd = commandTree.listen('/auth/switch', useFn('switch'), { usage: usage.auth.switch })
268+
// commandTree.synonym('/auth/switch', useFn('switch'), useCmd)
194269
//commandTree.synonym('/auth/use', useFn, useCmd)
195270

196-
const addCmd = commandTree.listen('/auth/add', add, { docs: 'Install an OpenWhisk authorization key' })
197-
commandTree.synonym('/auth/install', use, addCmd)
271+
const addCmd = commandTree.listen('/auth/add', add, { usage: usage.auth.add })
272+
//commandTree.synonym('/auth/install', use, addCmd)
198273

199274
/**
200275
* OpenWhisk API host: get and set commands
201276
*
202277
*/
203-
commandTree.listen('/host/get', () => wsk.apiHost.get(), { docs: 'Print the current OpenWhisk API host' })
278+
commandTree.listen('/host/get', () => wsk.apiHost.get(), { usage: usage.host.get })
204279
commandTree.listen('/host/set',
205280
(_1, _2, _a, _3, _4, _5, argv_without_options, options) => {
206281
const argv = slice(argv_without_options, 'set')
@@ -241,7 +316,7 @@ module.exports = (commandTree, prequire) => {
241316
host = 'https://192.168.33.13'
242317
ignoreCerts = true
243318
isLocal = true
244-
} else if (host === 'local') {
319+
} else if (host === 'local' || host === 'localhost') {
245320
// try a variety of options
246321
const variants = [ 'https://192.168.33.13', 'http://172.17.0.1:10001', 'http://192.168.99.100:10001' ]
247322
const request = require('request')
@@ -324,7 +399,7 @@ module.exports = (commandTree, prequire) => {
324399
}
325400
})))
326401
},
327-
{ docs: 'Update the current OpenWhisk API host' })
402+
{ usage: usage.host.set })
328403

329404
return {
330405
add: addFn

tests/lib/common.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ exports.before = (ctx, {fuzz, noApp=false}={}) => {
5656
// pass WSKNG_DEBUG on to NODE_DEBUG for the application
5757
opts.env.NODE_DEBUG = process.env.WSKNG_NODE_DEBUG
5858
}
59+
if (process.env.DEBUG) {
60+
opts.env.DEBUG = process.env.DEBUG
61+
}
5962

6063
ctx.app = new Application(opts)
6164
}

tests/tests/passes/02/host.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ describe('host tests', function() {
3939

4040
it('should have an active repl', () => cli.waitForRepl(this.app))
4141

42+
it('should command not found on hosts set', () => cli.do('hosts set', this.app)
43+
.then(cli.expectError(0, 'Command not found'))
44+
.catch(common.oops(this)))
45+
4246
it('bogus host from default context', () => cli.do(`host set xxx`, this.app)
4347
.then(cli.expectOKWithCustom({selector: '', expect: `Before you can proceed, please provide an OpenWhisk auth key, using auth add <AUTH_KEY>` })))
4448

tests/tests/passes/04/auth.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ describe('auth tests', function() {
7070
.then(cli.expectContext(undefined, ''))) // don't care about command context (undefined), and selection must be empty ('')
7171

7272
// switch back to second namespace
73-
it('should switch to the second namespace, using the CLI use command', () => cli.do(`auth use ${ns2}`, this.app)
73+
it('should switch to the second namespace, using the CLI switch command', () => cli.do(`auth switch ${ns2}`, this.app)
7474
.then(cli.expectOKWithCustom({selector: '', expect: `namespace ${ns2}` })))
7575

7676
// list should show only foo2

0 commit comments

Comments
 (0)