@jals1212/pi-spaces-cli 0.4.0 → 0.6.0

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,61 @@ commands and options and a patch version fixes.
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 0.6.0
10
+
11
+ ### Added
12
+
13
+ - Profiles: `--profile <name>` and `--config <path>`, before the command, use a
14
+ named profile of the config file (`~/.pi-spaces/config.json`, or
15
+ `PI_SPACES_CONFIG`, or `--config`) for the command, the spaces folder and the
16
+ setup command, and the file's `default` profile is used without `--profile`.
17
+ Each setting is taken from the call option, then the `--profile` profile,
18
+ then `PI_SPACES_COMMAND`, `PI_SPACES_DIR` or `PI_SPACES_SETUP`, then the
19
+ default profile, then the built-in. The built-in profile `pi` always exists.
20
+ With no file, nothing changes.
21
+ - A profile's `spaceEnv` is added, filled in for the space, to the environment
22
+ of the setup command (`new` and `setup`), whichever setup command runs.
23
+ - A profile's `home` is the base home a new space's declaration of the extension
24
+ is taken from, and `remove` and `rename` refuse the space that holds it.
25
+ - The started Pi gets `--config` as `PI_SPACES_CONFIG`, and a profile's `dir`
26
+ and `setup` as `PI_SPACES_DIR` and `PI_SPACES_SETUP`, as `--dir` and `--setup`
27
+ do.
28
+ - `pi-space completion zsh` prints a zsh completion script (`source
29
+ <(pi-space completion zsh)`): commands, profile names after `--profile`,
30
+ files after `--config` and `--dir`, and the spaces of the profile, config or
31
+ folder already typed. It calls the hidden `pi-space __complete`, which never
32
+ prints an error.
33
+ - A config file that is wrong, or an unknown profile, exits with status 2 and
34
+ prints the problem.
35
+
36
+ ### Changed
37
+
38
+ - `completion` is a command word, so it can no longer name a space.
39
+ - Needs `@jals1212/pi-spaces` 0.6.0, which reads the config file
40
+ (`@jals1212/pi-spaces/config`) and reserves the name `completion`.
41
+
42
+ ## 0.5.0
43
+
44
+ ### Added
45
+
46
+ - A setup command for new spaces: `--setup <cmd>` or `PI_SPACES_SETUP`. `pi-space
47
+ new` runs it in the new space, and `pi-space setup <name|--every>` runs it
48
+ again (never in `base`). `{home}` and `{name}` become the space's folder and
49
+ name, and it runs with `PI_CODING_AGENT_DIR`, `PI_SPACE_HOME` and `PI_SPACE`
50
+ set to the space, so a program that runs Pi can install what it needs there,
51
+ for example `pi-space --setup 'my-launcher --home {home} setup' new work`. A
52
+ failed setup keeps the space and names the command to run it again.
53
+ - `--setup` reaches the started Pi as `PI_SPACES_SETUP`.
54
+
55
+ ### Fixed
56
+
57
+ - A blank `--command` or `--dir` (only spaces) is a usage error, as an empty one
58
+ already was, instead of being taken as given.
59
+
60
+ ### Changed
61
+
62
+ - Needs `@jals1212/pi-spaces` 0.5.0, which reserves the name `setup`.
63
+
9
64
  ## 0.4.0
10
65
 
11
66
  ### 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, 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` or `uninstall`, so any other first word is a space name.
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
  | --- | --- |
@@ -20,12 +20,14 @@ The command words are reserved: a space can never be named `list`, `new`, `remov
20
20
  | `pi-space <name> [pi args...]` | Starts Pi in that space (`base` for the home Pi starts with). |
21
21
  | `pi-space list` | Lists the spaces. |
22
22
  | `pi-space list <name\|--every>` | Lists the packages of a space (Pi's `list`). |
23
- | `pi-space new <name>` | Creates an empty space, as `/space new <name>` does in Pi. It does not start Pi. |
23
+ | `pi-space new <name>` | Creates an empty space, as `/space new <name>` does in Pi, then runs the setup command in it when there is one (see [Preparing a new space](#preparing-a-new-space)). It does not start Pi. |
24
+ | `pi-space setup <name\|--every>` | Runs the setup command in a space, or in every space. Never in `base`. |
24
25
  | `pi-space rename <old> <new>` | Renames a space, as `/space rename` does in Pi. It refuses what `remove` refuses (see below). |
25
26
  | `pi-space remove <name> [--yes] [--no-backup]` | Deletes a space, as `/space remove <name>` does in Pi. It asks first (`[y/N]`), then whether to keep a backup in `<spaces folder>/.backups` (`[Y/n]`: Enter keeps one). `--yes` skips the questions and keeps a backup, and is required without a terminal; `--no-backup` deletes without one. |
26
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. |
27
28
  | `pi-space install <name\|--every> <source> [pi install options...]` | Runs Pi's `install` in a space. The source is required. |
28
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. |
29
31
  | `pi-space --help` | Shows the usage. |
30
32
  | `pi-space --version` | Prints the version. |
31
33
 
@@ -50,7 +52,7 @@ A new space gets a `settings.json` that declares the extension, so `/space` work
50
52
 
51
53
  ## Another program that runs Pi
52
54
 
53
- 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:
55
+ 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:
54
56
 
55
57
  ```sh
