omakit 0.1.9 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -217
- package/package.json +2 -2
- package/skills/omarchy-plugin-check/SKILL.md +102 -0
- package/skills/omarchy-plugin-submit/SKILL.md +4 -0
- package/skills/omarchy-plugin-weigh/SKILL.md +128 -0
- package/tools/marketplace/README.md +18 -1
- package/tools/marketplace/cli.mjs +165 -8
- package/tools/marketplace/completion-check.mjs +224 -0
- package/tools/marketplace/completion.mjs +103 -25
- package/tools/marketplace/doctor.mjs +9 -1
- package/tools/marketplace/options.mjs +72 -0
- package/tools/marketplace/paths.mjs +13 -0
- package/tools/marketplace/report.mjs +3 -2
- package/tools/marketplace/setup.mjs +90 -24
- package/tools/marketplace/upgrade.mjs +31 -5
- package/tools/marketplace/usage.mjs +30 -4
- package/tools/weigh/audit.mjs +666 -0
- package/tools/weigh/commands.mjs +69 -0
- package/tools/weigh/config.mjs +131 -0
- package/tools/weigh/confirm.mjs +32 -0
- package/tools/weigh/contract.mjs +167 -0
- package/tools/weigh/list.mjs +90 -0
- package/tools/weigh/proc.mjs +150 -0
- package/tools/weigh/report.mjs +196 -0
- package/tools/weigh/stats.mjs +63 -0
|
@@ -18,8 +18,8 @@ import { withHomeAbbreviated } from "./paths.mjs"
|
|
|
18
18
|
/**
|
|
19
19
|
* The completion model, read out of the help data. A subcommand is the word
|
|
20
20
|
* after `omakit` in its signature; a flag is every `--word` in it, valued when
|
|
21
|
-
* a `<placeholder>` follows it; `<target>`
|
|
22
|
-
* positional
|
|
21
|
+
* a `<placeholder>` follows it; `<target>` or `<plugin-id-or-dir>` in the
|
|
22
|
+
* signature means the first positional can be a directory.
|
|
23
23
|
*
|
|
24
24
|
* @param {ReadonlyArray<{ signature: string|string[], lines: string[] }>} commands
|
|
25
25
|
*/
|
|
@@ -28,15 +28,36 @@ export function subcommandsOf(commands = COMMANDS) {
|
|
|
28
28
|
const signature = [].concat(command.signature).join(" ")
|
|
29
29
|
const name = signature.match(/^omakit +([a-z][a-z-]*)/)?.[1]
|
|
30
30
|
if (!name) throw new Error(`completion: no subcommand in signature ${JSON.stringify(signature)}`)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
// A flag named on two signature lines (`--json` on weigh's main line and
|
|
32
|
+
// on its `--list` line) is one flag.
|
|
33
|
+
const seen = new Set()
|
|
34
|
+
const flags = [...signature.matchAll(/(--[a-z][a-z-]*)(?: <([^>]+)>)?/g)]
|
|
35
|
+
.filter(([, flag]) => !seen.has(flag) && seen.add(flag))
|
|
36
|
+
.map(([, flag, placeholder]) => ({
|
|
37
|
+
flag,
|
|
38
|
+
value: placeholder ? placeholderKind(placeholder) : null,
|
|
39
|
+
}))
|
|
35
40
|
const sentence = command.lines.join(" ").replace(/`/g, "").split(/(?<=\.)\s/)[0]
|
|
36
|
-
|
|
41
|
+
// `<target>` completes as a directory; `<plugin-id-or-dir>` as the ids
|
|
42
|
+
// the running shell has installed, read at TAB time, with a directory
|
|
43
|
+
// as the fallback.
|
|
44
|
+
const target = /<plugin-id-or-dir>/.test(signature) ? "plugin" : /<target>/.test(signature) ? "directory" : false
|
|
45
|
+
return { name, description: sentence, flags, target }
|
|
37
46
|
})
|
|
38
47
|
}
|
|
39
48
|
|
|
49
|
+
/**
|
|
50
|
+
* The plugin ids a TAB offers for `omakit weigh <TAB>`: what the running
|
|
51
|
+
* shell reports through `omarchy-shell shell listPlugins`, filtered with
|
|
52
|
+
* `jq` at TAB time, enabled ids first and whole bars left out, since a bar
|
|
53
|
+
* cannot be weighed. No node process behind the TAB: a shell that does not
|
|
54
|
+
* answer within a second yields nothing, and the script falls back to a
|
|
55
|
+
* directory. The pipeline is the same in every shell's script and the jq
|
|
56
|
+
* expression is exported so a test can run it through jq.
|
|
57
|
+
*/
|
|
58
|
+
export const PLUGIN_IDS_JQ = "[.[] | select(((.kinds // []) | index(\"bar\")) | not)] | sort_by(.enabled | not) | .[].id"
|
|
59
|
+
export const PLUGIN_IDS_COMMAND = `timeout 1 omarchy-shell shell listPlugins 2>/dev/null | jq -r '${PLUGIN_IDS_JQ}' 2>/dev/null`
|
|
60
|
+
|
|
40
61
|
/** What a valued flag takes, by its placeholder: a controlled value, a file, or free text. */
|
|
41
62
|
function placeholderKind(placeholder) {
|
|
42
63
|
if (placeholder === "c") return "category"
|
|
@@ -50,32 +71,59 @@ function placeholderKind(placeholder) {
|
|
|
50
71
|
* @param {{ contract: { categories: string[], tagLabels: string[] }, pin: string, commands?: typeof COMMANDS }} options
|
|
51
72
|
* @returns {string} the script
|
|
52
73
|
*/
|
|
53
|
-
export function renderCompletion(shell, { contract, pin, commands = COMMANDS }) {
|
|
74
|
+
export function renderCompletion(shell, { contract, pin, version = "unknown", commands = COMMANDS }) {
|
|
54
75
|
if (!COMPLETION_SHELLS.includes(shell)) throw new Error(`completion: no script for ${shell}`)
|
|
55
76
|
const model = {
|
|
56
77
|
subcommands: subcommandsOf(commands),
|
|
57
78
|
categories: [...contract.categories],
|
|
58
79
|
tags: contract.tagLabels.map(tagSlug),
|
|
59
80
|
pin,
|
|
81
|
+
version,
|
|
60
82
|
}
|
|
61
83
|
return { bash, zsh, fish }[shell](model)
|
|
62
84
|
}
|
|
63
85
|
|
|
64
|
-
|
|
86
|
+
/**
|
|
87
|
+
* The first line of every script names the omakit version and the pin it
|
|
88
|
+
* was rendered from, in one line, so the run-time staleness check
|
|
89
|
+
* (completion-check.mjs) reads one line and nothing else. The pin is what
|
|
90
|
+
* decides the categories and tags; the version is what decides the
|
|
91
|
+
* subcommands and flags, and a script from 0.1.9 knows no `weigh`.
|
|
92
|
+
*/
|
|
93
|
+
function header(comment, shell, pin, version) {
|
|
65
94
|
return [
|
|
66
|
-
`${comment} omakit completion for ${shell}
|
|
67
|
-
`${comment}
|
|
68
|
-
`${comment}
|
|
69
|
-
`${comment} again.`,
|
|
95
|
+
`${comment} omakit completion for ${shell}, omakit ${version}, marketplace pin ${pin}.`,
|
|
96
|
+
`${comment} Generated by \`omakit setup\`; the categories and tags below are that pin's`,
|
|
97
|
+
`${comment} submission form and the commands are that version's. Regenerate it by`,
|
|
98
|
+
`${comment} running setup again.`,
|
|
70
99
|
]
|
|
71
100
|
}
|
|
72
101
|
|
|
102
|
+
/**
|
|
103
|
+
* The version and the pin a script names, from its first line (its second
|
|
104
|
+
* for zsh, under `#compdef`), or null when
|
|
105
|
+
* the line is not one omakit wrote (a script from before the version was
|
|
106
|
+
* recorded reads as version null and its pin from the old header).
|
|
107
|
+
*
|
|
108
|
+
* @param {string} text the first two lines are enough
|
|
109
|
+
* @returns {{ shell: string|null, version: string|null, pin: string|null }}
|
|
110
|
+
*/
|
|
111
|
+
export function parseCompletionHeader(text) {
|
|
112
|
+
// zsh's script starts with its `#compdef` line; the header is the next.
|
|
113
|
+
const lines = String(text).split("\n", 2)
|
|
114
|
+
const line = lines[0].startsWith("#compdef") ? lines[1] || "" : lines[0]
|
|
115
|
+
const current = line.match(/^# omakit completion for (\w+), omakit (\S+), marketplace pin ([0-9a-f]{40})\.$/)
|
|
116
|
+
if (current) return { shell: current[1], version: current[2], pin: current[3] }
|
|
117
|
+
const older = line.match(/^#\s*omakit completion for (\w+)\./)
|
|
118
|
+
return { shell: older ? older[1] : null, version: null, pin: null }
|
|
119
|
+
}
|
|
120
|
+
|
|
73
121
|
const single = (text) => `'${String(text).replace(/'/g, "'\\''")}'`
|
|
74
122
|
|
|
75
123
|
// --- bash ---------------------------------------------------------------------
|
|
76
124
|
|
|
77
|
-
function bash({ subcommands, categories, tags, pin }) {
|
|
78
|
-
const lines = [...header("#", "bash", pin), ""]
|
|
125
|
+
function bash({ subcommands, categories, tags, pin, version }) {
|
|
126
|
+
const lines = [...header("#", "bash", pin, version), ""]
|
|
79
127
|
lines.push("_omakit() {")
|
|
80
128
|
lines.push(" local cur prev command")
|
|
81
129
|
lines.push(" cur=${COMP_WORDS[COMP_CWORD]}")
|
|
@@ -109,7 +157,12 @@ function bash({ subcommands, categories, tags, pin }) {
|
|
|
109
157
|
lines.push(" return")
|
|
110
158
|
lines.push(" fi")
|
|
111
159
|
}
|
|
112
|
-
if (sub.target) {
|
|
160
|
+
if (sub.target === "plugin") {
|
|
161
|
+
lines.push(' if ((COMP_CWORD == 2)); then')
|
|
162
|
+
lines.push(' COMPREPLY=($(compgen -W "$(_omakit_plugin_ids)" -- "$cur"))')
|
|
163
|
+
lines.push(' if ((${#COMPREPLY[@]} == 0)); then COMPREPLY=($(compgen -d -- "$cur")); compopt -o filenames 2>/dev/null; fi')
|
|
164
|
+
lines.push(" fi")
|
|
165
|
+
} else if (sub.target) {
|
|
113
166
|
lines.push(' COMPREPLY=($(compgen -d -- "$cur"))')
|
|
114
167
|
lines.push(" compopt -o filenames 2>/dev/null")
|
|
115
168
|
}
|
|
@@ -118,6 +171,13 @@ function bash({ subcommands, categories, tags, pin }) {
|
|
|
118
171
|
lines.push(" esac")
|
|
119
172
|
lines.push("}")
|
|
120
173
|
lines.push("")
|
|
174
|
+
lines.push("# The plugin ids the running shell has installed, enabled first, whole bars")
|
|
175
|
+
lines.push("# left out, read at TAB time; nothing when the shell does not answer in a")
|
|
176
|
+
lines.push("# second, and the caller falls back to a directory.")
|
|
177
|
+
lines.push("_omakit_plugin_ids() {")
|
|
178
|
+
lines.push(` ${PLUGIN_IDS_COMMAND}`)
|
|
179
|
+
lines.push("}")
|
|
180
|
+
lines.push("")
|
|
121
181
|
lines.push("# A controlled value may contain a space, so each match is one line and is")
|
|
122
182
|
lines.push("# escaped on the way out, the way the shell would have to type it.")
|
|
123
183
|
lines.push("_omakit_values() {")
|
|
@@ -142,8 +202,8 @@ function bash({ subcommands, categories, tags, pin }) {
|
|
|
142
202
|
|
|
143
203
|
const zshDescribe = (name, description) => single(`${name}:${description.replace(/:/g, "\\:")}`)
|
|
144
204
|
|
|
145
|
-
function zsh({ subcommands, categories, tags, pin }) {
|
|
146
|
-
const lines = ["#compdef omakit", ...header("#", "zsh", pin), ""]
|
|
205
|
+
function zsh({ subcommands, categories, tags, pin, version }) {
|
|
206
|
+
const lines = ["#compdef omakit", ...header("#", "zsh", pin, version), ""]
|
|
147
207
|
lines.push("_omakit() {")
|
|
148
208
|
lines.push(" local curcontext=\"$curcontext\" state line")
|
|
149
209
|
lines.push(" typeset -A opt_args")
|
|
@@ -169,7 +229,8 @@ function zsh({ subcommands, categories, tags, pin }) {
|
|
|
169
229
|
if (value === "text") return single(`${flag}:text:`)
|
|
170
230
|
return single(flag)
|
|
171
231
|
})
|
|
172
|
-
if (sub.target) specs.push(single("1:
|
|
232
|
+
if (sub.target === "plugin") specs.push(single("1:plugin:_omakit_plugins"))
|
|
233
|
+
else if (sub.target) specs.push(single("1:target:_directories"))
|
|
173
234
|
if (specs.length) lines.push(` _arguments ${specs.join(" ")}`)
|
|
174
235
|
lines.push(" ;;")
|
|
175
236
|
}
|
|
@@ -178,6 +239,14 @@ function zsh({ subcommands, categories, tags, pin }) {
|
|
|
178
239
|
lines.push(" esac")
|
|
179
240
|
lines.push("}")
|
|
180
241
|
lines.push("")
|
|
242
|
+
lines.push("# The plugin ids the running shell has installed, enabled first, whole bars")
|
|
243
|
+
lines.push("# left out, read at TAB time; a directory when the shell does not answer.")
|
|
244
|
+
lines.push("_omakit_plugins() {")
|
|
245
|
+
lines.push(" local -a ids")
|
|
246
|
+
lines.push(` ids=(\${(f)"$(${PLUGIN_IDS_COMMAND})"})`)
|
|
247
|
+
lines.push(" if (( ${#ids} )); then compadd -a ids; else _directories; fi")
|
|
248
|
+
lines.push("}")
|
|
249
|
+
lines.push("")
|
|
181
250
|
lines.push('_omakit "$@"')
|
|
182
251
|
return `${lines.join("\n")}\n`
|
|
183
252
|
}
|
|
@@ -186,14 +255,23 @@ function zsh({ subcommands, categories, tags, pin }) {
|
|
|
186
255
|
|
|
187
256
|
const fishWord = (text) => String(text).replace(/([\\'" ])/g, "\\$1")
|
|
188
257
|
|
|
189
|
-
function fish({ subcommands, categories, tags, pin }) {
|
|
190
|
-
const lines = [...header("#", "fish", pin), ""]
|
|
258
|
+
function fish({ subcommands, categories, tags, pin, version }) {
|
|
259
|
+
const lines = [...header("#", "fish", pin, version), ""]
|
|
191
260
|
lines.push("complete -c omakit -f")
|
|
261
|
+
lines.push("")
|
|
262
|
+
lines.push("# The plugin ids the running shell has installed, enabled first, whole bars")
|
|
263
|
+
lines.push("# left out, read at TAB time; nothing when the shell does not answer, and")
|
|
264
|
+
lines.push("# the directories offered beside them stand.")
|
|
265
|
+
lines.push("function __omakit_plugin_ids")
|
|
266
|
+
lines.push(` ${PLUGIN_IDS_COMMAND}`)
|
|
267
|
+
lines.push("end")
|
|
268
|
+
lines.push("")
|
|
192
269
|
for (const sub of subcommands) {
|
|
193
270
|
lines.push(`complete -c omakit -n __fish_use_subcommand -a ${sub.name} -d ${single(sub.description)}`)
|
|
194
271
|
}
|
|
195
272
|
for (const sub of subcommands) {
|
|
196
273
|
const when = `-n ${single(`__fish_seen_subcommand_from ${sub.name}`)}`
|
|
274
|
+
if (sub.target === "plugin") lines.push(`complete -c omakit ${when} -a '(__omakit_plugin_ids)'`)
|
|
197
275
|
if (sub.target) lines.push(`complete -c omakit ${when} -a '(__fish_complete_directories)'`)
|
|
198
276
|
for (const { flag, value } of sub.flags) {
|
|
199
277
|
const long = `-l ${flag.slice(2)}`
|
|
@@ -243,13 +321,13 @@ export function completionInstall(env = process.env) {
|
|
|
243
321
|
* the completion script at the path `completionInstall` names, and
|
|
244
322
|
* tests/unit/self-containment.test.mjs holds it to that.
|
|
245
323
|
*
|
|
246
|
-
* @param {{ contract: { categories: string[], tagLabels: string[] }, pin: string, env?: NodeJS.ProcessEnv }} options
|
|
247
|
-
* @returns {{ state: "installed"|"updated"|"current"|"unsupported", shell: string|null, display: string|null, note: string|null }}
|
|
324
|
+
* @param {{ contract: { categories: string[], tagLabels: string[] }, pin: string, version?: string, env?: NodeJS.ProcessEnv }} options
|
|
325
|
+
* @returns {{ state: "installed"|"updated"|"current"|"unsupported", shell: string|null, display: string|null, note: string|null, path?: string }}
|
|
248
326
|
*/
|
|
249
|
-
export function installCompletion({ contract, pin, env = process.env }) {
|
|
327
|
+
export function installCompletion({ contract, pin, version = "unknown", env = process.env }) {
|
|
250
328
|
const target = completionInstall(env)
|
|
251
329
|
if (!target) return { state: "unsupported", shell: basename(env.SHELL || "") || null, display: null, note: null }
|
|
252
|
-
const script = renderCompletion(target.shell, { contract, pin })
|
|
330
|
+
const script = renderCompletion(target.shell, { contract, pin, version })
|
|
253
331
|
let existing = null
|
|
254
332
|
try {
|
|
255
333
|
existing = readFileSync(target.path, "utf8")
|
|
@@ -44,6 +44,7 @@ import { LIVE_PATHS } from "./registry.mjs"
|
|
|
44
44
|
import { credential, defaultBranchHead, getJson, UNAUTHENTICATED_LIMIT, GitHubError } from "./github.mjs"
|
|
45
45
|
import { NPM_REGISTRY, registryLatest, upgradeCommand } from "./upgrade.mjs"
|
|
46
46
|
import { pathHint } from "./path-hint.mjs"
|
|
47
|
+
import { completionStatus } from "./completion-check.mjs"
|
|
47
48
|
|
|
48
49
|
/** "git+https://github.com/owner/name.git" in package.json -> "https://github.com/owner/name", or null. */
|
|
49
50
|
function repositoryPage(repository) {
|
|
@@ -52,7 +53,7 @@ function repositoryPage(repository) {
|
|
|
52
53
|
return match ? `https://github.com/${match[1]}/${match[2]}` : null
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
function tool(repoRoot) {
|
|
56
|
+
export function tool(repoRoot) {
|
|
56
57
|
try {
|
|
57
58
|
const pkg = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8"))
|
|
58
59
|
return { name: pkg.name, version: pkg.version, engines: pkg.engines?.node || null, repository: repositoryPage(pkg.repository) }
|
|
@@ -224,6 +225,13 @@ export async function doctor({ repoRoot, offline = false, onPhase, env = process
|
|
|
224
225
|
: `${reach.reason}${reach.where ? ` Keep the line below in ${reach.where}.` : ""}`,
|
|
225
226
|
reach.reachable ? null : reach.line)
|
|
226
227
|
|
|
228
|
+
// Tab completion, as it is and not as it was written: the script, its
|
|
229
|
+
// omakit version and pin against this one's, and a new shell asked
|
|
230
|
+
// whether it loads (completion-check.mjs). Measured before this (M8):
|
|
231
|
+
// setup reported success for a script no shell was ever asked about.
|
|
232
|
+
const completion = completionStatus({ version: self.version, pin: MARKETPLACE_PIN.commit, env })
|
|
233
|
+
add("omakit.completion", completion.state, completion.detail, completion.action, completion.evidence)
|
|
234
|
+
|
|
227
235
|
const node = process.versions.node
|
|
228
236
|
const major = Number(node.split(".")[0])
|
|
229
237
|
add("node", major >= 22 ? "ok" : "problem", `node ${node}${self.engines ? ` (needs ${self.engines})` : ""}`,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Every option every command accepts, in one table, and the one parser
|
|
2
|
+
// that reads a command line against it.
|
|
3
|
+
//
|
|
4
|
+
// The table is the source the help signatures and the completion scripts
|
|
5
|
+
// are held to (tests/unit/options.test.mjs): a flag a command reads must
|
|
6
|
+
// be in its signature, and a flag in a signature must be one the command
|
|
7
|
+
// reads, so `omakit help`, tab completion and the code cannot disagree. A
|
|
8
|
+
// token the command does not know, an option without its value, or one
|
|
9
|
+
// positional more than the command takes is refused before anything runs.
|
|
10
|
+
// Measured before this: `omakit weigh <plugin> -n 1` ran three runs as if
|
|
11
|
+
// nothing had been passed, and the other commands read their flags one by
|
|
12
|
+
// one and ignored the rest.
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* What each command accepts. `valued` options take the next token (or
|
|
16
|
+
* `--name=value`); `flags` stand alone; `positionals` is how many bare
|
|
17
|
+
* arguments the command takes. `--help` and `-h` are handled before any
|
|
18
|
+
* command runs and are not options of one.
|
|
19
|
+
*/
|
|
20
|
+
export const ACCEPTED = Object.freeze({
|
|
21
|
+
setup: Object.freeze({ valued: [], flags: ["--yes", "--completion"], positionals: 0 }),
|
|
22
|
+
pin: Object.freeze({ valued: [], flags: [], positionals: 0 }),
|
|
23
|
+
submit: Object.freeze({ valued: ["--category", "--tags", "--notes", "--suggest-tag", "--name", "--out"], flags: ["--offline", "--allow-dirty", "--json"], positionals: 1 }),
|
|
24
|
+
watch: Object.freeze({ valued: ["--out"], flags: ["--json"], positionals: 1 }),
|
|
25
|
+
verify: Object.freeze({ valued: ["--out"], flags: ["--allow-dirty", "--json"], positionals: 1 }),
|
|
26
|
+
help: Object.freeze({ valued: [], flags: ["--agent"], positionals: 0 }),
|
|
27
|
+
upgrade: Object.freeze({ valued: [], flags: ["--dry-run"], positionals: 0 }),
|
|
28
|
+
doctor: Object.freeze({ valued: ["--out"], flags: ["--offline", "--json"], positionals: 0 }),
|
|
29
|
+
parity: Object.freeze({ valued: ["--count", "--offset", "--out"], flags: [], positionals: 0 }),
|
|
30
|
+
weigh: Object.freeze({ valued: ["--runs", "--window", "--settle", "--out"], flags: ["--all", "--list", "--json", "--yes"], positionals: 1 }),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
/** The accepted options of a command in the words a refusal prints: `--runs N, --all, ...`. */
|
|
34
|
+
export function acceptedWords(name) {
|
|
35
|
+
const spec = ACCEPTED[name]
|
|
36
|
+
const value = (option) => ({ "--out": "FILE", "--runs": "N", "--count": "N", "--offset": "N", "--window": "S", "--settle": "S", "--category": "C", "--tags": "A,B" }[option] || "TEXT")
|
|
37
|
+
return [...spec.valued.map((option) => `${option} ${value(option)}`), ...spec.flags].join(", ")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Read a command line against a command's table. Every token is a known
|
|
42
|
+
* option (valued or not), the value of a valued option, or a positional up
|
|
43
|
+
* to the allowed count; the first token that is none of those is returned
|
|
44
|
+
* as `offending` with a reason, so the command refuses before any preflight.
|
|
45
|
+
*
|
|
46
|
+
* @param {string[]} args
|
|
47
|
+
* @param {{ valued: string[], flags: string[], positionals: number }} spec
|
|
48
|
+
* @returns {{ offending: string|null, reason: string|null, options: Map<string, string|true>, positionals: string[] }}
|
|
49
|
+
*/
|
|
50
|
+
export function checkArgs(args, spec) {
|
|
51
|
+
const options = new Map()
|
|
52
|
+
const positionals = []
|
|
53
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
54
|
+
const token = args[index]
|
|
55
|
+
const [name, inline] = token.startsWith("--") && token.includes("=") ? [token.slice(0, token.indexOf("=")), token.slice(token.indexOf("=") + 1)] : [token, undefined]
|
|
56
|
+
if (spec.valued.includes(name)) {
|
|
57
|
+
const value = inline !== undefined ? inline : args[index + 1]
|
|
58
|
+
if (value === undefined || (inline === undefined && value.startsWith("-"))) return { offending: token, reason: `${name} needs a value`, options, positionals }
|
|
59
|
+
options.set(name, value)
|
|
60
|
+
if (inline === undefined) index += 1
|
|
61
|
+
} else if (spec.flags.includes(token)) {
|
|
62
|
+
options.set(token, true)
|
|
63
|
+
} else if (token.startsWith("-")) {
|
|
64
|
+
return { offending: token, reason: `${token} is not an option this command knows`, options, positionals }
|
|
65
|
+
} else if (positionals.length < spec.positionals) {
|
|
66
|
+
positionals.push(token)
|
|
67
|
+
} else {
|
|
68
|
+
return { offending: token, reason: `${JSON.stringify(token)} is one argument more than the command takes`, options, positionals }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { offending: null, reason: null, options, positionals }
|
|
72
|
+
}
|
|
@@ -9,6 +9,19 @@ export function omakitCacheDir(name = "", env = process.env) {
|
|
|
9
9
|
return join(base, "omakit", name)
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Omakit's user-writable state, following XDG with the usual ~/.local/state
|
|
14
|
+
* fallback: where `omakit weigh` keeps its documents and its per-restart
|
|
15
|
+
* timing. State, not cache, because a measurement is not something to
|
|
16
|
+
* fetch again.
|
|
17
|
+
*/
|
|
18
|
+
export function omakitStateDir(name = "", env = process.env) {
|
|
19
|
+
const base = env.XDG_STATE_HOME
|
|
20
|
+
? resolve(env.XDG_STATE_HOME)
|
|
21
|
+
: join(env.HOME || homedir(), ".local/state")
|
|
22
|
+
return join(base, "omakit", name)
|
|
23
|
+
}
|
|
24
|
+
|
|
12
25
|
/**
|
|
13
26
|
* Text for a person, with every path under the home directory written the
|
|
14
27
|
* way a shell would take it: `~/.cache/omakit/marketplace`. Only `$HOME`
|
|
@@ -35,9 +35,10 @@ function stateOf(check) {
|
|
|
35
35
|
/**
|
|
36
36
|
* A check's head line: the mark, the id in bold, and the source in brackets
|
|
37
37
|
* pushed to the right edge, so the sources form a column of their own and the
|
|
38
|
-
* ids form another.
|
|
38
|
+
* ids form another. Exported for the weigh report, whose rows are plugins
|
|
39
|
+
* with their kinds where a check has its source.
|
|
39
40
|
*/
|
|
40
|
-
function head(state, id, source, c) {
|
|
41
|
+
export function head(state, id, source, c) {
|
|
41
42
|
const left = `${mark(state, c)}${c("name", id)}`
|
|
42
43
|
const tag = c("punctuation", `[${source}]`)
|
|
43
44
|
const gap = Math.max(2, COLUMNS - width(left) - width(tag))
|
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
// and what to try first. It is idempotent, so running it again on a machine that
|
|
6
6
|
// is already set up just confirms that.
|
|
7
7
|
//
|
|
8
|
-
// It writes
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
8
|
+
// It writes the pinned checkout, through the same `ensurePin` that `omakit
|
|
9
|
+
// pin` uses, and the completion script where the shell in $SHELL loads it
|
|
10
|
+
// from. It does not create symlinks or install anything, and it edits a shell
|
|
11
|
+
// profile in exactly one case, after an explicit yes: the guarded block that
|
|
12
|
+
// makes completions load, when a new shell has no loader. Everywhere else a
|
|
13
|
+
// step that is the user's to take is printed as the command, and stops.
|
|
12
14
|
|
|
13
15
|
import { execFileSync } from "node:child_process"
|
|
14
16
|
import { banner } from "./banner.mjs"
|
|
@@ -18,9 +20,86 @@ import { progress } from "./progress.mjs"
|
|
|
18
20
|
import { action, colourEnabled, GUTTER, mark, styler, wrap } from "./style.mjs"
|
|
19
21
|
import { TAGLINE } from "./usage.mjs"
|
|
20
22
|
import { installCompletion } from "./completion.mjs"
|
|
23
|
+
import { appendLoaderBlock, completionWorks, loaderBlock, loaderBlockPresent, verifyCompletion } from "./completion-check.mjs"
|
|
21
24
|
import { submissionContract } from "./form.mjs"
|
|
22
25
|
import { pathHint } from "./path-hint.mjs"
|
|
23
26
|
import { withHomeAbbreviated } from "./paths.mjs"
|
|
27
|
+
import { askYes } from "../weigh/confirm.mjs"
|
|
28
|
+
import { tool } from "./doctor.mjs"
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Tab completion, end to end: the script written for the shell in $SHELL
|
|
32
|
+
* where that shell loads it from, then a new interactive shell asked
|
|
33
|
+
* whether it can complete `omakit`, the way TAB asks. Measured before this
|
|
34
|
+
* (docs/MEASUREMENTS.md M8): setup reported the script installed and never
|
|
35
|
+
* asked a shell. When the shell has no completion loader, the one guarded
|
|
36
|
+
* block that gives it one is offered, once, and appended only on yes; with
|
|
37
|
+
* `askRc: false` (the step `upgrade` re-runs) it is named and not offered.
|
|
38
|
+
* `▁ ok` is printed only when a new shell shows the spec.
|
|
39
|
+
*
|
|
40
|
+
* @param {{ repoRoot: string, pin: string, version: string, stream?: NodeJS.WriteStream, env?: object,
|
|
41
|
+
* yes?: boolean, askRc?: boolean, input?: NodeJS.ReadStream, verify?: typeof verifyCompletion }} options
|
|
42
|
+
* @returns {Promise<{ state: "ok"|"note"|"unsupported"|"error", shell: string|null, rcAppended: boolean }>}
|
|
43
|
+
*/
|
|
44
|
+
export async function completionStep({ repoRoot, pin, version, stream = process.stdout, env = process.env, yes = false, askRc = true, input = process.stdin, verify = verifyCompletion }) {
|
|
45
|
+
const c = styler(colourEnabled(stream))
|
|
46
|
+
const out = (line = "") => stream.write(`${line}\n`)
|
|
47
|
+
const step = (state, text) => out(`${mark(state, c)}${wrap(withHomeAbbreviated(text, env), { indent: GUTTER }, c).join("\n").trimStart()}`)
|
|
48
|
+
const fix = (text) => { for (const line of action(withHomeAbbreviated(text, env), c)) out(line) }
|
|
49
|
+
let completion
|
|
50
|
+
try {
|
|
51
|
+
const contract = await submissionContract({ repoRoot })
|
|
52
|
+
completion = installCompletion({ contract, pin, version, env })
|
|
53
|
+
} catch (error) {
|
|
54
|
+
step("info", `tab completion was not installed: ${error.message}`)
|
|
55
|
+
return { state: "error", shell: null, rcAppended: false }
|
|
56
|
+
}
|
|
57
|
+
if (completion.state === "unsupported") {
|
|
58
|
+
step("info", completion.shell
|
|
59
|
+
? `tab completion: no script for ${completion.shell}; there is one for bash, zsh and fish.`
|
|
60
|
+
: "tab completion: $SHELL is not set, so no script was installed.")
|
|
61
|
+
return { state: "unsupported", shell: completion.shell, rcAppended: false }
|
|
62
|
+
}
|
|
63
|
+
const what = { installed: "installed", updated: "updated for this omakit and pin", current: "already installed" }[completion.state]
|
|
64
|
+
const where = `${completion.display}${completion.note ? `, ${completion.note}` : ""}`
|
|
65
|
+
let probe = verify(completion.shell, { env })
|
|
66
|
+
let rcAppended = false
|
|
67
|
+
if (!probe.ran) {
|
|
68
|
+
step("advisory", `tab completion for ${completion.shell} ${what} at ${where}, but a new ${completion.shell} could not be asked whether it loads: ${probe.reason}.`)
|
|
69
|
+
return { state: "note", shell: completion.shell, rcAppended }
|
|
70
|
+
}
|
|
71
|
+
if (!probe.loader) {
|
|
72
|
+
const block = loaderBlock(completion.shell, env)
|
|
73
|
+
const missing = completion.shell === "bash"
|
|
74
|
+
? "a new bash has no completion loader: /usr/share/bash-completion/bash_completion is not sourced, so a script under ~/.local/share/bash-completion/ is never read"
|
|
75
|
+
: "a new zsh has not run compinit, so no completion function is ever loaded"
|
|
76
|
+
step("advisory", `tab completion for ${completion.shell} ${what} at ${where}, but ${missing}.`)
|
|
77
|
+
if (block && !loaderBlockPresent(completion.shell, env)) {
|
|
78
|
+
const question = `Add one guarded line to ${block.display} so completions load?`
|
|
79
|
+
const agreed = askRc ? (yes || (Boolean(input.isTTY) && Boolean(stream.isTTY) && await askYes({ input, output: process.stderr, question }))) : false
|
|
80
|
+
if (agreed) {
|
|
81
|
+
const wrote = appendLoaderBlock(completion.shell, env)
|
|
82
|
+
rcAppended = wrote.appended
|
|
83
|
+
step("info", `appended to ${block.display}, marked \`${block.lines[0]}\`; omakit never edits or removes it.`)
|
|
84
|
+
probe = verify(completion.shell, { env })
|
|
85
|
+
} else {
|
|
86
|
+
step("info", `nothing was written. The lines that make completions load, for ${block.display}:`)
|
|
87
|
+
for (const line of block.lines) fix(line)
|
|
88
|
+
return { state: "note", shell: completion.shell, rcAppended }
|
|
89
|
+
}
|
|
90
|
+
} else if (block) {
|
|
91
|
+
step("info", `${block.display} already carries the \`${block.lines[0]}\` block; a new shell still reports no loader, so something later in that file undoes it.`)
|
|
92
|
+
return { state: "note", shell: completion.shell, rcAppended }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (completionWorks(probe)) {
|
|
96
|
+
step("pass", `tab completion for ${completion.shell} ${what} at ${where}; a new ${completion.shell} completes \`omakit\`${probe.spec === "lazy" ? " on the first TAB" : ""}.${rcAppended ? ` Open terminals need a new shell: \`exec ${completion.shell}\`.` : ""}`)
|
|
97
|
+
return { state: "ok", shell: completion.shell, rcAppended }
|
|
98
|
+
}
|
|
99
|
+
step("advisory", `tab completion for ${completion.shell} ${what} at ${where}, but a new ${completion.shell} ${probe.loader ? "does not load it" : "still has no completion loader"}${rcAppended ? " even after the block was appended" : ""}.`)
|
|
100
|
+
fix(`Open a new shell (\`exec ${completion.shell}\`) and run \`omakit doctor\`; it reports the script, the loader and the spec as \`omakit.completion\`.`)
|
|
101
|
+
return { state: "note", shell: completion.shell, rcAppended }
|
|
102
|
+
}
|
|
24
103
|
|
|
25
104
|
function version(command) {
|
|
26
105
|
try {
|
|
@@ -33,11 +112,13 @@ function version(command) {
|
|
|
33
112
|
}
|
|
34
113
|
|
|
35
114
|
/**
|
|
36
|
-
* @param {{ repoRoot: string, entryPoint: string, stream?: NodeJS.WriteStream, env?: object
|
|
115
|
+
* @param {{ repoRoot: string, entryPoint: string, stream?: NodeJS.WriteStream, env?: object, yes?: boolean,
|
|
116
|
+
* input?: NodeJS.ReadStream, verify?: typeof verifyCompletion }} options
|
|
37
117
|
* `env` is where `$HOME` is read from: this is output for a person, so a
|
|
38
|
-
* path under it is printed as `~/...`.
|
|
118
|
+
* path under it is printed as `~/...`. `yes` answers the one question
|
|
119
|
+
* setup can ask (the rc block for completion), for an agent.
|
|
39
120
|
*/
|
|
40
|
-
export async function setup({ repoRoot, entryPoint, stream = process.stdout, env = process.env }) {
|
|
121
|
+
export async function setup({ repoRoot, entryPoint, stream = process.stdout, env = process.env, yes = false, input = process.stdin, verify = verifyCompletion }) {
|
|
41
122
|
const c = styler(colourEnabled(stream))
|
|
42
123
|
const out = (line = "") => stream.write(`${line}\n`)
|
|
43
124
|
// A step is a status line: the mark, then the fact, wrapped under itself.
|
|
@@ -110,23 +191,8 @@ export async function setup({ repoRoot, entryPoint, stream = process.stdout, env
|
|
|
110
191
|
}
|
|
111
192
|
|
|
112
193
|
// Tab completion, installed for the shell in $SHELL where that shell loads
|
|
113
|
-
// it from,
|
|
114
|
-
|
|
115
|
-
// alone otherwise.
|
|
116
|
-
try {
|
|
117
|
-
const contract = await submissionContract({ repoRoot })
|
|
118
|
-
const completion = installCompletion({ contract, pin: identity.commit })
|
|
119
|
-
if (completion.state === "unsupported") {
|
|
120
|
-
step("info", completion.shell
|
|
121
|
-
? `tab completion: no script for ${completion.shell}; there is one for bash, zsh and fish.`
|
|
122
|
-
: "tab completion: $SHELL is not set, so no script was installed.")
|
|
123
|
-
} else {
|
|
124
|
-
const what = { installed: "installed", updated: "updated for this pin", current: "already installed" }[completion.state]
|
|
125
|
-
step("pass", `tab completion for ${completion.shell} ${what} at ${completion.display}${completion.note ? `, ${completion.note}` : ""}.`)
|
|
126
|
-
}
|
|
127
|
-
} catch (error) {
|
|
128
|
-
step("info", `tab completion was not installed: ${error.message}`)
|
|
129
|
-
}
|
|
194
|
+
// it from, and then proven in a new shell (completionStep).
|
|
195
|
+
await completionStep({ repoRoot, pin: identity.commit, version: tool(repoRoot).version, stream, env, yes, askRc: true, input, verify })
|
|
130
196
|
out()
|
|
131
197
|
|
|
132
198
|
out("Try it on a plugin you have checked out:")
|
|
@@ -144,7 +144,29 @@ export function isExpectedRemote(url, expected = REPOSITORY) {
|
|
|
144
144
|
* a way to point the command at somebody else's repository, because nothing
|
|
145
145
|
* on the command line reaches it.
|
|
146
146
|
*/
|
|
147
|
-
|
|
147
|
+
/**
|
|
148
|
+
* After a successful install, the completion step of the omakit that was
|
|
149
|
+
* just installed: it renders the script with its own version and proves it
|
|
150
|
+
* in a new shell, and never asks the rc question (the loader does not
|
|
151
|
+
* change with an upgrade). Run as a child of the new entry point, not in
|
|
152
|
+
* this process, whose code is the old version's. Frozen arguments.
|
|
153
|
+
*/
|
|
154
|
+
export const COMPLETION_REFRESH_ARGS = Object.freeze(["setup", "--completion"])
|
|
155
|
+
|
|
156
|
+
function refreshCompletionWith(root, stream) {
|
|
157
|
+
const entryPoint = join(root, "bin/omakit")
|
|
158
|
+
if (!existsSync(entryPoint)) return { ran: false, reason: `${entryPoint} is not there` }
|
|
159
|
+
try {
|
|
160
|
+
const out = execFileSync(process.execPath, [entryPoint, ...COMPLETION_REFRESH_ARGS], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] })
|
|
161
|
+
stream.write(out)
|
|
162
|
+
return { ran: true, ok: true }
|
|
163
|
+
} catch (error) {
|
|
164
|
+
stream.write(error.stdout || "")
|
|
165
|
+
return { ran: true, ok: false, reason: String(error.stderr || error.message).trim() }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function upgrade({ repoRoot, stream = process.stdout, dryRun = false, expectedRemote = REPOSITORY, latest = latestOnRegistry, npmRoot = npmGlobalRoot, name = "omakit", refreshCompletion = refreshCompletionWith }) {
|
|
148
170
|
const c = styler(colourEnabled(stream))
|
|
149
171
|
const out = (line = "") => stream.write(`${line}\n`)
|
|
150
172
|
const lines = (list) => { for (const line of list) out(line) }
|
|
@@ -160,7 +182,7 @@ export async function upgrade({ repoRoot, stream = process.stdout, dryRun = fals
|
|
|
160
182
|
if (installKind(repoRoot) === "distro") {
|
|
161
183
|
return refuse("this is a distro package under /usr, so omakit leaves upgrades to the package manager.", upgradeCommand(repoRoot))
|
|
162
184
|
}
|
|
163
|
-
return upgradeNpm({ repoRoot, stream, dryRun, latest, npmRoot, name, refuse, note, ok, out, lines, c })
|
|
185
|
+
return upgradeNpm({ repoRoot, stream, dryRun, latest, npmRoot, name, refuse, note, ok, out, lines, c, refreshCompletion })
|
|
164
186
|
}
|
|
165
187
|
|
|
166
188
|
let remote
|
|
@@ -245,15 +267,17 @@ export async function upgrade({ repoRoot, stream = process.stdout, dryRun = fals
|
|
|
245
267
|
ok(`${before.slice(0, 7)} to ${after.slice(0, 7)} on ${branch}, ${log.length} commit(s)`)
|
|
246
268
|
for (const line of log) out(subject(line))
|
|
247
269
|
out()
|
|
270
|
+
const completion = refreshCompletion(resolve(repoRoot), stream)
|
|
271
|
+
if (completion.ran === false) note(`tab completion was not refreshed: ${completion.reason}. Run \`omakit setup\`.`)
|
|
248
272
|
lines(wrap("The marketplace pin did not move: this updated the tool, not the commit its rules are read from. `omakit doctor` says whether that pin is behind, and docs/UPSTREAM_CONTRACT.md says what moving it involves.", {}, c))
|
|
249
|
-
return { ok: true, changed: true, from: before, to: after, commits: log.length }
|
|
273
|
+
return { ok: true, changed: true, from: before, to: after, commits: log.length, completion }
|
|
250
274
|
}
|
|
251
275
|
|
|
252
276
|
/**
|
|
253
277
|
* The npm route. `latest` and `npmRoot` are injectable for the tests only, the
|
|
254
278
|
* way `expectedRemote` is: nothing on the command line reaches them.
|
|
255
279
|
*/
|
|
256
|
-
async function upgradeNpm({ repoRoot, stream, dryRun, latest, npmRoot, name, refuse, note, ok, out, lines, c }) {
|
|
280
|
+
async function upgradeNpm({ repoRoot, stream, dryRun, latest, npmRoot, name, refuse, note, ok, out, lines, c, refreshCompletion }) {
|
|
257
281
|
const root = resolve(repoRoot)
|
|
258
282
|
const globalRoot = npmRoot()
|
|
259
283
|
if (!globalRoot) {
|
|
@@ -302,6 +326,8 @@ async function upgradeNpm({ repoRoot, stream, dryRun, latest, npmRoot, name, ref
|
|
|
302
326
|
}
|
|
303
327
|
ok(`${current} to ${after}, through the npm that installed it`)
|
|
304
328
|
out()
|
|
329
|
+
const completion = refreshCompletion(root, stream)
|
|
330
|
+
if (completion.ran === false) note(`tab completion was not refreshed: ${completion.reason}. Run \`omakit setup\`.`)
|
|
305
331
|
lines(wrap("The marketplace pin did not move: this updated the tool, not the commit its rules are read from. `omakit doctor` says whether that pin is behind, and docs/UPSTREAM_CONTRACT.md says what moving it involves.", {}, c))
|
|
306
|
-
return { ok: true, changed: true, from: current, to: after }
|
|
332
|
+
return { ok: true, changed: true, from: current, to: after, completion }
|
|
307
333
|
}
|
|
@@ -22,10 +22,13 @@ export const COMPLETION_SHELLS = Object.freeze(["bash", "zsh", "fish"])
|
|
|
22
22
|
|
|
23
23
|
export const COMMANDS = Object.freeze([
|
|
24
24
|
{
|
|
25
|
-
signature: "omakit setup",
|
|
25
|
+
signature: "omakit setup [--yes] [--completion]",
|
|
26
26
|
lines: [
|
|
27
27
|
"First run, in one command: check the environment, fetch the pinned",
|
|
28
|
-
"marketplace checkout, and
|
|
28
|
+
"marketplace checkout, install tab completion and prove it in a new shell,",
|
|
29
|
+
"and say what to try first. Idempotent. When a new shell has no completion",
|
|
30
|
+
"loader it asks once before adding one guarded block to the rc file; --yes",
|
|
31
|
+
"answers for an agent. --completion is that step alone, never the question.",
|
|
29
32
|
],
|
|
30
33
|
},
|
|
31
34
|
{
|
|
@@ -55,7 +58,7 @@ export const COMMANDS = Object.freeze([
|
|
|
55
58
|
],
|
|
56
59
|
},
|
|
57
60
|
{
|
|
58
|
-
signature: "omakit watch <issue-url> [--json]",
|
|
61
|
+
signature: "omakit watch <issue-url> [--json] [--out <file>]",
|
|
59
62
|
lines: [
|
|
60
63
|
"Compare the commit the marketplace validated on a submission issue with",
|
|
61
64
|
"the plugin repository's current default-branch HEAD, and say what makes",
|
|
@@ -86,7 +89,7 @@ export const COMMANDS = Object.freeze([
|
|
|
86
89
|
],
|
|
87
90
|
},
|
|
88
91
|
{
|
|
89
|
-
signature: "omakit doctor [--offline] [--json]",
|
|
92
|
+
signature: "omakit doctor [--offline] [--json] [--out <file>]",
|
|
90
93
|
lines: [
|
|
91
94
|
"What is installed, what is pinned, and what has moved since. Reads and",
|
|
92
95
|
"prints; it installs nothing and never moves the pin.",
|
|
@@ -99,6 +102,29 @@ export const COMMANDS = Object.freeze([
|
|
|
99
102
|
"listed repositories; a packaged install requires --out for evidence.",
|
|
100
103
|
],
|
|
101
104
|
},
|
|
105
|
+
{
|
|
106
|
+
signature: [
|
|
107
|
+
"omakit weigh <plugin-id-or-dir> [--runs <n>] [--window <s>] [--settle <s>]",
|
|
108
|
+
" [--yes] [--json] [--out <file>]",
|
|
109
|
+
"omakit weigh --all",
|
|
110
|
+
"omakit weigh --list [--json]",
|
|
111
|
+
],
|
|
112
|
+
lines: [
|
|
113
|
+
"What a plugin weighs on the shell, measured: the shell is restarted",
|
|
114
|
+
"without it and with it, several runs, and the difference is the weight,",
|
|
115
|
+
"with the baseline's own spread as the noise floor; memory is printed as",
|
|
116
|
+
"the shell's own startup variance, CPU and child processes as the weight.",
|
|
117
|
+
"The one command that changes your machine: it edits shell.json for the",
|
|
118
|
+
"duration, backs it up first, restores it on every exit path, and asks",
|
|
119
|
+
"before the first restart (--yes answers for you): about a minute per",
|
|
120
|
+
"restart, six restarts for one plugin at three runs. --all weighs every",
|
|
121
|
+
"enabled third-party plugin and is sized for a lab machine, not a working",
|
|
122
|
+
"desktop. Writes the document to --out, by default",
|
|
123
|
+
"$XDG_STATE_HOME/omakit/weigh/<date>.json, and ends with the sentence",
|
|
124
|
+
"for the plugin's README. --list is read-only: every installed plugin",
|
|
125
|
+
"and when it was last weighed, unweighed enabled plugins first.",
|
|
126
|
+
],
|
|
127
|
+
},
|
|
102
128
|
])
|
|
103
129
|
|
|
104
130
|
// A command sits one STEP in from the heading; what it does sits one STEP in
|