@jals1212/pi-spaces-cli 0.5.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 +59 -0
- package/README.md +116 -4
- package/dist/cli.js +478 -42
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,65 @@ 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
|
+
|
|
35
|
+
## 0.6.0
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- Profiles: `--profile <name>` and `--config <path>`, before the command, use a
|
|
40
|
+
named profile of the config file (`~/.pi-spaces/config.json`, or
|
|
41
|
+
`PI_SPACES_CONFIG`, or `--config`) for the command, the spaces folder and the
|
|
42
|
+
setup command, and the file's `default` profile is used without `--profile`.
|
|
43
|
+
Each setting is taken from the call option, then the `--profile` profile,
|
|
44
|
+
then `PI_SPACES_COMMAND`, `PI_SPACES_DIR` or `PI_SPACES_SETUP`, then the
|
|
45
|
+
default profile, then the built-in. The built-in profile `pi` always exists.
|
|
46
|
+
With no file, nothing changes.
|
|
47
|
+
- A profile's `spaceEnv` is added, filled in for the space, to the environment
|
|
48
|
+
of the setup command (`new` and `setup`), whichever setup command runs.
|
|
49
|
+
- A profile's `home` is the base home a new space's declaration of the extension
|
|
50
|
+
is taken from, and `remove` and `rename` refuse the space that holds it.
|
|
51
|
+
- The started Pi gets `--config` as `PI_SPACES_CONFIG`, and a profile's `dir`
|
|
52
|
+
and `setup` as `PI_SPACES_DIR` and `PI_SPACES_SETUP`, as `--dir` and `--setup`
|
|
53
|
+
do.
|
|
54
|
+
- `pi-space completion zsh` prints a zsh completion script (`source
|
|
55
|
+
<(pi-space completion zsh)`): commands, profile names after `--profile`,
|
|
56
|
+
files after `--config` and `--dir`, and the spaces of the profile, config or
|
|
57
|
+
folder already typed. It calls the hidden `pi-space __complete`, which never
|
|
58
|
+
prints an error.
|
|
59
|
+
- A config file that is wrong, or an unknown profile, exits with status 2 and
|
|
60
|
+
prints the problem.
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
|
|
64
|
+
- `completion` is a command word, so it can no longer name a space.
|
|
65
|
+
- Needs `@jals1212/pi-spaces` 0.6.0, which reads the config file
|
|
66
|
+
(`@jals1212/pi-spaces/config`) and reserves the name `completion`.
|
|
67
|
+
|
|
9
68
|
## 0.5.0
|
|
10
69
|
|
|
11
70
|
### Added
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-space
|
|
2
2
|
|
|
3
|
-
The `pi-space` command: pick a [Pi](https://pi.dev) space and start Pi in it, create, set up, rename and remove spaces, and install, update and list the packages of a space, from your shell. It is the optional companion of the Pi extension [`@jals1212/pi-spaces`](https://github.com/jals1212/pi-spaces#readme), which holds the spaces themselves.
|
|
3
|
+
The `pi-space` command: pick a [Pi](https://pi.dev) space and start Pi in it, create, set up, rename and remove spaces, and install, update and list the packages of a space, from your shell. Named [profiles](#profiles) in a config file say which program to run and where its spaces live, so one short command is enough for each program, and [shell completion](#shell-completion) completes the commands, the profiles and the spaces. It is the optional companion of the Pi extension [`@jals1212/pi-spaces`](https://github.com/jals1212/pi-spaces#readme), which holds the spaces themselves.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -12,7 +12,7 @@ Node 20 or later. The Pi extension `@jals1212/pi-spaces` must be installed in th
|
|
|
12
12
|
|
|
13
13
|
## Commands
|
|
14
14
|
|
|
15
|
-
The command words are reserved: a space can never be named `list`, `new`, `remove`, `rename`, `switch`, `update`, `install`, `uninstall` or `
|
|
15
|
+
The command words are reserved: a space can never be named `list`, `new`, `remove`, `rename`, `switch`, `update`, `install`, `uninstall`, `setup` or `completion`, so any other first word is a space name (the reservation of `completion` is in `@jals1212/pi-spaces` 0.6.0).
|
|
16
16
|
|
|
17
17
|
| Command | What it does |
|
|
18
18
|
| --- | --- |
|
|
@@ -27,9 +27,13 @@ The command words are reserved: a space can never be named `list`, `new`, `remov
|
|
|
27
27
|
| `pi-space update <name\|--every> [pi update options...]` | Runs Pi's `update` in a space. What follows is Pi's: `--extensions`, `--all`, a source. With nothing after the name it is Pi's default. |
|
|
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
|
+
| `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). |
|
|
30
32
|
| `pi-space --help` | Shows the usage. |
|
|
31
33
|
| `pi-space --version` | Prints the version. |
|
|
32
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
|
+
|
|
33
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.
|
|
34
38
|
|
|
35
39
|
### Packages in a space, or in every space
|
|
@@ -51,7 +55,7 @@ A new space gets a `settings.json` that declares the extension, so `/space` work
|
|
|
51
55
|
|
|
52
56
|
## Another program that runs Pi
|
|
53
57
|
|
|
54
|
-
To start Pi through another program for one call, give `--command` and `--dir` before the rest; the words of `--command` come before `--space`. The words are split on spaces; double or single quotes keep a path with spaces in one word. Such a program usually has its own home, so `--dir` points at its spaces folder, and the Pi it starts gets that folder as `PI_SPACES_DIR` too. Nothing is exported, so a plain `pi` in the same shell is not affected. Every command accepts these options and uses the ones it needs (`list` ignores `--command`, `rename` ignores `--setup`), so one alias can give them all:
|
|
58
|
+
To start Pi through another program for one call, give `--command` and `--dir` before the rest (or name a [profile](#profiles) that holds both); the words of `--command` come before `--space`. The words are split on spaces; double or single quotes keep a path with spaces in one word. Such a program usually has its own home, so `--dir` points at its spaces folder, and the Pi it starts gets that folder as `PI_SPACES_DIR` too. Nothing is exported, so a plain `pi` in the same shell is not affected. Every command accepts these options and uses the ones it needs (`list` ignores `--command`, `rename` ignores `--setup`), so one alias can give them all:
|
|
55
59
|
|
|
56
60
|
```sh
|
|
57
61
|
pi-space --command "my-launcher --" --dir ~/.my-launcher/spaces work # → my-launcher -- --space work
|
|
@@ -73,8 +77,116 @@ export PI_SPACES_DIR=~/.my-launcher/spaces # the spaces next to that program's
|
|
|
73
77
|
| `PI_SPACES_COMMAND` | The program to run, split on spaces (double or single quotes keep a path with spaces in one word, as in `"/opt/my launcher/run" --`), before `--space <name>`. Default: `pi`. `--command` wins for one call. |
|
|
74
78
|
| `PI_SPACES_DIR` | The spaces folder. A relative value is taken from your home folder. Default: `spaces` next to the base home. `--dir` wins for one call; a relative `--dir` is taken from the current folder. |
|
|
75
79
|
| `PI_SPACES_SETUP` | The command that prepares a new space (see [Preparing a new space](#preparing-a-new-space)). Unset or blank: none. `--setup` wins for one call. |
|
|
80
|
+
| `PI_SPACES_CONFIG` | The [config file](#profiles) with the profiles. A leading `~` is your home folder and a relative value is taken from it. Default: `~/.pi-spaces/config.json`. `--config` wins for one call; a relative `--config` is taken from the current folder. |
|
|
76
81
|
| `PI_CODING_AGENT_DIR` | Pi's agent home; its parent holds the default spaces folder. Default: `~/.pi/agent`. |
|
|
77
82
|
|
|
83
|
+
## Profiles
|
|
84
|
+
|
|
85
|
+
Profiles are named in an optional JSON config file, so you do not have to repeat `--command`, `--dir` and `--setup`, or write shell variables and functions, for each program that runs Pi. This section is the command's side of it; the file format (every key, the schema version, `spaceEnv`, what happens when the file is wrong) is in the [main README](https://github.com/jals1212/pi-spaces#profiles).
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
pi-space --profile ml list # the spaces of the profile "ml"
|
|
89
|
+
pi-space --profile ml work # start the profile's program in its space "work"
|
|
90
|
+
pi-space --profile ml new work # create it, then run the profile's setup in it
|
|
91
|
+
pi-space --config ./other.json --profile ml list # the same profile from another file, for this call
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`--profile <name>` and `--config <path>` come before the command, like `--command`, `--dir` and `--setup`: each needs a value (not blank) and may be given once, and after a space name they belong to Pi. An unknown profile exits with status 2 and lists the known ones: `pi-space: Unknown profile "ghost"; known profiles: ml, pi.`
|
|
95
|
+
|
|
96
|
+
- **The file** is `--config`, else `PI_SPACES_CONFIG`, else `~/.pi-spaces/config.json`. A missing file is not an error: with no file the command behaves exactly as without profiles. A file that is wrong exits with status 2 and prints its message (`pi-space: <file>: <problem>`), never a stack. `--help`, `--version` and `completion` never read it.
|
|
97
|
+
- **`default`**: without `--profile`, the file's `default` profile is used, if it names one. With `--profile`, the default is not consulted at all: a profile never borrows from another.
|
|
98
|
+
- **`pi`** is built in: command `pi`, home `PI_CODING_AGENT_DIR` (else `~/.pi/agent`). It always exists, so `--profile pi` goes back to plain Pi when the default is another program, and a `pi` in the file's `profiles` replaces it.
|
|
99
|
+
|
|
100
|
+
### Which value wins
|
|
101
|
+
|
|
102
|
+
For each of the command, the spaces folder and the setup command, the first of these that has a value is used:
|
|
103
|
+
|
|
104
|
+
1. the call option (`--command`, `--dir`, `--setup`);
|
|
105
|
+
2. the profile named with `--profile`;
|
|
106
|
+
3. the variable (`PI_SPACES_COMMAND`, `PI_SPACES_DIR`, `PI_SPACES_SETUP`);
|
|
107
|
+
4. the file's `default` profile;
|
|
108
|
+
5. the built-in: `pi`, the `spaces` folder next to the base home, no setup command.
|
|
109
|
+
|
|
110
|
+
A profile's spaces folder is its `dir`, which is `spaces` next to its `home` when it leaves `dir` out. A profile's `home` is also the base home a new space's `settings.json` takes the extension's declaration from (instead of `PI_SPACES_BASE_HOME` or `PI_CODING_AGENT_DIR`), and `remove` and `rename` refuse the space that holds it.
|
|
111
|
+
|
|
112
|
+
### What the started Pi is told
|
|
113
|
+
|
|
114
|
+
The Pi that `pi-space` starts (a space, or a package command) reads the same file and the same folders, so `pi-space` hands over what it settled, as environment variables for that child only:
|
|
115
|
+
|
|
116
|
+
- `--config <path>` becomes `PI_SPACES_CONFIG` (absolute), like `--dir` becomes `PI_SPACES_DIR`.
|
|
117
|
+
- When a profile supplies a `dir` or a `setup`, they reach it as `PI_SPACES_DIR` and `PI_SPACES_SETUP`, exactly as `--dir` and `--setup` would, so `/space new` there agrees with `pi-space new`.
|
|
118
|
+
- Nothing else is added: when nothing is to be passed, the child inherits your environment as it is. `--command` is never passed.
|
|
119
|
+
|
|
120
|
+
### `spaceEnv` and the setup command
|
|
121
|
+
|
|
122
|
+
When a profile is selected (`--profile`, or the default), its `spaceEnv` is added to the environment of the setup command, with `{home}` and `{name}` filled in for the space being set up, next to `PI_CODING_AGENT_DIR`, `PI_SPACE` and `PI_SPACE_HOME`. It applies whichever setup command runs: the profile's own `setup`, or one from `--setup` or `PI_SPACES_SETUP`, because the space is that profile's. A call without any profile adds nothing, as before.
|
|
123
|
+
|
|
124
|
+
### A complete example
|
|
125
|
+
|
|
126
|
+
A program called `my-launcher` keeps its agent home in `~/.my-launcher/agent` and its own configuration outside it. `~/.pi-spaces/config.json`:
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{
|
|
130
|
+
"schema": "pi-spaces.config/v1",
|
|
131
|
+
"profiles": {
|
|
132
|
+
"my": {
|
|
133
|
+
"home": "~/.my-launcher/agent",
|
|
134
|
+
"command": "my-launcher --",
|
|
135
|
+
"setup": "my-launcher --home {home} setup",
|
|
136
|
+
"spaceEnv": { "MY_LAUNCHER_CONFIG_HOME": "{home}/config" }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Then, without a variable, an alias or a function:
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
pi-space --profile my new work # creates ~/.my-launcher/spaces/work, then runs:
|
|
146
|
+
# my-launcher --home ~/.my-launcher/spaces/work setup
|
|
147
|
+
# with MY_LAUNCHER_CONFIG_HOME=~/.my-launcher/spaces/work/config
|
|
148
|
+
pi-space --profile my work -c # my-launcher -- --space work -c
|
|
149
|
+
pi-space --profile my setup --every # run the setup again in every space of "my"
|
|
150
|
+
pi-space --profile pi list # the spaces of plain Pi, from the same shell
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Add `"default": "my"` to the file and `pi-space work` alone does the second line; `pi-space --profile pi work` is then plain Pi.
|
|
154
|
+
|
|
155
|
+
### Shortening it (optional)
|
|
156
|
+
|
|
157
|
+
The command needs no help, but a shell abbreviation or alias can shorten `--profile my` to one word. It is only sugar: no logic lives in it, and the profile stays in the config file.
|
|
158
|
+
|
|
159
|
+
```sh
|
|
160
|
+
abbr my-space="pi-space --profile my" # zsh-abbr; typing `my-space work` expands to `pi-space --profile my work`
|
|
161
|
+
alias my-space='pi-space --profile my' # any POSIX shell
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Shell completion
|
|
165
|
+
|
|
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`):
|
|
167
|
+
|
|
168
|
+
```sh
|
|
169
|
+
source <(pi-space completion zsh)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
It asks the command itself for the candidates each time, through the hidden `pi-space __complete`, so it always sees your current config and spaces. Options already typed are honored: after `pi-space --profile my remove <TAB>` you get the spaces of `my`, not those of the default. It lists:
|
|
173
|
+
|
|
174
|
+
| After | Candidates |
|
|
175
|
+
| --- | --- |
|
|
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 |
|
|
178
|
+
| `--config`, `--dir` | files (the shell's own completion) |
|
|
179
|
+
| `remove`, `rename` | the spaces |
|
|
180
|
+
| `setup` | the spaces and `--every` |
|
|
181
|
+
| `list`, `update`, `install`, `uninstall` | `base`, the spaces and `--every` |
|
|
182
|
+
| `completion` | `zsh` |
|
|
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
|
+
|
|
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.
|
|
189
|
+
|
|
78
190
|
## Preparing a new space
|
|
79
191
|
|
|
80
192
|
A new space starts clean: only a `settings.json` that declares the extension. A program that runs Pi may need its own packages in every home it opens. Give its setup command with `--setup` (or `PI_SPACES_SETUP`), and `pi-space new` runs it right after it creates the space. The words are split as for `--command`, and in each word `{home}` becomes the space's folder and `{name}` its name. The command runs with your stdio, and with `PI_CODING_AGENT_DIR` and `PI_SPACE_HOME` set to the space's folder and `PI_SPACE` to its name:
|
|
@@ -95,7 +207,7 @@ Only those three variables change. A variable that points into the base home sta
|
|
|
95
207
|
| --- | --- |
|
|
96
208
|
| 0 … | Pi's own exit status, for a space and for a package command in one space; the setup's exit status for `setup <name>` and for a `new` whose setup failed. |
|
|
97
209
|
| 1 | An unknown or invalid space, a name `new` or `rename` cannot use (taken, reserved or invalid), a space `remove` or `rename` cannot touch, `remove` without `--yes` and no terminal, an out-of-range pick, the program could not start, or `--every` and at least one run failed (the failed spaces are on the last line of stderr), or `setup base`, since setup runs only in spaces. |
|
|
98
|
-
| 2 |
|
|
210
|
+
| 2 | A config file that is wrong or an unknown `--profile` (the problem is printed), `completion` with another shell or none, no arguments and no terminal to pick in, a command with the wrong number of names (`new`, `remove`, `rename`), `install` or `uninstall` without a source, a package command without a space or `--every`, or `--command`, `--dir`, `--setup`, `--profile` or `--config` without a value (or a blank one) or given twice: the usage is printed. `setup` without a setup command: the problem is printed. The old `--new` and `--remove`: the replacing command is printed. A `--command`, `PI_SPACES_COMMAND`, `--setup`, `PI_SPACES_SETUP` or a profile's `command` or `setup` with an unclosed quote: the problem is printed, and nothing is created or started. |
|
|
99
211
|
|
|
100
212
|
## More
|
|
101
213
|
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// The optional `pi-space` command: pick a space and start Pi in it, and manage the spaces and their packages.
|
|
2
2
|
// It only passes `--space <name>` (and, for a package command, Pi's command after it); the extension does the
|
|
3
3
|
// switch, so it sets PI_CODING_AGENT_DIR only for the setup command, which is not Pi and does no switch.
|
|
4
|
+
// Profiles of the config file (--profile, "default") stand in for the command, the spaces folder and the setup.
|
|
4
5
|
import { readFileSync } from "node:fs";
|
|
5
6
|
import { homedir } from "node:os";
|
|
6
7
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -24,8 +25,18 @@ import {
|
|
|
24
25
|
spacesDir,
|
|
25
26
|
validateName,
|
|
26
27
|
} from "@jals1212/pi-spaces/spaces";
|
|
28
|
+
import {
|
|
29
|
+
ConfigError,
|
|
30
|
+
|
|
31
|
+
configPath,
|
|
32
|
+
loadConfig,
|
|
33
|
+
|
|
34
|
+
profileByName,
|
|
35
|
+
profileNames,
|
|
36
|
+
spaceEnvFor,
|
|
37
|
+
} from "@jals1212/pi-spaces/config";
|
|
27
38
|
|
|
28
|
-
export const USAGE = `Usage: pi-space [--command <cmd>] [--dir <path>] [--setup <cmd>] [<name> [pi args...] | <command> ...]
|
|
39
|
+
export const USAGE = `Usage: pi-space [--command <cmd>] [--dir <path>] [--setup <cmd>] [--profile <name>] [--config <path>] [<name> [pi args...] | <command> ...]
|
|
29
40
|
pi-space --help | --version
|
|
30
41
|
pi-space pick a space and start Pi in it
|
|
31
42
|
pi-space <name> [pi args...] start Pi in a space ("base" for the home Pi starts with)
|
|
@@ -39,6 +50,8 @@ export const USAGE = `Usage: pi-space [--command <cmd>] [--dir <path>] [--setup
|
|
|
39
50
|
pi-space update <name|--every> update the packages of a space (more words go to Pi: --extensions, --all, a source)
|
|
40
51
|
pi-space install <name|--every> <source> install a package in a space (more words go to Pi)
|
|
41
52
|
pi-space uninstall <name|--every> <source> remove a package from a space (more words go to Pi)
|
|
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
|
|
42
55
|
pi-space --help print this help
|
|
43
56
|
pi-space --version print the version
|
|
44
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.
|
|
@@ -46,16 +59,20 @@ Options before the command, for this call only:
|
|
|
46
59
|
--command <cmd> the program to run instead of $PI_SPACES_COMMAND (default: pi), e.g. "my-launcher --"
|
|
47
60
|
--dir <path> the spaces folder instead of $PI_SPACES_DIR; the Pi it starts gets it too
|
|
48
61
|
--setup <cmd> the setup command instead of $PI_SPACES_SETUP; {home} and {name} become the space's folder and name
|
|
62
|
+
--profile <name> the profile of the config file to use instead of its "default": its command, spaces folder, setup and spaceEnv
|
|
63
|
+
--config <path> the config file instead of $PI_SPACES_CONFIG (default: ~/.pi-spaces/config.json); the Pi it starts gets it too
|
|
49
64
|
`;
|
|
50
65
|
|
|
51
66
|
/** This package's version; package.json sits one folder above both src/ and dist/. */
|
|
52
67
|
export const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
53
68
|
|
|
54
|
-
/** --command, --dir and --
|
|
69
|
+
/** --command, --dir, --setup, --profile and --config: for this call only, so nothing has to be exported into the shell. */
|
|
55
70
|
|
|
56
71
|
|
|
57
72
|
|
|
58
73
|
|
|
74
|
+
|
|
75
|
+
|
|
59
76
|
|
|
60
77
|
|
|
61
78
|
/** The Pi commands that pi-space runs in one space or in every space. */
|
|
@@ -74,6 +91,7 @@ const REPLACED = {
|
|
|
74
91
|
|
|
75
92
|
|
|
76
93
|
|
|
94
|
+
|
|
77
95
|
|
|
78
96
|
|
|
79
97
|
|
|
@@ -83,18 +101,169 @@ const REPLACED = {
|
|
|
83
101
|
|
|
84
102
|
|
|
85
103
|
|
|
104
|
+
|
|
105
|
+
|
|
86
106
|
|
|
87
107
|
|
|
88
108
|
|
|
89
109
|
|
|
90
110
|
|
|
91
111
|
|
|
92
|
-
const CALL_OPTIONS = { "--command": "command", "--dir": "dir", "--setup": "setup" } ;
|
|
112
|
+
const CALL_OPTIONS = { "--command": "command", "--dir": "dir", "--setup": "setup", "--profile": "profile", "--config": "config" } ;
|
|
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
|
+
}
|
|
93
261
|
|
|
94
262
|
/**
|
|
95
|
-
* --command, --dir
|
|
96
|
-
* The commands (list, new, setup, remove, rename, update, install, uninstall) are reserved words, so they
|
|
97
|
-
* before a name: the core never lets a space have one of them as its name.
|
|
263
|
+
* The call options (--command, --dir, --setup, --profile, --config) come first; after a space name every argument belongs to Pi.
|
|
264
|
+
* The commands (list, new, setup, remove, rename, update, install, uninstall, completion) are reserved words, so they
|
|
265
|
+
* are read before a name: the core never lets a space have one of them as its name. `__complete` is the hidden
|
|
266
|
+
* command the completion script calls; it starts with "_", which no space name does.
|
|
98
267
|
* A space name never starts with "-", so any other option in first place is unknown, not a name.
|
|
99
268
|
*/
|
|
100
269
|
export function parseCli(args ) {
|
|
@@ -115,14 +284,18 @@ export function parseCli(args ) {
|
|
|
115
284
|
function parseCommand(args ) {
|
|
116
285
|
const [first, ...rest] = args;
|
|
117
286
|
if (first === undefined) return { kind: "picker" };
|
|
118
|
-
if (first === "
|
|
287
|
+
if (isHelpFlag(first)) return rest.length === 0 ? { kind: "help" } : rest.length === 1 ? { kind: "command-help", command: rest[0] } : { kind: "usage" };
|
|
119
288
|
if (first === "--version" || first === "-v") return { kind: "version" };
|
|
120
289
|
if (Object.hasOwn(REPLACED, first)) return { kind: "replaced", option: first, instead: REPLACED[first ] };
|
|
121
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 };
|
|
122
293
|
if (first === "list" && rest.length === 0) return { kind: "list" };
|
|
123
294
|
if (PACKAGE_VERBS.includes(first)) return parsePackages(first , rest);
|
|
124
295
|
if (first === "new") return rest.length === 1 ? { kind: "new", name: rest[0] } : { kind: "usage" };
|
|
125
296
|
if (first === "setup") return parseSetup(rest);
|
|
297
|
+
if (first === "completion") return rest.length <= 1 ? { kind: "completion", ...(rest.length === 1 ? { shell: rest[0] } : {}) } : { kind: "usage" };
|
|
298
|
+
if (first === "__complete") return { kind: "complete", words: rest };
|
|
126
299
|
if (first === "rename") return rest.length === 2 ? { kind: "rename", from: rest[0], to: rest[1] } : { kind: "usage" };
|
|
127
300
|
if (first === "remove") {
|
|
128
301
|
const flags = rest.filter((arg) => arg.startsWith("-"));
|
|
@@ -211,10 +384,11 @@ const EXTENSION = "@jals1212/pi-spaces";
|
|
|
211
384
|
|
|
212
385
|
/**
|
|
213
386
|
* The extension's declaration for a new space's settings.json: the one in the base home's settings.json
|
|
214
|
-
* (PI_SPACES_BASE_HOME, else PI_CODING_AGENT_DIR, else ~/.pi/agent),
|
|
387
|
+
* (the selected profile's home, else PI_SPACES_BASE_HOME, else PI_CODING_AGENT_DIR, else ~/.pi/agent),
|
|
388
|
+
* else npm:@jals1212/pi-spaces.
|
|
215
389
|
*/
|
|
216
|
-
export function newSpaceDeclaration(env , home ) {
|
|
217
|
-
const baseHome = env.PI_SPACES_BASE_HOME || agentHome(env, home);
|
|
390
|
+
export function newSpaceDeclaration(env , home , profileHome ) {
|
|
391
|
+
const baseHome = profileHome || env.PI_SPACES_BASE_HOME || agentHome(env, home);
|
|
218
392
|
let packageRoot ;
|
|
219
393
|
try {
|
|
220
394
|
packageRoot = dirname(fileURLToPath(import.meta.resolve(`${EXTENSION}/package.json`)));
|
|
@@ -231,7 +405,7 @@ function agentHome(env , home ) {
|
|
|
231
405
|
|
|
232
406
|
|
|
233
407
|
|
|
234
|
-
|
|
408
|
+
|
|
235
409
|
|
|
236
410
|
|
|
237
411
|
|
|
@@ -245,7 +419,7 @@ function agentHome(env , home ) {
|
|
|
245
419
|
|
|
246
420
|
|
|
247
421
|
|
|
248
|
-
|
|
422
|
+
|
|
249
423
|
|
|
250
424
|
|
|
251
425
|
|
|
@@ -280,25 +454,97 @@ export function processIo(proc = process) {
|
|
|
280
454
|
};
|
|
281
455
|
}
|
|
282
456
|
|
|
457
|
+
/** Where a setting came from, which decides what the started Pi is told and how an error names it. */
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
/** What one call settled: its environment, the spaces folder, the profile it runs under and where each setting came from. */
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
/** Whether the file itself gives the profile a `dir` (the resolved one always has one). */
|
|
473
|
+
function hasOwnDir(config , profile ) {
|
|
474
|
+
return config?.profiles.find((spec) => spec.name === profile.name)?.dir !== undefined;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* The command or setup of this call, with where it came from: the call option, then the --profile profile, then
|
|
479
|
+
* the environment variable, then the default profile. A blank variable is none.
|
|
480
|
+
*/
|
|
481
|
+
function choose(field , call , asked , fallback , env ) {
|
|
482
|
+
const variable = field === "command" ? "PI_SPACES_COMMAND" : "PI_SPACES_SETUP";
|
|
483
|
+
const named = (profile ) => `profile "${profile?.name}" ${field}`;
|
|
484
|
+
const candidates = [
|
|
485
|
+
[call[field], "option", `--${field}`],
|
|
486
|
+
[asked?.[field], "profile", named(asked)],
|
|
487
|
+
[env[variable]?.trim() ? env[variable] : undefined, "env", variable],
|
|
488
|
+
[fallback?.[field], "profile", named(fallback)],
|
|
489
|
+
];
|
|
490
|
+
const found = candidates.find(([value]) => value !== undefined);
|
|
491
|
+
return found && { value: found[0] , origin: found[1], source: found[2] };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Reads the config file, picks the profile and applies the precedence: call option > --profile > PI_SPACES_* >
|
|
496
|
+
* the default profile > built-in. The call's own environment gets the winners (so the rest of the CLI reads
|
|
497
|
+
* PI_SPACES_COMMAND, PI_SPACES_DIR and PI_SPACES_SETUP as ever); the started Pi gets --dir, --setup, --config and
|
|
498
|
+
* what a profile supplies for the folder and the setup, but never --command. Throws a ConfigError.
|
|
499
|
+
*/
|
|
500
|
+
function resolveSettings(call , io ) {
|
|
501
|
+
const configFile = call.config !== undefined ? resolve(call.config) : configPath(io.env, io.home);
|
|
502
|
+
const config = loadConfig(configFile);
|
|
503
|
+
const asked = call.profile !== undefined ? profileByName(config, call.profile, io.env, io.home) : undefined;
|
|
504
|
+
// With --profile the default is not consulted: a profile never borrows from another.
|
|
505
|
+
const fallback = asked === undefined && config?.default !== undefined ? profileByName(config, config.default, io.env, io.home) : undefined;
|
|
506
|
+
const env = { ...io.env };
|
|
507
|
+
const forChild = {};
|
|
508
|
+
if (call.config !== undefined) forChild.PI_SPACES_CONFIG = configFile;
|
|
509
|
+
if (call.dir !== undefined) {
|
|
510
|
+
// Absolute: a relative one is relative to where the command runs.
|
|
511
|
+
forChild.PI_SPACES_DIR = resolve(call.dir);
|
|
512
|
+
} else if (asked) {
|
|
513
|
+
env.PI_SPACES_DIR = asked.dir;
|
|
514
|
+
// The Pi would find the same folder by itself unless the file names it or PI_SPACES_DIR would win there.
|
|
515
|
+
if (hasOwnDir(config, asked) || io.env.PI_SPACES_DIR) forChild.PI_SPACES_DIR = asked.dir;
|
|
516
|
+
} else if (fallback && !io.env.PI_SPACES_DIR && hasOwnDir(config, fallback)) {
|
|
517
|
+
forChild.PI_SPACES_DIR = fallback.dir;
|
|
518
|
+
}
|
|
519
|
+
const command = choose("command", call, asked, fallback, io.env);
|
|
520
|
+
const setup = choose("setup", call, asked, fallback, io.env);
|
|
521
|
+
if (command && command.origin !== "env") env.PI_SPACES_COMMAND = command.value;
|
|
522
|
+
if (setup && setup.origin !== "env") forChild.PI_SPACES_SETUP = env.PI_SPACES_SETUP = setup.value;
|
|
523
|
+
Object.assign(env, forChild);
|
|
524
|
+
const dir = spacesDir(env, io.home, fallback?.dir);
|
|
525
|
+
return {
|
|
526
|
+
io: { ...io, env, childEnv: Object.keys(forChild).length > 0 ? { ...io.env, ...forChild } : undefined },
|
|
527
|
+
dir,
|
|
528
|
+
config,
|
|
529
|
+
profile: asked ?? fallback,
|
|
530
|
+
commandSource: command?.source ?? "PI_SPACES_COMMAND",
|
|
531
|
+
setupSource: setup?.source ?? "PI_SPACES_SETUP",
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
283
535
|
export async function main(argv , io ) {
|
|
284
536
|
const command = parseCli(argv);
|
|
285
|
-
if (command.dir !== undefined || command.command !== undefined || command.setup !== undefined) {
|
|
286
|
-
// env is this call's view; the started Pi also gets --dir (absolute: a relative one is relative to where
|
|
287
|
-
// the command runs) and --setup, but not --command, which only says how to start it.
|
|
288
|
-
const forChild = {};
|
|
289
|
-
if (command.dir !== undefined) forChild.PI_SPACES_DIR = resolve(command.dir);
|
|
290
|
-
if (command.setup !== undefined) forChild.PI_SPACES_SETUP = command.setup;
|
|
291
|
-
const env = { ...io.env, ...forChild };
|
|
292
|
-
if (command.command !== undefined) env.PI_SPACES_COMMAND = command.command;
|
|
293
|
-
io = { ...io, env, childEnv: Object.keys(forChild).length > 0 ? { ...io.env, ...forChild } : undefined };
|
|
294
|
-
}
|
|
295
|
-
const commandSource = command.command !== undefined ? "--command" : "PI_SPACES_COMMAND";
|
|
296
|
-
const setupSource = command.setup !== undefined ? "--setup" : "PI_SPACES_SETUP";
|
|
297
|
-
const dir = spacesDir(io.env, io.home);
|
|
298
537
|
switch (command.kind) {
|
|
299
538
|
case "help":
|
|
300
539
|
io.stdout.write(USAGE);
|
|
301
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);
|
|
302
548
|
case "version":
|
|
303
549
|
io.stdout.write(`${VERSION}\n`);
|
|
304
550
|
return io.exit(0);
|
|
@@ -311,6 +557,22 @@ export async function main(argv , io ) {
|
|
|
311
557
|
case "usage":
|
|
312
558
|
io.stderr.write(USAGE);
|
|
313
559
|
return io.exit(2);
|
|
560
|
+
case "completion":
|
|
561
|
+
return printCompletion(command.shell, io);
|
|
562
|
+
case "complete":
|
|
563
|
+
return complete(command.words, io);
|
|
564
|
+
}
|
|
565
|
+
let settings ;
|
|
566
|
+
try {
|
|
567
|
+
settings = resolveSettings(command, io);
|
|
568
|
+
} catch (error) {
|
|
569
|
+
if (!(error instanceof ConfigError)) throw error;
|
|
570
|
+
io.stderr.write(`pi-space: ${error.message}\n`);
|
|
571
|
+
return io.exit(2);
|
|
572
|
+
}
|
|
573
|
+
const { dir, profile, commandSource, setupSource } = settings;
|
|
574
|
+
io = settings.io;
|
|
575
|
+
switch (command.kind) {
|
|
314
576
|
case "list": {
|
|
315
577
|
const spaces = listSpaces(dir);
|
|
316
578
|
const lines = [`Spaces in ${dir}:`, ` ${BASE}`, ...spaces.map((name) => ` ${name}`)];
|
|
@@ -321,13 +583,13 @@ export async function main(argv , io ) {
|
|
|
321
583
|
case "packages":
|
|
322
584
|
return runPackages(command.verb, command.target, command.args, dir, io, commandSource);
|
|
323
585
|
case "new":
|
|
324
|
-
return create(command.name, dir, io, setupSource);
|
|
586
|
+
return create(command.name, dir, io, setupSource, profile);
|
|
325
587
|
case "setup":
|
|
326
|
-
return runSetup(command.target, dir, io, setupSource);
|
|
588
|
+
return runSetup(command.target, dir, io, setupSource, profile);
|
|
327
589
|
case "rename":
|
|
328
|
-
return rename(command.from, command.to, dir, io);
|
|
590
|
+
return rename(command.from, command.to, dir, io, profile);
|
|
329
591
|
case "remove":
|
|
330
|
-
return remove(command.name, command.yes, command.backup, dir, io);
|
|
592
|
+
return remove(command.name, command.yes, command.backup, dir, io, profile);
|
|
331
593
|
case "launch":
|
|
332
594
|
return launch(command.name, command.args, dir, io, commandSource);
|
|
333
595
|
case "picker":
|
|
@@ -335,6 +597,173 @@ export async function main(argv , io ) {
|
|
|
335
597
|
}
|
|
336
598
|
}
|
|
337
599
|
|
|
600
|
+
const SHELLS = ["zsh"];
|
|
601
|
+
|
|
602
|
+
/** `pi-space completion <shell>`: the script on stdout, or exit 2 for a shell it does not know. */
|
|
603
|
+
function printCompletion(shell , io ) {
|
|
604
|
+
if (shell === undefined) {
|
|
605
|
+
io.stderr.write(`pi-space: Name a shell: pi-space completion zsh (supported shells: ${SHELLS.join(", ")}).\n`);
|
|
606
|
+
return io.exit(2);
|
|
607
|
+
}
|
|
608
|
+
if (shell !== "zsh") {
|
|
609
|
+
io.stderr.write(`pi-space: Unsupported shell "${shell}"; supported shells: ${SHELLS.join(", ")}.\n`);
|
|
610
|
+
return io.exit(2);
|
|
611
|
+
}
|
|
612
|
+
io.stdout.write(ZSH_COMPLETION);
|
|
613
|
+
return io.exit(0);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/** What `__complete` prints when the shell should complete a file or folder itself. */
|
|
617
|
+
const FILES = "__files__";
|
|
618
|
+
|
|
619
|
+
/**
|
|
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.
|
|
625
|
+
*/
|
|
626
|
+
const ZSH_COMPLETION = [
|
|
627
|
+
"#compdef pi-space",
|
|
628
|
+
"# zsh completion for pi-space. Load it from .zshrc with: source <(pi-space completion zsh)",
|
|
629
|
+
"_pi_space() {",
|
|
630
|
+
" local -a lines cmds spaces profiles shells opts",
|
|
631
|
+
" local line group value text ret=1",
|
|
632
|
+
' lines=("${(@f)$(command "${words[1]}" __complete "${(@)words[2,CURRENT-1]}" 2>/dev/null)}")',
|
|
633
|
+
` if [[ "\${lines[1]}" == ${FILES} ]]; then`,
|
|
634
|
+
" _files",
|
|
635
|
+
" return",
|
|
636
|
+
" fi",
|
|
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",
|
|
659
|
+
"}",
|
|
660
|
+
"compdef _pi_space pi-space",
|
|
661
|
+
"",
|
|
662
|
+
].join("\n");
|
|
663
|
+
|
|
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
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* The hidden `__complete <words before the cursor>`: one candidate per line, honoring the call options already
|
|
674
|
+
* typed. It never fails loudly: whatever goes wrong, it prints nothing and exits 0.
|
|
675
|
+
*/
|
|
676
|
+
function complete(words , io ) {
|
|
677
|
+
let lines = [];
|
|
678
|
+
try {
|
|
679
|
+
lines = candidates(words, io);
|
|
680
|
+
} catch {
|
|
681
|
+
lines = [];
|
|
682
|
+
}
|
|
683
|
+
if (lines.length > 0) io.stdout.write(`${lines.join("\n")}\n`);
|
|
684
|
+
return io.exit(0);
|
|
685
|
+
}
|
|
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
|
+
|
|
730
|
+
function candidates(words , io ) {
|
|
731
|
+
const call = {};
|
|
732
|
+
let index = 0;
|
|
733
|
+
while (Object.hasOwn(CALL_OPTIONS, words[index] ?? "")) {
|
|
734
|
+
const option = words[index] ;
|
|
735
|
+
// The option is the last word: the cursor is on its value.
|
|
736
|
+
if (index + 1 >= words.length) return optionValues(option, call, io);
|
|
737
|
+
call[CALL_OPTIONS[option]] = words[index + 1];
|
|
738
|
+
index += 2;
|
|
739
|
+
}
|
|
740
|
+
const { dir } = resolveSettings(call, io);
|
|
741
|
+
const rest = words.slice(index);
|
|
742
|
+
const spaces = listSpaces(dir);
|
|
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);
|
|
746
|
+
}
|
|
747
|
+
|
|
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. */
|
|
749
|
+
function optionValues(option , call , io ) {
|
|
750
|
+
if (option === "--profile") {
|
|
751
|
+
const { config } = resolveSettings(call, io);
|
|
752
|
+
return profileNames(config).map((name) => candidate("profile", name, profileText(config, name, io)));
|
|
753
|
+
}
|
|
754
|
+
if (option === "--config" || option === "--dir") return [FILES];
|
|
755
|
+
return [];
|
|
756
|
+
}
|
|
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
|
+
|
|
338
767
|
/** Prints the reason and returns false when the setup command has an unclosed quote. */
|
|
339
768
|
function checkSetup(io , source ) {
|
|
340
769
|
try {
|
|
@@ -346,9 +775,11 @@ function checkSetup(io , source ) {
|
|
|
346
775
|
}
|
|
347
776
|
}
|
|
348
777
|
|
|
349
|
-
/** Runs a setup command in a space, as that space's agent home, and returns its exit status. */
|
|
350
|
-
function spawnSetup(setup , name , home , io ) {
|
|
351
|
-
|
|
778
|
+
/** Runs a setup command in a space, as that space's agent home (with the profile's spaceEnv), and returns its exit status. */
|
|
779
|
+
function spawnSetup(setup , name , home , io , profile ) {
|
|
780
|
+
// The profile's spaceEnv, for this space, then the three variables of the space, which a profile cannot set.
|
|
781
|
+
const spaceEnv = profile ? spaceEnvFor(profile, home, name) : {};
|
|
782
|
+
return spawnPi(setup.program, setup.args, io, { ...io.env, ...spaceEnv, PI_CODING_AGENT_DIR: home, PI_SPACE: name, PI_SPACE_HOME: home });
|
|
352
783
|
}
|
|
353
784
|
|
|
354
785
|
/** The setup command for one space, from PI_SPACES_SETUP after the call options; checkSetup has accepted it. */
|
|
@@ -356,17 +787,17 @@ function setupFor(name , home , io )
|
|
|
356
787
|
return setupCommand(io.env.PI_SPACES_SETUP ?? "", home, name);
|
|
357
788
|
}
|
|
358
789
|
|
|
359
|
-
async function create(name , dir , io , setupSource ) {
|
|
790
|
+
async function create(name , dir , io , setupSource , profile ) {
|
|
360
791
|
const withSetup = hasSetupCommand(io.env);
|
|
361
792
|
// A bad setup command is reported before anything is created.
|
|
362
793
|
if (withSetup && !checkSetup(io, setupSource)) return io.exit(2);
|
|
363
794
|
try {
|
|
364
|
-
const path = createSpace(dir, name, newSpaceDeclaration(io.env, io.home));
|
|
795
|
+
const path = createSpace(dir, name, newSpaceDeclaration(io.env, io.home, profile?.home));
|
|
365
796
|
io.stdout.write(`Created space "${name}" at ${path}.\n`);
|
|
366
797
|
if (withSetup) {
|
|
367
798
|
const setup = setupFor(name, path, io);
|
|
368
799
|
io.stdout.write(`Setting it up: ${[setup.program, ...setup.args].join(" ")}\n`);
|
|
369
|
-
const status = await spawnSetup(setup, name, path, io);
|
|
800
|
+
const status = await spawnSetup(setup, name, path, io, profile);
|
|
370
801
|
if (status !== 0) {
|
|
371
802
|
io.stderr.write(`pi-space: Setup failed in space "${name}" (exit status ${status}); the space is kept. Run it again with: pi-space setup ${name}\n`);
|
|
372
803
|
return io.exit(status);
|
|
@@ -381,10 +812,15 @@ async function create(name , dir , io , setupSource )
|
|
|
381
812
|
}
|
|
382
813
|
}
|
|
383
814
|
|
|
815
|
+
/** The base homes a space must not be: the one Pi started with, and the selected profile's. */
|
|
816
|
+
function protectedHomes(io , profile ) {
|
|
817
|
+
return [io.env.PI_SPACES_BASE_HOME, profile?.home].filter((home) => Boolean(home));
|
|
818
|
+
}
|
|
819
|
+
|
|
384
820
|
/** Renames a space after the same checks as remove: not base, not the active space, not one that holds the base home. */
|
|
385
|
-
function rename(from , to , dir , io ) {
|
|
821
|
+
function rename(from , to , dir , io , profile ) {
|
|
386
822
|
try {
|
|
387
|
-
const protect = io
|
|
823
|
+
const protect = protectedHomes(io, profile);
|
|
388
824
|
renameSpace(dir, from, to, agentHome(io.env, io.home), protect, io.home);
|
|
389
825
|
io.stdout.write(`Renamed space "${from}" to "${to}".\n`);
|
|
390
826
|
return io.exit(0);
|
|
@@ -398,9 +834,9 @@ function rename(from , to , dir , io ) {
|
|
|
398
834
|
* Without --yes it asks to remove ([y/N]), then, without --no-backup, whether to keep a backup ([Y/n]).
|
|
399
835
|
* With --yes it asks nothing and keeps a backup unless --no-backup.
|
|
400
836
|
*/
|
|
401
|
-
async function remove(name , yes , backup , dir , io ) {
|
|
837
|
+
async function remove(name , yes , backup , dir , io , profile ) {
|
|
402
838
|
const current = agentHome(io.env, io.home);
|
|
403
|
-
const protect = io
|
|
839
|
+
const protect = protectedHomes(io, profile);
|
|
404
840
|
let reader ;
|
|
405
841
|
try {
|
|
406
842
|
// Fail before asking when the space cannot be removed anyway.
|
|
@@ -550,7 +986,7 @@ async function runPackages(verb , target , args
|
|
|
550
986
|
* Runs the setup command in a space, or in every space with --every (never base), as `pi-space setup`.
|
|
551
987
|
* With --every each run gets a `== <name>` header, one failure does not stop the rest, and the status is 1 if any failed.
|
|
552
988
|
*/
|
|
553
|
-
async function runSetup(target , dir , io , setupSource ) {
|
|
989
|
+
async function runSetup(target , dir , io , setupSource , profile ) {
|
|
554
990
|
if (!hasSetupCommand(io.env)) {
|
|
555
991
|
io.stderr.write("pi-space: There is no setup command; set PI_SPACES_SETUP or give --setup <cmd>.\n");
|
|
556
992
|
return io.exit(2);
|
|
@@ -563,7 +999,7 @@ async function runSetup(target , dir , io , setupSour
|
|
|
563
999
|
}
|
|
564
1000
|
if (!checkSpace(target.name, dir, io)) return io.exit(1);
|
|
565
1001
|
const home = join(dir, target.name);
|
|
566
|
-
return io.exit(await spawnSetup(setupFor(target.name, home, io), target.name, home, io));
|
|
1002
|
+
return io.exit(await spawnSetup(setupFor(target.name, home, io), target.name, home, io, profile));
|
|
567
1003
|
}
|
|
568
1004
|
const names = listSpaces(dir);
|
|
569
1005
|
if (names.length === 0) {
|
|
@@ -574,7 +1010,7 @@ async function runSetup(target , dir , io , setupSour
|
|
|
574
1010
|
for (const name of names) {
|
|
575
1011
|
io.stdout.write(`== ${name}\n`);
|
|
576
1012
|
const home = join(dir, name);
|
|
577
|
-
if ((await spawnSetup(setupFor(name, home, io), name, home, io)) !== 0) failed.push(name);
|
|
1013
|
+
if ((await spawnSetup(setupFor(name, home, io), name, home, io, profile)) !== 0) failed.push(name);
|
|
578
1014
|
}
|
|
579
1015
|
if (failed.length > 0) {
|
|
580
1016
|
io.stderr.write(`pi-space: setup failed in: ${failed.join(", ")}\n`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jals1212/pi-spaces-cli",
|
|
3
|
-
"version": "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",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"cross-spawn": "^7.0.6",
|
|
42
|
-
"@jals1212/pi-spaces": "^0.
|
|
42
|
+
"@jals1212/pi-spaces": "^0.6.0"
|
|
43
43
|
},
|
|
44
44
|
"publishConfig": {
|
|
45
45
|
"access": "public",
|