56
58
  pi-space --command "my-launcher --" --dir ~/.my-launcher/spaces work # → my-launcher -- --space work
@@ -71,15 +73,134 @@ export PI_SPACES_DIR=~/.my-launcher/spaces # the spaces next to that program's
71
73
  | --- | --- |
72
74
  | `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. |
73
75
  | `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. |
76
+ | `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. |
77
+ | `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. |
74
78
  | `PI_CODING_AGENT_DIR` | Pi's agent home; its parent holds the default spaces folder. Default: `~/.pi/agent`. |
75
79
 
80
+ ## Profiles
81
+
82
+ 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).
83
+
84
+ ```sh
85
+ pi-space --profile ml list # the spaces of the profile "ml"
86
+ pi-space --profile ml work # start the profile's program in its space "work"
87
+ pi-space --profile ml new work # create it, then run the profile's setup in it
88
+ pi-space --config ./other.json --profile ml list # the same profile from another file, for this call
89
+ ```
90
+
91
+ `--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.`
92
+
93
+ - **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.
94
+ - **`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.
95
+ - **`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.
96
+
97
+ ### Which value wins
98
+
99
+ For each of the command, the spaces folder and the setup command, the first of these that has a value is used:
100
+
101
+ 1. the call option (`--command`, `--dir`, `--setup`);
102
+ 2. the profile named with `--profile`;
103
+ 3. the variable (`PI_SPACES_COMMAND`, `PI_SPACES_DIR`, `PI_SPACES_SETUP`);
104
+ 4. the file's `default` profile;
105
+ 5. the built-in: `pi`, the `spaces` folder next to the base home, no setup command.
106
+
107
+ 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.
108
+
109
+ ### What the started Pi is told
110
+
111
+ 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:
112
+
113
+ - `--config <path>` becomes `PI_SPACES_CONFIG` (absolute), like `--dir` becomes `PI_SPACES_DIR`.
114
+ - 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`.
115
+ - Nothing else is added: when nothing is to be passed, the child inherits your environment as it is. `--command` is never passed.
116
+
117
+ ### `spaceEnv` and the setup command
118
+
119
+ 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.
120
+
121
+ ### A complete example
122
+
123
+ A program called `my-launcher` keeps its agent home in `~/.my-launcher/agent` and its own configuration outside it. `~/.pi-spaces/config.json`:
124
+
125
+ ```json
126
+ {
127
+ "schema": "pi-spaces.config/v1",
128
+ "profiles": {
129
+ "my": {
130
+ "home": "~/.my-launcher/agent",
131
+ "command": "my-launcher --",
132
+ "setup": "my-launcher --home {home} setup",
133
+ "spaceEnv": { "MY_LAUNCHER_CONFIG_HOME": "{home}/config" }
134
+ }
135
+ }
136
+ }
137
+ ```
138
+
139
+ Then, without a variable, an alias or a function:
140
+
141
+ ```sh
142
+ pi-space --profile my new work # creates ~/.my-launcher/spaces/work, then runs:
143
+ # my-launcher --home ~/.my-launcher/spaces/work setup
144
+ # with MY_LAUNCHER_CONFIG_HOME=~/.my-launcher/spaces/work/config
145
+ pi-space --profile my work -c # my-launcher -- --space work -c
146
+ pi-space --profile my setup --every # run the setup again in every space of "my"
147
+ pi-space --profile pi list # the spaces of plain Pi, from the same shell
148
+ ```
149
+
150
+ Add `"default": "my"` to the file and `pi-space work` alone does the second line; `pi-space --profile pi work` is then plain Pi.
151
+
152
+ ### Shortening it (optional)
153
+
154
+ 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.
155
+
156
+ ```sh
157
+ abbr my-space="pi-space --profile my" # zsh-abbr; typing `my-space work` expands to `pi-space --profile my work`
158
+ alias my-space='pi-space --profile my' # any POSIX shell
159
+ ```
160
+
161
+ ## Shell completion
162
+
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`):
164
+
165
+ ```sh
166
+ source <(pi-space completion zsh)
167
+ ```
168
+
169
+ 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:
170
+
171
+ | After | Candidates |
172
+ | --- | --- |
173
+ | `pi-space` (and the call options) | the commands, `base` and the spaces |
174
+ | `--profile` | the profile names, `pi` included |
175
+ | `--config`, `--dir` | files (the shell's own completion) |
176
+ | `remove`, `rename` | the spaces |
177
+ | `setup` | the spaces and `--every` |
178
+ | `list`, `update`, `install`, `uninstall` | `base`, the spaces and `--every` |
179
+ | `completion` | `zsh` |
180
+
181
+ 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
+
183
+ ## Preparing a new space
184
+
185
+ 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:
186
+
187
+ ```sh
188
+ pi-space --setup 'my-launcher --home {home} setup' new work # create "work", then: my-launcher --home ~/.pi/spaces/work setup
189
+ pi-space --setup 'pi install npm:some-package' new work # plain Pi: install a package in the new space
190
+ pi-space --setup 'my-launcher --home {home} setup' setup --every # run it again in every space, for example after an update
191
+ ```
192
+
193
+ When the setup fails, the space is kept and `pi-space` exits with the setup's status, naming `pi-space setup <name>` to run it again. `pi-space setup <name>` ends with the setup's status. With `--every` it runs in every space in order, never in `base`, each after a `== <name>` line, and exits as the package commands do. `base` is the home the program prepares itself.
194
+
195
+ Only those three variables change. A variable that points into the base home stays as it is, so a setup that needs one per space sets it itself, for example `env MY_CONFIG={home}/config my-launcher --home {home} setup`. `--setup` also reaches the Pi that `pi-space` starts as `PI_SPACES_SETUP`, so `/space new` there names `pi-space setup <name>`.
196
+
76
197
  ## Exit status
77
198
 
78
199
  | Status | Meaning |
79
200
  | --- | --- |
80
- | 0 … | Pi's own exit status, for a space and for a package command in one space. |
81
- | 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). |
82
- | 2 | 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` or `--dir` without a value or given twice: the usage is printed. The old `--new` and `--remove`: the replacing command is printed. A `--command` or `PI_SPACES_COMMAND` with an unclosed quote: the problem is printed. |
201
+ | 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. |
202
+ | 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. |
203
+ | 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. |
83
204
 
84
205
  ## More
85
206
 
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
- // switch, so it never sets PI_CODING_AGENT_DIR.
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";
@@ -15,6 +16,7 @@ import {
15
16
  createSpace,
16
17
 
17
18
  findOwnDeclaration,
19
+ hasSetupCommand,
18
20
  listSpaces,
19
21
 
20
22
  removeSpace,
@@ -23,35 +25,53 @@ import {
23
25
  spacesDir,
24
26
  validateName,
25
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";
26
38
 
27
- export const USAGE = `Usage: pi-space [--command <cmd>] [--dir <path>] [<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> ...]
28
40
  pi-space --help | --version
