@jals1212/pi-spaces-cli 0.6.0 → 0.6.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/CHANGELOG.md CHANGED
@@ -6,6 +6,32 @@ commands and options and a patch version fixes.
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 0.6.1
10
+
11
+ ### Added
12
+
13
+ - `pi-space <command> --help` (also `-h`, and `pi-space --help <command>`)
14
+ prints the help of one command: what it does, its arguments and options, the
15
+ call options that apply to it, and examples. `--help` anywhere after the
16
+ command word counts, so `pi-space remove work --help` prints the help and
17
+ removes nothing. After a space name it still belongs to Pi:
18
+ `pi-space work --help` starts Pi with `--help`. An unknown command exits with
19
+ status 2. It reads no config file and starts nothing.
20
+
21
+ ### Changed
22
+
23
+ - Completion groups its candidates (commands, spaces, profiles, options) and
24
+ describes them: each command shows a one-line summary, `base` and the spaces
25
+ are told apart, a profile shows its command, and `remove` offers `--yes` and
26
+ `--no-backup`. `--help` is offered, as the options are, once the word starts
27
+ with `-`. The hidden `__complete` prints `<group>\t<value>\t<description>`
28
+ lines instead of bare words.
29
+ - `pi-space -<TAB>` offers the call options not typed yet (`--profile`,
30
+ `--dir`, `--config`, `--command`, `--setup`), and `--help` and `--version`
31
+ when nothing comes before.
32
+ - Completion no longer offers `completion` among the commands; it still works
33
+ typed in full, and `pi-space completion <TAB>` still offers `zsh`.
34
+
9
35
  ## 0.6.0
10
36
 
11
37
  ### Added
package/README.md CHANGED
@@ -28,9 +28,12 @@ The command words are reserved: a space can never be named `list`, `new`, `remov
28
28
  | `pi-space install <name\|--every> <source> [pi install options...]` | Runs Pi's `install` in a space. The source is required. |
29
29
  | `pi-space uninstall <name\|--every> <source> [pi options...]` | Runs Pi's `uninstall` (its alias of `remove`) in a space. The source is required. |
30
30
  | `pi-space completion zsh` | Prints the zsh completion script (see [Shell completion](#shell-completion)). Any other shell, or none, exits with status 2. |
31
+ | `pi-space <command> --help` | Shows the help of one command: what it does, its arguments and options, the call options that apply, and examples (`pi-space --help <command>` and `-h` work too). |
31
32
  | `pi-space --help` | Shows the usage. |
32
33
  | `pi-space --version` | Prints the version. |
33
34
 
35
+ `--help` (or `-h`) anywhere after a command word shows that command's help and does nothing else: `pi-space remove work --help` removes nothing, and it reads no config file. After a space name every argument belongs to Pi, so `pi-space work --help` starts Pi with `--help`. An unknown command (`pi-space --help ghost`) exits with status 2 and lists the commands.
36
+
34
37
  `--new` and `--remove`, the options of 0.3, were replaced by `new` and `remove`; they exit with status 2 and name the command to use.
35
38
 
36
39
  ### Packages in a space, or in every space
@@ -160,7 +163,7 @@ alias my-space='pi-space --profile my' # any POSIX shell
160
163
 
161
164
  ## Shell completion
162
165
 
163
- `pi-space` completes its commands, the profile names after `--profile`, files after `--config` and `--dir`, and the spaces. For zsh, add to `~/.zshrc` (after `compinit`):
166
+ `pi-space` completes its commands (each with a one-line summary), the profile names after `--profile`, files after `--config` and `--dir`, and the spaces. For zsh, add to `~/.zshrc` (after `compinit`):
164
167
 
165
168
  ```sh
166
169
  source <(pi-space completion zsh)
