-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathcommands.ts
173 lines (147 loc) · 5.57 KB
/
commands.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
import {Command, Flags, toConfiguredId} from '@oclif/core'
import {printTable} from '@oclif/table'
import _ from 'lodash'
// @ts-expect-error because object-treeify does not have types: https://github.com/blackflux/object-treeify/issues/1077
import treeify from 'object-treeify'
type Dictionary = {[index: string]: object}
const COLUMNS = ['id', 'plugin', 'summary', 'type'] as const
type Column = (typeof COLUMNS)[number]
interface RecursiveTree {
[key: string]: RecursiveTree | string
}
function createTree(commands: Command.Loadable[]): RecursiveTree {
const tree: RecursiveTree = {}
for (const command of commands) {
const parts = command.id.split(':')
let current = tree
for (const part of parts) {
current[part] = current[part] || {}
current = current[part] as RecursiveTree
}
}
return tree
}
function mergePrototype(result: Command.Class, command: Command.Class): Command.Class {
const proto = Object.getPrototypeOf(command)
const filteredProto = _.pickBy(proto, (v) => v !== undefined) as Command.Class
return Object.keys(proto).length > 0 ? mergePrototype({...filteredProto, ...result} as Command.Class, proto) : result
}
export default class Commands extends Command {
static description = 'List all <%= config.bin %> commands.'
static enableJsonFlag = true
static flags = {
columns: Flags.custom<Column>({
char: 'c',
delimiter: ',',
description: 'Only show provided columns (comma-separated).',
exclusive: ['tree'],
multiple: true,
options: COLUMNS,
})(),
deprecated: Flags.boolean({description: 'Show deprecated commands.'}),
extended: Flags.boolean({char: 'x', description: 'Show extra columns.', exclusive: ['tree']}),
hidden: Flags.boolean({description: 'Show hidden commands.'}),
'no-truncate': Flags.boolean({description: 'Do not truncate output.', exclusive: ['tree']}),
sort: Flags.option({
default: 'id',
description: 'Property to sort by.',
exclusive: ['tree'],
options: COLUMNS,
})(),
tree: Flags.boolean({description: 'Show tree of commands.'}),
}
async run() {
const {flags} = await this.parse(Commands)
let commands = this.getCommands()
if (!flags.hidden) {
commands = commands.filter((c) => !c.hidden)
}
if (!flags.deprecated) {
const deprecatedAliases = new Set(commands.filter((c) => c.deprecateAliases).flatMap((c) => c.aliases))
commands = commands.filter((c) => c.state !== 'deprecated' && !deprecatedAliases.has(c.id))
}
const {config} = this
commands = _.sortBy(commands, flags.sort).map((command) =>
// Template supported fields.
({
...command,
description:
(typeof command.description === 'string' && _.template(command.description)({command, config})) || undefined,
summary: (typeof command.summary === 'string' && _.template(command.summary)({command, config})) || undefined,
usage: (typeof command.usage === 'string' && _.template(command.usage)({command, config})) || undefined,
}),
)
if (flags.tree) {
const tree = createTree(commands)
this.log(treeify(tree))
} else if (!this.jsonEnabled()) {
printTable({
borderStyle: 'vertical-with-outline',
columns: (flags.columns ?? ['id', 'summary', ...(flags.extended ? ['plugin', 'type'] : [])]) as Column[],
data: commands.map((c) => ({
id: toConfiguredId(c.id, config),
plugin: c.pluginName,
summary: c.summary ?? c.description,
type: c.pluginType,
})),
headerOptions: {
formatter: 'capitalCase',
},
overflow: flags['no-truncate'] ? 'wrap' : 'truncate',
sort: {[flags.sort]: 'asc'},
})
}
const json = _.uniqBy(
await Promise.all(
commands.map(async (cmd) => {
let commandClass: Command.Class
try {
commandClass = await cmd.load()
} catch (error) {
this.debug(error)
return cmd
}
const obj = {...mergePrototype(commandClass, commandClass), ...cmd}
// The plugin property on the loaded class contains a LOT of information including all the commands again. Remove it.
delete obj.plugin
// If Command classes have circular references, don't break the commands command.
return this.removeCycles(obj)
}),
),
'id',
)
return json
}
private getCommands() {
return this.config.commands
}
private removeCycles(object: unknown) {
// Keep track of seen objects.
const seenObjects = new WeakMap<Dictionary, undefined>()
const _removeCycles = (obj: unknown) => {
// Use object prototype to get around type and null checks
if (Object.prototype.toString.call(obj) === '[object Object]') {
// We know it is a "Dictionary" because of the conditional
const dictionary = obj as Dictionary
// Seen, return undefined to remove.
if (seenObjects.has(dictionary)) return
seenObjects.set(dictionary, undefined)
for (const key in dictionary) {
// Delete the duplicate object if cycle found.
if (_removeCycles(dictionary[key]) === undefined) {
delete dictionary[key]
}
}
} else if (Array.isArray(obj)) {
for (const i in obj) {
if (_removeCycles(obj[i]) === undefined) {
// We don't want to delete the array, but we can replace the element with null.
obj[i] = null
}
}
}
return obj
}
return _removeCycles(object)
}
}