29
41
  pi-space pick a space and start Pi in it
30
42
  pi-space <name> [pi args...] start Pi in a space ("base" for the home Pi starts with)
31
43
  pi-space list list the spaces
32
44
  pi-space list <name|--every> list the packages of a space
33
- pi-space new <name> create a space
45
+ pi-space new <name> create a space, then run the setup command in it when there is one
46
+ pi-space setup <name|--every> run the setup command in a space, or in every space (never in base)
34
47
  pi-space rename <old> <new> rename a space
35
48
  pi-space remove <name> delete a space and everything in it, after asking, and keep a backup in .backups unless told not to
36
49
  (--yes skips the questions; --no-backup deletes without a backup)
37
50
  pi-space update <name|--every> update the packages of a space (more words go to Pi: --extensions, --all, a source)
38
51
  pi-space install <name|--every> <source> install a package in a space (more words go to Pi)
39
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))
40
54
  pi-space --help print this help
41
55
  pi-space --version print the version
42
- --every takes the place of the name: base first, then every space in turn; a failure does not stop the rest, and the exit status is 1 if any failed.
56
+ --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.
43
57
  Options before the command, for this call only:
44
58
  --command <cmd> the program to run instead of $PI_SPACES_COMMAND (default: pi), e.g. "my-launcher --"
45
59
  --dir <path> the spaces folder instead of $PI_SPACES_DIR; the Pi it starts gets it too