@@ -170,14 +173,18 @@ It asks the command itself for the candidates each time, through the hidden `pi-
170
173
 
171
174
  | After | Candidates |
172
175
  | --- | --- |
173
- | `pi-space` (and the call options) | the commands, `base` and the spaces |
174
- | `--profile` | the profile names, `pi` included |
176
+ | `pi-space` (and the call options) | the commands, then `base` and the spaces (`completion` is not offered, but works typed in full) |
177
+ | `--profile` | the profile names, `pi` included, each with its command |
175
178
  | `--config`, `--dir` | files (the shell's own completion) |
176
179
  | `remove`, `rename` | the spaces |
177
180
  | `setup` | the spaces and `--every` |
178
181
  | `list`, `update`, `install`, `uninstall` | `base`, the spaces and `--every` |
179
182
  | `completion` | `zsh` |
180
183
 
184
+ In first place the call options not typed yet come too, and `--help` and `--version` when nothing comes before. After a command word the options come too: `--help` for every command, and `--yes` and `--no-backup` after `remove`. zsh shows them once the word starts with `-`, so `pi-space new <TAB>` never fills in a lone `--help`.
185
+
186
+ The candidates come in groups, each with a description, and zsh lists each group apart: commands, spaces (`base` is "the home Pi starts with"), profiles, shells and options. The hidden `pi-space __complete` prints one `<group>\t<value>\t<description>` line per candidate (or the single line `__files__` when the shell should complete a file), so a script for another shell can use it.
187
+
181
188
  Completion also works through an abbreviation or alias like the one above: they expand first, so `my-space <TAB>` completes what `pi-space --profile my <TAB>` would. A config file that is wrong, or a profile that does not exist, just gives no candidates; completion never prints an error.
182
189
 
183
190
  ## Preparing a new space
package/dist/cli.js CHANGED
@@ -51,6 +51,7 @@ export const USAGE = `Usage: pi-space [--command <cmd>] [--dir <path>] [--setup
51
51
  pi-space install <name|--every> <source> install a package in a space (more words go to Pi)
52
52
  pi-space uninstall <name|--every> <source> remove a package from a space (more words go to Pi)
53
53
  pi-space completion zsh print the zsh completion script (source <(pi-space completion zsh))
54
+ pi-space <command> --help the help of one command
54
55
  pi-space --help print this help
55
56
  pi-space --version print the version
56
57
  --every takes the place of the name: base first (not for setup), then every space in turn; a failure does not stop the rest, and the exit status is 1 if any failed.
@@ -90,6 +91,7 @@ const REPLACED = {
90
91
 
91
92
 
92
93
 
94
+
93
95
 
94
96
 
95
97
 
@@ -109,6 +111,154 @@ const REPLACED = {
109
111
 
110
112
  const CALL_OPTIONS = { "--command": "command", "--dir": "dir", "--setup": "setup", "--profile": "profile", "--config": "config" } ;
111
113
 
114
+ function isHelpFlag(word ) {
115
+ return word === "--help" || word === "-h";
116
+ }
117
+
118
+ /** What each call option is, for the help of the commands it applies to. */
119
+ const CALL_HELP = {
120
+ command: ["--command <cmd>", 'the program to run instead of $PI_SPACES_COMMAND (default: pi), e.g. "my-launcher --"'],
121
+ dir: ["--dir <path>", "the spaces folder instead of $PI_SPACES_DIR; the Pi it starts gets it too"],
122
+ setup: ["--setup <cmd>", "the setup command instead of $PI_SPACES_SETUP; {home} and {name} become the space's folder and name"],
123
+ profile: ["--profile <name>", 'the profile of the config file instead of its "default"'],
124
+ config: ["--config <path>", "the config file instead of $PI_SPACES_CONFIG (default: ~/.pi-spaces/config.json)"],
125
+ };
126
+
127
+ /** The help of one command; the summary is the one line completion shows next to it. */
128
+
129
+
130
+
131
+
132
+
133
+
134
+
135
+
136
+
137
+
138
+
139
+
140
+
141
+
142
+ const PI_ARGS = ["[pi args...]", "more words go to Pi, e.g. --extensions, --all or a source"];
143
+ const NAME_OR_EVERY = [
144
+ ["<name>", "base or a space"],
145
+ ["--every", "base first, then every space in turn; a failure does not stop the rest, and the exit status is 1 if any failed"],
146
+ ];
147
+
148
+ /** The commands, in the order completion lists them. They are reserved words: the core never lets a space be named one. */
149
+ const COMMANDS = {
150
+ list: {
151
+ summary: "list the spaces, or the packages of one",
152
+ usage: "list [<name|--every>]",
153
+ about: ["With a name it runs Pi's list in that space; with --every, in base and then every space."],
154
+ args: NAME_OR_EVERY,
155
+ options: [],
156
+ calls: ["command", "dir", "profile", "config"],
157
+ examples: ["pi-space list", "pi-space list work", "pi-space list --every"],
158
+ },
159
+ new: {
160
+ summary: "create a space",
161
+ usage: "new <name>",
162
+ about: ["Its settings.json declares the extension. It runs the setup command in it when there is one, and does not start Pi."],
163
+ args: [["<name>", "the name of the new space"]],
164
+ options: [],
165
+ calls: ["dir", "setup", "profile", "config"],
166
+ examples: ["pi-space new work", "pi-space --setup 'my-launcher --home {home} setup' new work"],
167
+ },
168
+ setup: {
169
+ summary: "run the setup command in a space",
170
+ usage: "setup <name|--every>",
171
+ about: ["It needs a setup command: --setup or $PI_SPACES_SETUP. It never runs in base."],
172
+ args: [
173
+ ["<name>", "a space"],
174
+ ["--every", "every space, never base; a failure does not stop the rest, and the exit status is 1 if any failed"],
175
+ ],
176
+ options: [],
177
+ calls: ["dir", "setup", "profile", "config"],
178
+ examples: ["pi-space setup work", "pi-space setup --every", "pi-space --setup 'my-launcher --home {home} setup' setup --every"],
179
+ },
180
+ rename: {
181
+ summary: "rename a space",
182
+ usage: "rename <old> <new>",
183
+ about: ["It refuses what remove refuses: base, the space you are in, and a space that holds the base home."],
184
+ args: [
185
+ ["<old>", "the space to rename"],
186
+ ["<new>", "its new name"],
187
+ ],
188
+ options: [],
189
+ calls: ["dir", "profile", "config"],
190
+ examples: ["pi-space rename work client"],
191
+ },
192
+ remove: {
193
+ summary: "delete a space, keeping a backup",
194
+ usage: "remove <name> [--yes|-y] [--no-backup]",
195
+ about: ["It deletes the space and everything in it, after asking, and keeps a backup in .backups unless told not to."],
196
+ args: [["<name>", "the space to delete"]],
197
+ options: [
198
+ { flag: "--yes", alias: "-y", text: "do not ask; keep a backup" },
199
+ { flag: "--no-backup", text: "delete without a backup" },
200
+ ],
201
+ calls: ["dir", "profile", "config"],
202
+ examples: ["pi-space remove work", "pi-space remove work --yes", "pi-space remove work --yes --no-backup"],
203
+ },
204
+ update: {
205
+ summary: "update the packages of a space",
206
+ usage: "update <name|--every> [pi args...]",
207
+ about: ["It runs Pi's update in a space, or in base and then every space."],
208
+ args: [...NAME_OR_EVERY, PI_ARGS],
209
+ options: [],
210
+ calls: ["command", "dir", "profile", "config"],
211
+ examples: ["pi-space update work", "pi-space update base --extensions", "pi-space --command 'my-launcher --' update --every"],
212
+ },
213
+ install: {
214
+ summary: "install a package in a space",
215
+ usage: "install <name|--every> <source> [pi args...]",
216
+ about: ["It runs Pi's install in a space, or in base and then every space."],
217
+ args: [...NAME_OR_EVERY, ["<source>", "the package to install, e.g. npm:some-package"], PI_ARGS],
218
+ options: [],
219
+ calls: ["command", "dir", "profile", "config"],
220
+ examples: ["pi-space install work npm:some-package", "pi-space install --every npm:some-package"],
221
+ },
222
+ uninstall: {
223
+ summary: "remove a package from a space",
224
+ usage: "uninstall <name|--every> <source> [pi args...]",
225
+ about: ["It runs Pi's uninstall (its alias of remove) in a space, or in base and then every space."],
226
+ args: [...NAME_OR_EVERY, ["<source>", "the package to remove, e.g. npm:some-package"], PI_ARGS],
227
+ options: [],
228
+ calls: ["command", "dir", "profile", "config"],
229
+ examples: ["pi-space uninstall work npm:some-package", "pi-space uninstall --every npm:some-package"],
230
+ },
231
+ completion: {
232
+ summary: "print the shell completion script",
233
+ usage: "completion <shell>",
234
+ about: ["Supported shells: zsh."],
235
+ args: [["<shell>", "zsh"]],
236
+ options: [],
237
+ calls: [],
238
+ examples: ["pi-space completion zsh", "source <(pi-space completion zsh)"],
239
+ },
240
+ };
241
+
242
+ const HELP_OPTION = { flag: "--help", alias: "-h", text: "the help of this command" };
243
+
244
+ /** The help of one command: usage, what it does, arguments, options, the call options that apply, and examples. */
245
+ function commandHelp(name ) {
246
+ const help = COMMANDS[name];
247
+ const options = [...help.options, HELP_OPTION].map((option) => [option.alias ? `${option.flag}, ${option.alias}` : option.flag, option.text]);
248
+ const calls = help.calls.map((call) => CALL_HELP[call]);
249
+ const width = Math.max(...[...help.args, ...options, ...calls].map(([label]) => label.length)) + 2;
250
+ const rows = (list ) => list.map(([label, text]) => ` ${label.padEnd(width)}${text}`);
251
+ const sections = [
252
+ [`Usage: pi-space ${help.usage}`],
253
+ [`${help.summary[0].toUpperCase()}${help.summary.slice(1)}.`, ...help.about],
254
+ ["Arguments:", ...rows(help.args)],
255
+ ["Options:", ...rows(options)],
256
+ ...(calls.length > 0 ? [["Call options (before the command, for this call only):", ...rows(calls)]] : []),
257
+ ["Examples:", ...help.examples.map((example) => ` ${example}`)],
258
+ ];
259
+ return `${sections.map((section) => section.join("\n")).join("\n\n")}\n`;
260
+ }
261
+
112
262
  /**
113
263
  * The call options (--command, --dir, --setup, --profile, --config) come first; after a space name every argument belongs to Pi.
114
264
  * The commands (list, new, setup, remove, rename, update, install, uninstall, completion) are reserved words, so they
@@ -134,10 +284,12 @@ export function parseCli(args ) {
134
284
  function parseCommand(args ) {
135
285
  const [first, ...rest] = args;
136
286
  if (first === undefined) return { kind: "picker" };
137
- if (first === "--help" || first === "-h") return { kind: "help" };
287
+ if (isHelpFlag(first)) return rest.length === 0 ? { kind: "help" } : rest.length === 1 ? { kind: "command-help", command: rest[0] } : { kind: "usage" };
138
288
  if (first === "--version" || first === "-v") return { kind: "version" };
139
289
  if (Object.hasOwn(REPLACED, first)) return { kind: "replaced", option: first, instead: REPLACED[first ] };
140
290
  if (first.startsWith("-")) return { kind: "unknown-option", option: first };
291
+ // --help after a command word is that command's help; after a space name it belongs to Pi (the launch below).
292
+ if (Object.hasOwn(COMMANDS, first) && rest.some(isHelpFlag)) return { kind: "command-help", command: first };
141
293
  if (first === "list" && rest.length === 0) return { kind: "list" };
142
294
  if (PACKAGE_VERBS.includes(first)) return parsePackages(first , rest);
143
295
  if (first === "new") return rest.length === 1 ? { kind: "new", name: rest[0] } : { kind: "usage" };
@@ -386,6 +538,13 @@ export async function main(argv , io ) {
386
538
  case "help":
387
539
  io.stdout.write(USAGE);
388
540
  return io.exit(0);
541
+ case "command-help":
542
+ if (Object.hasOwn(COMMANDS, command.command)) {
543
+ io.stdout.write(commandHelp(command.command));
544
+ return io.exit(0);
545
+ }
546
+ io.stderr.write(`pi-space: No help for "${command.command}"; commands: ${Object.keys(COMMANDS).join(", ")}.\n`);
547
+ return io.exit(2);
389
548
  case "version":
390
549
  io.stdout.write(`${VERSION}\n`);
391
550
  return io.exit(0);
@@ -458,28 +617,57 @@ function printCompletion(shell , io ) {
458
617
  const FILES = "__files__";
459
618
 
460
619
  /**
461
- * The zsh script: it asks `pi-space __complete <the words before the cursor>` for candidates, one per line,
462
- * and falls back to the shell's file completion on the marker. An abbreviation or alias has already expanded
463
- * by then, so `my-space <TAB>` completes the words of what it stands for.
620
+ * The zsh script: it asks `pi-space __complete <the words before the cursor>` for candidates, one per line as
621
+ * `<group>\t<value>\t<description>`, and hands each group to _describe (a ":" in a value is escaped for it).
622
+ * The `__files__` marker falls back to the shell's file completion. An abbreviation or alias has already
623
+ * expanded by then, so `my-space <TAB>` completes the words of what it stands for.
624
+ * The options show only for a word that starts with "-", so `pi-space new <TAB>` never inserts a lone --help.
464
625
  */
465
626
  const ZSH_COMPLETION = [
466
627
  "#compdef pi-space",
467
628
  "# zsh completion for pi-space. Load it from .zshrc with: source <(pi-space completion zsh)",
468
629
  "_pi_space() {",
469
- " local -a lines",
630
+ " local -a lines cmds spaces profiles shells opts",
631
+ " local line group value text ret=1",
470
632
  ' lines=("${(@f)$(command "${words[1]}" __complete "${(@)words[2,CURRENT-1]}" 2>/dev/null)}")',
471
633
  ` if [[ "\${lines[1]}" == ${FILES} ]]; then`,
472
634
  " _files",
473
635
  " return",
474
636
  " fi",
475
- ' lines=("${(@)lines:#}")',
476
- " compadd -a lines",
637
+ ' for line in "${(@)lines}"; do',
638
+ ' [[ -n "$line" ]] || continue',
639
+ " group=${line%%$'\\t'*}",
640
+ " line=${line#*$'\\t'}",
641
+ " value=${line%%$'\\t'*}",
642
+ " text=${line#*$'\\t'}",
643
+ " [[ \"$text\" == \"$line\" ]] && text=",
644
+ " value=${value//:/\\\\:}",
645
+ ' case "$group" in',
646
+ ' command) cmds+=("$value:$text") ;;',
647
+ ' space) spaces+=("$value:$text") ;;',
648
+ ' profile) profiles+=("$value:$text") ;;',
649
+ ' shell) shells+=("$value:$text") ;;',
650
+ ' option) [[ "$PREFIX" == -* ]] && opts+=("$value:$text") ;;',
651
+ " esac",
652
+ " done",
653
+ " (( $#cmds )) && _describe -t commands 'command' cmds && ret=0",
654
+ " (( $#spaces )) && _describe -t spaces 'space' spaces && ret=0",
655
+ " (( $#profiles )) && _describe -t profiles 'profile' profiles && ret=0",
656
+ " (( $#shells )) && _describe -t shells 'shell' shells && ret=0",
657
+ " (( $#opts )) && _describe -t options 'option' opts && ret=0",
658
+ " return ret",
477
659
  "}",
478
660
  "compdef _pi_space pi-space",
479
661
  "",
480
662
  ].join("\n");
481
663
 
482
- const COMMAND_WORDS = ["list", "new", "setup", "rename", "remove", "update", "install", "uninstall", "completion"];
664
+ /** The groups of a `__complete` line. */
665
+
666
+
667
+ /** One `__complete` line; the description never holds a tab or a newline. */
668
+ function candidate(group , value , text ) {
669
+ return `${group}\t${value}\t${text.replace(/\s+/g, " ")}`;
670
+ }
483
671
 
484
672
  /**
485
673
  * The hidden `__complete <words before the cursor>`: one candidate per line, honoring the call options already
@@ -496,6 +684,49 @@ function complete(words , io ) {
496
684
  return io.exit(0);
497
685
  }
498
686
 
687
+ /** The first-place commands: `completion` is left out (it still works typed in full). */
688
+ function commandCandidates() {
689
+ return Object.entries(COMMANDS)
690
+ .filter(([name]) => name !== "completion")
691
+ .map(([name, help]) => candidate("command", name, help.summary));
692
+ }
693
+
694
+ /** The call options not typed yet, then --help and --version when nothing was typed before. */
695
+ function firstOptions(call , alone ) {
696
+ const calls = Object.entries(CALL_OPTIONS)
697
+ .filter(([, key]) => call[key] === undefined)
698
+ .map(([flag, key]) => candidate("option", flag, CALL_HELP[key][1]));
699
+ const general = alone ? [candidate("option", "--help", "print the help"), candidate("option", "--version", "print the version")] : [];
700
+ return [...calls, ...general];
701
+ }
702
+
703
+ /** `base`, if asked, then the spaces. */
704
+ function spaceCandidates(spaces , withBase ) {
705
+ return [...(withBase ? [candidate("space", BASE, "the home Pi starts with")] : []), ...spaces.map((name) => candidate("space", name, "space"))];
706
+ }
707
+
708
+ /** What follows a command word: its target, its own options and --help. */
709
+ function commandArguments(command , spaces ) {
710
+ const every = (text ) => candidate("space", "--every", text);
711
+ const options = [...COMMANDS[command].options, HELP_OPTION].map((option) => candidate("option", option.flag, option.text));
712
+ switch (command) {
713
+ case "remove":
714
+ case "rename":
715
+ return [...spaceCandidates(spaces, false), ...options];
716
+ case "setup":
717
+ return [...spaceCandidates(spaces, false), every("every space"), ...options];
718
+ case "list":
719
+ case "update":
720
+ case "install":
721
+ case "uninstall":
722
+ return [...spaceCandidates(spaces, true), every("every space (base first)"), ...options];
723
+ case "completion":
724
+ return [...SHELLS.map((shell) => candidate("shell", shell, `the ${shell} completion script`)), ...options];
725
+ default:
726
+ return options;
727
+ }
728
+ }
729
+
499
730
  function candidates(words , io ) {
500
731
  const call = {};
501
732
  let index = 0;
@@ -509,33 +740,30 @@ function candidates(words , io ) {
509
740
  const { dir } = resolveSettings(call, io);
510
741
  const rest = words.slice(index);
511
742
  const spaces = listSpaces(dir);
512
- if (rest.length === 0) return [...COMMAND_WORDS, BASE, ...spaces];
513
- if (rest.length > 1) return [];
514
- switch (rest[0]) {
515
- case "remove":
516
- case "rename":
517
- return spaces;
518
- case "setup":
519
- return [...spaces, "--every"];
520
- case "list":
521
- case "update":
522
- case "install":
523
- case "uninstall":
524
- return [BASE, ...spaces, "--every"];
525
- case "completion":
526
- return SHELLS;
527
- default:
528
- return [];
529
- }
743
+ if (rest.length === 0) return [...commandCandidates(), ...spaceCandidates(spaces, true), ...firstOptions(call, index === 0)];
744
+ if (rest.length > 1 || !Object.hasOwn(COMMANDS, rest[0])) return [];
745
+ return commandArguments(rest[0], spaces);
530
746
  }
531
747
 
532
- /** The candidates for the value of a call option: profile names, the shell's files for a path, nothing for a command. */
748
+ /** The candidates for the value of a call option: profile names with their command, the shell's files for a path, nothing for a command. */
533
749
  function optionValues(option , call , io ) {
534
- if (option === "--profile") return profileNames(resolveSettings(call, io).config);
750
+ if (option === "--profile") {
751
+ const { config } = resolveSettings(call, io);
752
+ return profileNames(config).map((name) => candidate("profile", name, profileText(config, name, io)));
753
+ }
535
754
  if (option === "--config" || option === "--dir") return [FILES];
536
755
  return [];
537
756
  }
538
757
 
758
+ /** A profile's command, else "profile". */
759
+ function profileText(config , name , io ) {
760
+ try {
761
+ return profileByName(config, name, io.env, io.home).command || "profile";
762
+ } catch {
763
+ return "profile";
764
+ }
765
+ }
766
+
539
767
  /** Prints the reason and returns false when the setup command has an unclosed quote. */
540
768
  function checkSetup(io , source ) {
541
769
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jals1212/pi-spaces-cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "The pi-space command: pick a space and start Pi in it. Companion to the Pi extension @jals1212/pi-spaces.",
5
5
  "license": "MIT",
6
6
  "author": "jals1212",