60
+ --setup <cmd> the setup command instead of $PI_SPACES_SETUP; {home} and {name} become the space's folder and name
61
+ --profile <name> the profile of the config file to use instead of its "default": its command, spaces folder, setup and spaceEnv
62
+ --config <path> the config file instead of $PI_SPACES_CONFIG (default: ~/.pi-spaces/config.json); the Pi it starts gets it too
46
63
  `;
47
64
 
48
65
  /** This package's version; package.json sits one folder above both src/ and dist/. */
49
66
  export const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
50
67
 
51
- /** --command and --dir: for this call only, so nothing has to be exported into the shell. */
68
+ /** --command, --dir, --setup, --profile and --config: for this call only, so nothing has to be exported into the shell. */
52
69
 
53
70
 
54
71
 
72
+
73
+
74
+
55
75
 
56
76
 
57
77
  /** The Pi commands that pi-space runs in one space or in every space. */
@@ -76,20 +96,24 @@ const REPLACED = {
76
96
 
77
97
 
78
98
 
99
+
79
100
 
80
101
 
102
+
103
+
81
104
 
82
105
 
83
106
 
84
107
 
85
108
 
86
109
 
87
- const CALL_OPTIONS = { "--command": "command", "--dir": "dir" } ;
110
+ const CALL_OPTIONS = { "--command": "command", "--dir": "dir", "--setup": "setup", "--profile": "profile", "--config": "config" } ;
88
111
 
89
112
  /**
90
- * --command and --dir come first; after a space name every argument belongs to Pi.
91
- * The commands (list, new, remove, rename, update, install, uninstall) are reserved words, so they are read
92
- * before a name: the core never lets a space have one of them as its name.
113
+ * The call options (--command, --dir, --setup, --profile, --config) come first; after a space name every argument belongs to Pi.
114
+ * The commands (list, new, setup, remove, rename, update, install, uninstall, completion) are reserved words, so they
115
+ * are read before a name: the core never lets a space have one of them as its name. `__complete` is the hidden
116
+ * command the completion script calls; it starts with "_", which no space name does.
93
117
  * A space name never starts with "-", so any other option in first place is unknown, not a name.
94
118
  */
95
119
  export function parseCli(args ) {
@@ -98,7 +122,8 @@ export function parseCli(args ) {
98
122
  while (Object.hasOwn(CALL_OPTIONS, args[index] ?? "")) {
99
123
  const key = CALL_OPTIONS[args[index] ];
100
124
  const value = args[index + 1];
101
- if (!value || options[key] !== undefined) return { kind: "usage" };
125
+ // A blank value is as good as none: it would name no program, folder or command.
126
+ if (!value?.trim() || options[key] !== undefined) return { kind: "usage" };
102
127
  options[key] = value;
103
128
  index += 2;
104
129
  }
@@ -116,6 +141,9 @@ function parseCommand(args ) {
116
141
  if (first === "list" && rest.length === 0) return { kind: "list" };
117
142
  if (PACKAGE_VERBS.includes(first)) return parsePackages(first , rest);
118
143
  if (first === "new") return rest.length === 1 ? { kind: "new", name: rest[0] } : { kind: "usage" };
144
+ if (first === "setup") return parseSetup(rest);
145
+ if (first === "completion") return rest.length <= 1 ? { kind: "completion", ...(rest.length === 1 ? { shell: rest[0] } : {}) } : { kind: "usage" };
146
+ if (first === "__complete") return { kind: "complete", words: rest };
119
147
  if (first === "rename") return rest.length === 2 ? { kind: "rename", from: rest[0], to: rest[1] } : { kind: "usage" };
120
148
  if (first === "remove") {
121
149
  const flags = rest.filter((arg) => arg.startsWith("-"));
@@ -128,6 +156,14 @@ function parseCommand(args ) {
128
156
  return { kind: "launch", name: first, args: rest };
129
157
  }
130
158
 
159
+ /** `setup <name>` or `setup --every`: exactly one argument. */
160
+ function parseSetup(rest ) {
161
+ if (rest.length !== 1) return { kind: "usage" };
162
+ if (rest[0] === "--every") return { kind: "setup", target: { every: true } };
163
+ if (rest[0].startsWith("-")) return { kind: "unknown-option", option: rest[0] };
164
+ return { kind: "setup", target: { name: rest[0] } };
165
+ }
166
+
131
167
  /**
132
168
  * `<verb> <name|--every> [args...]`. list takes nothing after the target; install and uninstall need a source
133
169
  * (the first argument after it); update may have nothing, which is Pi's own default.
@@ -150,6 +186,17 @@ export function launchCommand(env , name , args )
150
186
  return { program, args: [...prefix, "--space", name, ...args] };
151
187
  }
152
188
 
189
+ /**
190
+ * The program and arguments of the setup command for one space: the template split like --command, with every
191
+ * {home} and {name} in each word replaced by the space's folder and name. Throws when it has an unclosed quote.
192
+ */
193
+ export function setupCommand(template , home , name ) {
194
+ // Replacer functions, so a "$" in the folder or name is put in as written, never read as a pattern.
195
+ const fill = (word ) => word.replaceAll("{home}", () => home).replaceAll("{name}", () => name);
196
+ const [program = "", ...args] = splitCommand(template).map(fill);
197
+ return { program, args };
198
+ }
199
+
153
200
  /**
154
201
  * Splits a command into words on whitespace. Double or single quotes keep whitespace inside a word and are
155
202
  * removed. There are no backslash escapes, so a Windows path in quotes keeps its backslashes.
@@ -185,10 +232,11 @@ const EXTENSION = "@jals1212/pi-spaces";
185
232
 
186
233
  /**
187
234
  * The extension's declaration for a new space's settings.json: the one in the base home's settings.json
188
- * (PI_SPACES_BASE_HOME, else PI_CODING_AGENT_DIR, else ~/.pi/agent), else npm:@jals1212/pi-spaces.
235
+ * (the selected profile's home, else PI_SPACES_BASE_HOME, else PI_CODING_AGENT_DIR, else ~/.pi/agent),
236
+ * else npm:@jals1212/pi-spaces.
189
237
  */
190
- export function newSpaceDeclaration(env , home ) {
191
- const baseHome = env.PI_SPACES_BASE_HOME || agentHome(env, home);
238
+ export function newSpaceDeclaration(env , home , profileHome ) {
239
+ const baseHome = profileHome || env.PI_SPACES_BASE_HOME || agentHome(env, home);
192
240
  let packageRoot ;
193
241
  try {
194
242
  packageRoot = dirname(fileURLToPath(import.meta.resolve(`${EXTENSION}/package.json`)));
@@ -205,7 +253,7 @@ function agentHome(env , home ) {
205
253
 
206
254
 
207
255
 
208
-
256
+
209
257
 
210
258
 
211
259
 
@@ -219,7 +267,7 @@ function agentHome(env , home ) {
219
267
 
220
268
 
221
269
 
222
-
270
+
223
271
 
224
272
 
225
273
 
@@ -254,17 +302,86 @@ export function processIo(proc = process) {
254
302
  };
255
303
  }
256
304
 
305
+ /** Where a setting came from, which decides what the started Pi is told and how an error names it. */
306
+
307
+
308
+ /** What one call settled: its environment, the spaces folder, the profile it runs under and where each setting came from. */
309
+
310
+
311
+
312
+
313
+
314
+
315
+
316
+
317
+
318
+
319
+
320
+ /** Whether the file itself gives the profile a `dir` (the resolved one always has one). */
321
+ function hasOwnDir(config , profile ) {
322
+ return config?.profiles.find((spec) => spec.name === profile.name)?.dir !== undefined;
323
+ }
324
+
325
+ /**
326
+ * The command or setup of this call, with where it came from: the call option, then the --profile profile, then
327
+ * the environment variable, then the default profile. A blank variable is none.
328
+ */
329
+ function choose(field , call , asked , fallback , env ) {
330
+ const variable = field === "command" ? "PI_SPACES_COMMAND" : "PI_SPACES_SETUP";
331
+ const named = (profile ) => `profile "${profile?.name}" ${field}`;
332
+ const candidates = [
333
+ [call[field], "option", `--${field}`],
334
+ [asked?.[field], "profile", named(asked)],
335
+ [env[variable]?.trim() ? env[variable] : undefined, "env", variable],
336
+ [fallback?.[field], "profile", named(fallback)],
337
+ ];
338
+ const found = candidates.find(([value]) => value !== undefined);
339
+ return found && { value: found[0] , origin: found[1], source: found[2] };
340
+ }
341
+
342
+ /**
343
+ * Reads the config file, picks the profile and applies the precedence: call option > --profile > PI_SPACES_* >
344
+ * the default profile > built-in. The call's own environment gets the winners (so the rest of the CLI reads
345
+ * PI_SPACES_COMMAND, PI_SPACES_DIR and PI_SPACES_SETUP as ever); the started Pi gets --dir, --setup, --config and
346
+ * what a profile supplies for the folder and the setup, but never --command. Throws a ConfigError.
347
+ */
348
+ function resolveSettings(call , io ) {
349
+ const configFile = call.config !== undefined ? resolve(call.config) : configPath(io.env, io.home);
350
+ const config = loadConfig(configFile);
351
+ const asked = call.profile !== undefined ? profileByName(config, call.profile, io.env, io.home) : undefined;
352
+ // With --profile the default is not consulted: a profile never borrows from another.
353
+ const fallback = asked === undefined && config?.default !== undefined ? profileByName(config, config.default, io.env, io.home) : undefined;
354
+ const env = { ...io.env };
355
+ const forChild = {};
356
+ if (call.config !== undefined) forChild.PI_SPACES_CONFIG = configFile;
357
+ if (call.dir !== undefined) {
358
+ // Absolute: a relative one is relative to where the command runs.
359
+ forChild.PI_SPACES_DIR = resolve(call.dir);
360
+ } else if (asked) {
361
+ env.PI_SPACES_DIR = asked.dir;
362
+ // The Pi would find the same folder by itself unless the file names it or PI_SPACES_DIR would win there.
363
+ if (hasOwnDir(config, asked) || io.env.PI_SPACES_DIR) forChild.PI_SPACES_DIR = asked.dir;
364
+ } else if (fallback && !io.env.PI_SPACES_DIR && hasOwnDir(config, fallback)) {
365
+ forChild.PI_SPACES_DIR = fallback.dir;
366
+ }
367
+ const command = choose("command", call, asked, fallback, io.env);
368
+ const setup = choose("setup", call, asked, fallback, io.env);
369
+ if (command && command.origin !== "env") env.PI_SPACES_COMMAND = command.value;
370
+ if (setup && setup.origin !== "env") forChild.PI_SPACES_SETUP = env.PI_SPACES_SETUP = setup.value;
371
+ Object.assign(env, forChild);
372
+ const dir = spacesDir(env, io.home, fallback?.dir);
373
+ return {
374
+ io: { ...io, env, childEnv: Object.keys(forChild).length > 0 ? { ...io.env, ...forChild } : undefined },
375
+ dir,
376
+ config,
377
+ profile: asked ?? fallback,
378
+ commandSource: command?.source ?? "PI_SPACES_COMMAND",
379
+ setupSource: setup?.source ?? "PI_SPACES_SETUP",
380
+ };
381
+ }
382
+
257
383
  export async function main(argv , io ) {
258
384
  const command = parseCli(argv);
259
- if (command.dir !== undefined || command.command !== undefined) {
260
- // A relative --dir is relative to where the command runs; the child gets it absolute.
261
- const env = { ...io.env };
262
- if (command.dir !== undefined) env.PI_SPACES_DIR = resolve(command.dir);
263
- if (command.command !== undefined) env.PI_SPACES_COMMAND = command.command;
264
- io = { ...io, env, childEnv: command.dir !== undefined ? { ...io.env, PI_SPACES_DIR: env.PI_SPACES_DIR } : undefined };
265
- }
266
- const commandSource = command.command !== undefined ? "--command" : "PI_SPACES_COMMAND";
267
- const dir = spacesDir(io.env, io.home);
268
385
  switch (command.kind) {
269
386
  case "help":
270
387
  io.stdout.write(USAGE);
@@ -281,6 +398,22 @@ export async function main(argv , io ) {
281
398
  case "usage":
282
399
  io.stderr.write(USAGE);
283
400
  return io.exit(2);
401
+ case "completion":
402
+ return printCompletion(command.shell, io);
403
+ case "complete":
404
+ return complete(command.words, io);
405
+ }
406
+ let settings ;
407
+ try {
408
+ settings = resolveSettings(command, io);
409
+ } catch (error) {
410
+ if (!(error instanceof ConfigError)) throw error;
411
+ io.stderr.write(`pi-space: ${error.message}\n`);
412
+ return io.exit(2);
413
+ }
414
+ const { dir, profile, commandSource, setupSource } = settings;
415
+ io = settings.io;
416
+ switch (command.kind) {
284
417
  case "list": {
285
418
  const spaces = listSpaces(dir);
286
419
  const lines = [`Spaces in ${dir}:`, ` ${BASE}`, ...spaces.map((name) => ` ${name}`)];
@@ -291,11 +424,13 @@ export async function main(argv , io ) {
291
424
  case "packages":
292
425
  return runPackages(command.verb, command.target, command.args, dir, io, commandSource);
293
426
  case "new":
294
- return create(command.name, dir, io);
427
+ return create(command.name, dir, io, setupSource, profile);
428
+ case "setup":
429
+ return runSetup(command.target, dir, io, setupSource, profile);
295
430
  case "rename":
296
- return rename(command.from, command.to, dir, io);
431
+ return rename(command.from, command.to, dir, io, profile);
297
432
  case "remove":
298
- return remove(command.name, command.yes, command.backup, dir, io);
433
+ return remove(command.name, command.yes, command.backup, dir, io, profile);
299
434
  case "launch":
300
435
  return launch(command.name, command.args, dir, io, commandSource);
301
436
  case "picker":
@@ -303,10 +438,144 @@ export async function main(argv , io ) {
303
438
  }
304
439
  }
305
440
 
306
- function create(name , dir , io ) {
441
+ const SHELLS = ["zsh"];
442
+
443
+ /** `pi-space completion <shell>`: the script on stdout, or exit 2 for a shell it does not know. */
444
+ function printCompletion(shell , io ) {
445
+ if (shell === undefined) {
446
+ io.stderr.write(`pi-space: Name a shell: pi-space completion zsh (supported shells: ${SHELLS.join(", ")}).\n`);
447
+ return io.exit(2);
448
+ }
449
+ if (shell !== "zsh") {
450
+ io.stderr.write(`pi-space: Unsupported shell "${shell}"; supported shells: ${SHELLS.join(", ")}.\n`);
451
+ return io.exit(2);
452
+ }
453
+ io.stdout.write(ZSH_COMPLETION);
454
+ return io.exit(0);
455
+ }
456
+
457
+ /** What `__complete` prints when the shell should complete a file or folder itself. */
458
+ const FILES = "__files__";
459
+
460
+ /**
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.
464
+ */
465
+ const ZSH_COMPLETION = [
466
+ "#compdef pi-space",
467
+ "# zsh completion for pi-space. Load it from .zshrc with: source <(pi-space completion zsh)",
468
+ "_pi_space() {",
469
+ " local -a lines",
470
+ ' lines=("${(@f)$(command "${words[1]}" __complete "${(@)words[2,CURRENT-1]}" 2>/dev/null)}")',
471
+ ` if [[ "\${lines[1]}" == ${FILES} ]]; then`,
472
+ " _files",
473
+ " return",
474
+ " fi",
475
+ ' lines=("${(@)lines:#}")',
476
+ " compadd -a lines",
477
+ "}",
478
+ "compdef _pi_space pi-space",
479
+ "",
480
+ ].join("\n");
481
+
482
+ const COMMAND_WORDS = ["list", "new", "setup", "rename", "remove", "update", "install", "uninstall", "completion"];
483
+
484
+ /**
485
+ * The hidden `__complete <words before the cursor>`: one candidate per line, honoring the call options already
486
+ * typed. It never fails loudly: whatever goes wrong, it prints nothing and exits 0.
487
+ */
488
+ function complete(words , io ) {
489
+ let lines = [];
490
+ try {
491
+ lines = candidates(words, io);
492
+ } catch {
493
+ lines = [];
494
+ }
495
+ if (lines.length > 0) io.stdout.write(`${lines.join("\n")}\n`);
496
+ return io.exit(0);
497
+ }
498
+
499
+ function candidates(words , io ) {
500
+ const call = {};
501
+ let index = 0;
502
+ while (Object.hasOwn(CALL_OPTIONS, words[index] ?? "")) {
503
+ const option = words[index] ;
504
+ // The option is the last word: the cursor is on its value.
505
+ if (index + 1 >= words.length) return optionValues(option, call, io);
506
+ call[CALL_OPTIONS[option]] = words[index + 1];
507
+ index += 2;
508
+ }
509
+ const { dir } = resolveSettings(call, io);
510
+ const rest = words.slice(index);
511
+ 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
+ }
530
+ }
531
+
532
+ /** The candidates for the value of a call option: profile names, the shell's files for a path, nothing for a command. */
533
+ function optionValues(option , call , io ) {
534
+ if (option === "--profile") return profileNames(resolveSettings(call, io).config);
535
+ if (option === "--config" || option === "--dir") return [FILES];
536
+ return [];
537
+ }
538
+
539
+ /** Prints the reason and returns false when the setup command has an unclosed quote. */
540
+ function checkSetup(io , source ) {
541
+ try {
542
+ splitCommand(io.env.PI_SPACES_SETUP ?? "");
543
+ return true;
544
+ } catch (error) {
545
+ io.stderr.write(`pi-space: ${source}: ${(error ).message}.\n`);
546
+ return false;
547
+ }
548
+ }
549
+
550
+ /** Runs a setup command in a space, as that space's agent home (with the profile's spaceEnv), and returns its exit status. */
551
+ function spawnSetup(setup , name , home , io , profile ) {
552
+ // The profile's spaceEnv, for this space, then the three variables of the space, which a profile cannot set.
553
+ const spaceEnv = profile ? spaceEnvFor(profile, home, name) : {};
554
+ return spawnPi(setup.program, setup.args, io, { ...io.env, ...spaceEnv, PI_CODING_AGENT_DIR: home, PI_SPACE: name, PI_SPACE_HOME: home });
555
+ }
556
+
557
+ /** The setup command for one space, from PI_SPACES_SETUP after the call options; checkSetup has accepted it. */
558
+ function setupFor(name , home , io ) {
559
+ return setupCommand(io.env.PI_SPACES_SETUP ?? "", home, name);
560
+ }
561
+
562
+ async function create(name , dir , io , setupSource , profile ) {
563
+ const withSetup = hasSetupCommand(io.env);
564
+ // A bad setup command is reported before anything is created.
565
+ if (withSetup && !checkSetup(io, setupSource)) return io.exit(2);
307
566
  try {
308
- const path = createSpace(dir, name, newSpaceDeclaration(io.env, io.home));
309
- io.stdout.write(`Created space "${name}" at ${path}.\nStart Pi in it with: pi-space ${name}\n`);
567
+ const path = createSpace(dir, name, newSpaceDeclaration(io.env, io.home, profile?.home));
568
+ io.stdout.write(`Created space "${name}" at ${path}.\n`);
569
+ if (withSetup) {
570
+ const setup = setupFor(name, path, io);
571
+ io.stdout.write(`Setting it up: ${[setup.program, ...setup.args].join(" ")}\n`);
572
+ const status = await spawnSetup(setup, name, path, io, profile);
573
+ if (status !== 0) {
574
+ 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`);
575
+ return io.exit(status);
576
+ }
577
+ }
578
+ io.stdout.write(`Start Pi in it with: pi-space ${name}\n`);
310
579
  return io.exit(0);
311
580
  } catch (error) {
312
581
  // A SpaceError is a rule (a bad or taken name); anything else is the filesystem, such as no permission.
@@ -315,10 +584,15 @@ function create(name , dir , io ) {
315
584
  }
316
585
  }
317
586
 
587
+ /** The base homes a space must not be: the one Pi started with, and the selected profile's. */
588
+ function protectedHomes(io , profile ) {
589
+ return [io.env.PI_SPACES_BASE_HOME, profile?.home].filter((home) => Boolean(home));
590
+ }
591
+
318
592
  /** Renames a space after the same checks as remove: not base, not the active space, not one that holds the base home. */
319
- function rename(from , to , dir , io ) {
593
+ function rename(from , to , dir , io , profile ) {
320
594
  try {
321
- const protect = io.env.PI_SPACES_BASE_HOME ? [io.env.PI_SPACES_BASE_HOME] : [];
595
+ const protect = protectedHomes(io, profile);
322
596
  renameSpace(dir, from, to, agentHome(io.env, io.home), protect, io.home);
323
597
  io.stdout.write(`Renamed space "${from}" to "${to}".\n`);
324
598
  return io.exit(0);
@@ -332,9 +606,9 @@ function rename(from , to , dir , io ) {
332
606
  * Without --yes it asks to remove ([y/N]), then, without --no-backup, whether to keep a backup ([Y/n]).
333
607
  * With --yes it asks nothing and keeps a backup unless --no-backup.
334
608
  */
335
- async function remove(name , yes , backup , dir , io ) {
609
+ async function remove(name , yes , backup , dir , io , profile ) {
336
610
  const current = agentHome(io.env, io.home);
337
- const protect = io.env.PI_SPACES_BASE_HOME ? [io.env.PI_SPACES_BASE_HOME] : [];
611
+ const protect = protectedHomes(io, profile);
338
612
  let reader ;
339
613
  try {
340
614
  // Fail before asking when the space cannot be removed anyway.
@@ -423,10 +697,13 @@ function checkSpace(name , dir , io ) {
423
697
  return true;
424
698
  }
425
699
 
426
- /** Runs the command with Pi's `args` in a space and returns its exit status; 1, with the reason printed, when it cannot start. */
427
- async function spawnPi(program , programArgs , io ) {
700
+ /**
701
+ * Runs a program with inherited stdio and returns its exit status; 1, with the reason printed, when it cannot start.
702
+ * `env` is the child's whole environment; undefined lets it inherit this process's. Pi gets io.childEnv.
703
+ */
704
+ async function spawnPi(program , programArgs , io , env ) {
428
705
  const options = { stdio: "inherit" };
429
- if (io.childEnv) options.env = io.childEnv;
706
+ if (env) options.env = env;
430
707
  const result = await io.spawn(program, programArgs, options);
431
708
  if (result.error) {
432
709
  io.stderr.write(`pi-space: could not start ${program}: ${result.error.message}\n`);
@@ -444,7 +721,7 @@ async function launch(name , args , dir , io , comm
444
721
  io.stderr.write(`pi-space: ${commandSource}: ${(error ).message}.\n`);
445
722
  return io.exit(2);
446
723
  }
447
- io.exit(await spawnPi(launched.program, launched.args, io));
724
+ io.exit(await spawnPi(launched.program, launched.args, io, io.childEnv));
448
725
  }
449
726
 
450
727
  /**
@@ -466,7 +743,7 @@ async function runPackages(verb , target , args
466
743
  return io.exit(2);
467
744
  }
468
745
  if ("every" in target) io.stdout.write(`== ${name}\n`);
469
- const status = await spawnPi(launched.program, launched.args, io);
746
+ const status = await spawnPi(launched.program, launched.args, io, io.childEnv);
470
747
  if (!("every" in target)) return io.exit(status);
471
748
  if (status !== 0) failed.push(name);
472
749
  }
@@ -477,6 +754,43 @@ async function runPackages(verb , target , args
477
754
  io.exit(0);
478
755
  }
479
756
 
757
+ /**
758
+ * Runs the setup command in a space, or in every space with --every (never base), as `pi-space setup`.
759
+ * With --every each run gets a `== <name>` header, one failure does not stop the rest, and the status is 1 if any failed.
760
+ */
761
+ async function runSetup(target , dir , io , setupSource , profile ) {
762
+ if (!hasSetupCommand(io.env)) {
763
+ io.stderr.write("pi-space: There is no setup command; set PI_SPACES_SETUP or give --setup <cmd>.\n");
764
+ return io.exit(2);
765
+ }
766
+ if (!checkSetup(io, setupSource)) return io.exit(2);
767
+ if (!("every" in target)) {
768
+ if (target.name === BASE) {
769
+ io.stderr.write("pi-space: setup runs only in spaces, not in base (the home Pi starts with).\n");
770
+ return io.exit(1);
771
+ }
772
+ if (!checkSpace(target.name, dir, io)) return io.exit(1);
773
+ const home = join(dir, target.name);
774
+ return io.exit(await spawnSetup(setupFor(target.name, home, io), target.name, home, io, profile));
775
+ }
776
+ const names = listSpaces(dir);
777
+ if (names.length === 0) {
778
+ io.stdout.write("No spaces yet. Create one with pi-space new <name>.\n");
779
+ return io.exit(0);
780
+ }
781
+ const failed = [];
782
+ for (const name of names) {
783
+ io.stdout.write(`== ${name}\n`);
784
+ const home = join(dir, name);
785
+ if ((await spawnSetup(setupFor(name, home, io), name, home, io, profile)) !== 0) failed.push(name);
786
+ }
787
+ if (failed.length > 0) {
788
+ io.stderr.write(`pi-space: setup failed in: ${failed.join(", ")}\n`);
789
+ return io.exit(1);
790
+ }
791
+ io.exit(0);
792
+ }
793
+
480
794
  async function pick(dir , io , commandSource ) {
481
795
  if (!io.stdin.isTTY) {
482
796
  io.stderr.write(USAGE);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jals1212/pi-spaces-cli",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
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.4.0"
42
+ "@jals1212/pi-spaces": "^0.6.0"
43
43
  },
44
44
  "publishConfig": {
45
45
  "access": "public",