@jals1212/pi-spaces-cli 0.3.0 → 0.4.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 +50 -0
- package/README.md +32 -10
- package/dist/cli.js +153 -59
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,56 @@ commands and options and a patch version fixes.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.4.0
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `pi-space list <name|--every>` lists the packages of a space, and
|
|
14
|
+
`pi-space update`, `install` and `uninstall` take a space and run Pi's
|
|
15
|
+
command of that name in it, for example `pi-space update work --extensions`
|
|
16
|
+
or `pi-space install work npm:some-package`. What follows is Pi's own:
|
|
17
|
+
`install` and `uninstall` need a source, and `update` may have none. The
|
|
18
|
+
command runs through the extension (`pi --space <name> <command> ...`) and
|
|
19
|
+
its exit status is `pi-space`'s. Needs `@jals1212/pi-spaces` 0.4.0 or later
|
|
20
|
+
in the base home.
|
|
21
|
+
- `--every` takes the place of the space: the command runs in `base`, then in
|
|
22
|
+
every space in turn, each after a `== <name>` line. A failure does not stop
|
|
23
|
+
the rest; the exit status is 1 if any failed, and the last line on stderr
|
|
24
|
+
names them: `pi-space: update failed in: base, work`.
|
|
25
|
+
- `pi-space rename <old> <new>` renames a space, as `/space rename` does, with
|
|
26
|
+
the same refusals as `remove`.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- `pi-space new <name>` and `pi-space remove <name> [--yes] [--no-backup]` are
|
|
31
|
+
commands, like `list`, `rename` and Pi's own. They do what `--new` and
|
|
32
|
+
`--remove` did, with the same questions and backup.
|
|
33
|
+
- The command words `list`, `new`, `remove`, `rename`, `switch`, `update`,
|
|
34
|
+
`install` and `uninstall` can no longer name a space, so `pi-space <name>`
|
|
35
|
+
never reads as a command (see the extension's 0.4.0).
|
|
36
|
+
- Requires `@jals1212/pi-spaces` 0.4.0 or later.
|
|
37
|
+
|
|
38
|
+
### Removed
|
|
39
|
+
|
|
40
|
+
- The options `--new` and `--remove`. They exit with status 2 and name the
|
|
41
|
+
command that replaced them: `pi-space --new work` →
|
|
42
|
+
`pi-space new <name>`, `pi-space --remove work` →
|
|
43
|
+
`pi-space remove <name> [--yes] [--no-backup]`.
|
|
44
|
+
|
|
45
|
+
## 0.3.1
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- On Windows the program is started through `cross-spawn`. npm shims such as
|
|
50
|
+
`pi.cmd`, paths with spaces, a trailing backslash, `%` and other `cmd.exe`
|
|
51
|
+
characters in the command or in Pi's arguments are passed as written.
|
|
52
|
+
- The picker reports a bad command (an unclosed quote) before it lists the
|
|
53
|
+
spaces, not after you have chosen one.
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
|
|
57
|
+
- Depends on `cross-spawn`.
|
|
58
|
+
|
|
9
59
|
## 0.3.0
|
|
10
60
|
|
|
11
61
|
### 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, 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, 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.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -8,23 +8,45 @@ The `pi-space` command: pick a [Pi](https://pi.dev) space and start Pi in it, fr
|
|
|
8
8
|
npm install -g @jals1212/pi-spaces-cli
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Node 20 or later. The Pi extension `@jals1212/pi-spaces` must be installed in the home the command opens: `pi-space` only runs `pi --space <name
|
|
11
|
+
Node 20 or later. The Pi extension `@jals1212/pi-spaces` must be installed in the home the command opens: `pi-space` only runs `pi --space <name> [command]`, and the extension does the switch. For the package commands (`list <name>`, `update`, `install`, `uninstall`) the extension in the base home must be **0.4.0 or later**: an older one starts Pi in the space instead of running the command.
|
|
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.
|
|
16
|
+
|
|
15
17
|
| Command | What it does |
|
|
16
18
|
| --- | --- |
|
|
17
19
|
| `pi-space` | Shows a numbered list of spaces and starts Pi in the one you pick. |
|
|
18
20
|
| `pi-space <name> [pi args...]` | Starts Pi in that space (`base` for the home Pi starts with). |
|
|
19
|
-
| `pi-space list` | Lists the spaces.
|
|
20
|
-
| `pi-space
|
|
21
|
-
| `pi-space
|
|
21
|
+
| `pi-space list` | Lists the spaces. |
|
|
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. |
|
|
24
|
+
| `pi-space rename <old> <new>` | Renames a space, as `/space rename` does in Pi. It refuses what `remove` refuses (see below). |
|
|
25
|
+
| `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
|
+
| `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
|
+
| `pi-space install <name\|--every> <source> [pi install options...]` | Runs Pi's `install` in a space. The source is required. |
|
|
28
|
+
| `pi-space uninstall <name\|--every> <source> [pi options...]` | Runs Pi's `uninstall` (its alias of `remove`) in a space. The source is required. |
|
|
22
29
|
| `pi-space --help` | Shows the usage. |
|
|
23
30
|
| `pi-space --version` | Prints the version. |
|
|
24
31
|
|
|
32
|
+
`--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.
|
|
33
|
+
|
|
34
|
+
### Packages in a space, or in every space
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
pi-space list work # the packages of "work"
|
|
38
|
+
pi-space install work npm:some-package # install in "work" only
|
|
39
|
+
pi-space update base --extensions # update the extensions of the base home
|
|
40
|
+
pi-space update --every # base, then every space
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
A package command runs `pi --space <name> <command> [args...]` with Pi's stdio, through `$PI_SPACES_COMMAND` (or `--command`), and ends with the exit status of that Pi. The extension relaunches Pi in the space's home, so the command works on that home's `settings.json` and packages; `pi-space` never sets `PI_CODING_AGENT_DIR` itself. `<name>` is `base` or an existing space; anything else exits with status 1 before Pi starts.
|
|
44
|
+
|
|
45
|
+
`--every` takes the place of the name. The command runs in `base` first, then in every space in order, one after the other, each after a `== <name>` line on stdout. A failure does not stop the rest. The exit status is 0 if every run succeeded, otherwise 1, and the last line on stderr names the spaces that failed: `pi-space: update failed in: base, work`.
|
|
46
|
+
|
|
25
47
|
A new space gets a `settings.json` that declares the extension, so `/space` works inside it: the same declaration as in the base home's `settings.json` (`PI_SPACES_BASE_HOME`, else `PI_CODING_AGENT_DIR`, else `~/.pi/agent`), or `npm:@jals1212/pi-spaces` when the base home does not declare it. A local path counts when it is a copy of the extension: a folder whose `package.json` is named `@jals1212/pi-spaces`.
|
|
26
48
|
|
|
27
|
-
|
|
49
|
+
`remove` and `rename` refuse `base`, the space you are in, a space that holds the base home, and anything that is not a real folder inside the spaces folder, before it asks. A backup is the whole space folder, moved to `.backups/<name>-<UTC time>`; move it back to restore it. See [what it leaves on your machine](https://github.com/jals1212/pi-spaces#what-it-leaves-on-your-machine).
|
|
28
50
|
|
|
29
51
|
## Another program that runs Pi
|
|
30
52
|
|
|
@@ -32,7 +54,7 @@ To start Pi through another program for one call, give `--command` and `--dir` b
|
|
|
32
54
|
|
|
33
55
|
```sh
|
|
34
56
|
pi-space --command "my-launcher --" --dir ~/.my-launcher/spaces work # → my-launcher -- --space work
|
|
35
|
-
pi-space --dir ~/.my-launcher/spaces
|
|
57
|
+
pi-space --dir ~/.my-launcher/spaces new work # create one in that folder
|
|
36
58
|
pi-space --command '"/opt/my launcher/run" --' work # a path with spaces, in quotes
|
|
37
59
|
```
|
|
38
60
|
|
|
@@ -55,9 +77,9 @@ export PI_SPACES_DIR=~/.my-launcher/spaces # the spaces next to that program's
|
|
|
55
77
|
|
|
56
78
|
| Status | Meaning |
|
|
57
79
|
| --- | --- |
|
|
58
|
-
| 0 … | Pi's own exit status. |
|
|
59
|
-
| 1 | An unknown or invalid space, a name
|
|
60
|
-
| 2 | No arguments and no terminal to pick in,
|
|
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. |
|
|
61
83
|
|
|
62
84
|
## More
|
|
63
85
|
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
// The optional `pi-space` command: pick a space and start Pi in it.
|
|
2
|
-
// It only passes `--space <name
|
|
3
|
-
|
|
1
|
+
// The optional `pi-space` command: pick a space and start Pi in it, and manage the spaces and their packages.
|
|
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.
|
|
4
4
|
import { readFileSync } from "node:fs";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { dirname, join, resolve } from "node:path";
|
|
7
7
|
import { createInterface } from "node:readline";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
+
import crossSpawn from "cross-spawn";
|
|
9
10
|
import {
|
|
10
11
|
BACKUPS,
|
|
11
12
|
BASE,
|
|
@@ -17,23 +18,31 @@ import {
|
|
|
17
18
|
listSpaces,
|
|
18
19
|
|
|
19
20
|
removeSpace,
|
|
21
|
+
renameSpace,
|
|
20
22
|
SpaceError,
|
|
21
23
|
spacesDir,
|
|
22
24
|
validateName,
|
|
23
25
|
} from "@jals1212/pi-spaces/spaces";
|
|
24
26
|
|
|
25
|
-
export const USAGE = `Usage: pi-space [--command <cmd>] [--dir <path>] [<name> [pi args...] |
|
|
27
|
+
export const USAGE = `Usage: pi-space [--command <cmd>] [--dir <path>] [<name> [pi args...] | <command> ...]
|
|
26
28
|
pi-space --help | --version
|
|
27
|
-
pi-space
|
|
28
|
-
pi-space <name> ...
|
|
29
|
-
pi-space list
|
|
30
|
-
pi-space
|
|
31
|
-
pi-space
|
|
32
|
-
|
|
33
|
-
pi-space
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
pi-space pick a space and start Pi in it
|
|
30
|
+
pi-space <name> [pi args...] start Pi in a space ("base" for the home Pi starts with)
|
|
31
|
+
pi-space list list the spaces
|
|
32
|
+
pi-space list <name|--every> list the packages of a space
|
|
33
|
+
pi-space new <name> create a space
|
|
34
|
+
pi-space rename <old> <new> rename a space
|
|
35
|
+
pi-space remove <name> delete a space and everything in it, after asking, and keep a backup in .backups unless told not to
|
|
36
|
+
(--yes skips the questions; --no-backup deletes without a backup)
|
|
37
|
+
pi-space update <name|--every> update the packages of a space (more words go to Pi: --extensions, --all, a source)
|
|
38
|
+
pi-space install <name|--every> <source> install a package in a space (more words go to Pi)
|
|
39
|
+
pi-space uninstall <name|--every> <source> remove a package from a space (more words go to Pi)
|
|
40
|
+
pi-space --help print this help
|
|
41
|
+
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.
|
|
43
|
+
Options before the command, for this call only:
|
|
44
|
+
--command <cmd> the program to run instead of $PI_SPACES_COMMAND (default: pi), e.g. "my-launcher --"
|
|
45
|
+
--dir <path> the spaces folder instead of $PI_SPACES_DIR; the Pi it starts gets it too
|
|
37
46
|
`;
|
|
38
47
|
|
|
39
48
|
/** This package's version; package.json sits one folder above both src/ and dist/. */
|
|
@@ -45,12 +54,29 @@ export const VERSION = JSON.parse(readFileSync(new URL("../package.json"
|
|
|
45
54
|
|
|
46
55
|
|
|
47
56
|
|
|
57
|
+
/** The Pi commands that pi-space runs in one space or in every space. */
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
/** A space name (or `base`), or every space: base first, then the spaces in order. */
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
const PACKAGE_VERBS = ["list", "update", "install", "uninstall"];
|
|
64
|
+
|
|
65
|
+
/** The old options, and the command that replaced each. */
|
|
66
|
+
const REPLACED = {
|
|
67
|
+
"--new": "pi-space new <name>",
|
|
68
|
+
"--remove": "pi-space remove <name> [--yes] [--no-backup]",
|
|
69
|
+
} ;
|
|
70
|
+
|
|
48
71
|
|
|
49
72
|
|
|
50
73
|
|
|
51
74
|
|
|
75
|
+
|
|
52
76
|
|
|
77
|
+
|
|
53
78
|
|
|
79
|
+
|
|
54
80
|
|
|
55
81
|
|
|
56
82
|
|
|
@@ -62,8 +88,8 @@ const CALL_OPTIONS = { "--command": "command", "--dir": "dir" } ;
|
|
|
62
88
|
|
|
63
89
|
/**
|
|
64
90
|
* --command and --dir come first; after a space name every argument belongs to Pi.
|
|
65
|
-
*
|
|
66
|
-
*
|
|
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.
|
|
67
93
|
* A space name never starts with "-", so any other option in first place is unknown, not a name.
|
|
68
94
|
*/
|
|
69
95
|
export function parseCli(args ) {
|
|
@@ -85,8 +111,13 @@ function parseCommand(args ) {
|
|
|
85
111
|
if (first === undefined) return { kind: "picker" };
|
|
86
112
|
if (first === "--help" || first === "-h") return { kind: "help" };
|
|
87
113
|
if (first === "--version" || first === "-v") return { kind: "version" };
|
|
88
|
-
if (first
|
|
89
|
-
if (first
|
|
114
|
+
if (Object.hasOwn(REPLACED, first)) return { kind: "replaced", option: first, instead: REPLACED[first ] };
|
|
115
|
+
if (first.startsWith("-")) return { kind: "unknown-option", option: first };
|
|
116
|
+
if (first === "list" && rest.length === 0) return { kind: "list" };
|
|
117
|
+
if (PACKAGE_VERBS.includes(first)) return parsePackages(first , rest);
|
|
118
|
+
if (first === "new") return rest.length === 1 ? { kind: "new", name: rest[0] } : { kind: "usage" };
|
|
119
|
+
if (first === "rename") return rest.length === 2 ? { kind: "rename", from: rest[0], to: rest[1] } : { kind: "usage" };
|
|
120
|
+
if (first === "remove") {
|
|
90
121
|
const flags = rest.filter((arg) => arg.startsWith("-"));
|
|
91
122
|
const names = rest.filter((arg) => !arg.startsWith("-"));
|
|
92
123
|
const yes = flags.filter((flag) => flag === "--yes" || flag === "-y").length;
|
|
@@ -94,11 +125,22 @@ function parseCommand(args ) {
|
|
|
94
125
|
if (names.length !== 1 || yes > 1 || noBackup > 1 || yes + noBackup !== flags.length) return { kind: "usage" };
|
|
95
126
|
return { kind: "remove", name: names[0], yes: yes === 1, backup: noBackup === 0 };
|
|
96
127
|
}
|
|
97
|
-
if (first.startsWith("-")) return { kind: "unknown-option", option: first };
|
|
98
|
-
if (first === "list") return rest.length === 0 ? { kind: "list" } : { kind: "usage" };
|
|
99
128
|
return { kind: "launch", name: first, args: rest };
|
|
100
129
|
}
|
|
101
130
|
|
|
131
|
+
/**
|
|
132
|
+
* `<verb> <name|--every> [args...]`. list takes nothing after the target; install and uninstall need a source
|
|
133
|
+
* (the first argument after it); update may have nothing, which is Pi's own default.
|
|
134
|
+
*/
|
|
135
|
+
function parsePackages(verb , rest ) {
|
|
136
|
+
const [first, ...args] = rest;
|
|
137
|
+
if (first === undefined) return { kind: "usage" };
|
|
138
|
+
if (first !== "--every" && first.startsWith("-")) return { kind: "unknown-option", option: first };
|
|
139
|
+
if (verb === "list" && args.length > 0) return { kind: "usage" };
|
|
140
|
+
if ((verb === "install" || verb === "uninstall") && args.length === 0) return { kind: "usage" };
|
|
141
|
+
return { kind: "packages", verb, target: first === "--every" ? { every: true } : { name: first }, args };
|
|
142
|
+
}
|
|
143
|
+
|
|
102
144
|
/**
|
|
103
145
|
* The program and arguments that start Pi in a space: $PI_SPACES_COMMAND (default pi) plus --space <name>.
|
|
104
146
|
* Throws when the command has an unclosed quote.
|
|
@@ -139,15 +181,6 @@ export function splitCommand(text ) {
|
|
|
139
181
|
return words;
|
|
140
182
|
}
|
|
141
183
|
|
|
142
|
-
/**
|
|
143
|
-
* One word for the single command line cmd.exe gets when the child runs with shell: true on Windows:
|
|
144
|
-
* wrapped in double quotes unless it is plain, with inner double quotes doubled.
|
|
145
|
-
*/
|
|
146
|
-
export function quoteForCmd(word ) {
|
|
147
|
-
if (word !== "" && !/[\s"&|<>^()%!,;=]/.test(word)) return word;
|
|
148
|
-
return `"${word.replaceAll('"', '""')}"`;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
184
|
const EXTENSION = "@jals1212/pi-spaces";
|
|
152
185
|
|
|
153
186
|
/**
|
|
@@ -172,7 +205,6 @@ function agentHome(env , home ) {
|
|
|
172
205
|
|
|
173
206
|
|
|
174
207
|
|
|
175
|
-
|
|
176
208
|
|
|
177
209
|
|
|
178
210
|
|
|
@@ -190,19 +222,20 @@ function agentHome(env , home ) {
|
|
|
190
222
|
|
|
191
223
|
|
|
192
224
|
|
|
193
|
-
|
|
194
|
-
|
|
195
225
|
|
|
196
226
|
|
|
197
227
|
|
|
198
228
|
|
|
199
|
-
/**
|
|
229
|
+
/**
|
|
230
|
+
* The real child process: stdio inherited, the environment unchanged. cross-spawn is plain child_process.spawn
|
|
231
|
+
* except on Windows, where it finds .cmd and .bat shims such as pi.cmd and escapes the arguments for them.
|
|
232
|
+
*/
|
|
200
233
|
export function spawnChild(program , args , options ) {
|
|
201
234
|
// Ctrl-C belongs to Pi while it runs; this process waits for it instead of dying first.
|
|
202
235
|
const ignore = () => {};
|
|
203
236
|
process.on("SIGINT", ignore);
|
|
204
237
|
return new Promise ((resolve) => {
|
|
205
|
-
const child =
|
|
238
|
+
const child = crossSpawn(program, args, options);
|
|
206
239
|
child.once("error", (error) => resolve({ status: null, error }));
|
|
207
240
|
child.once("close", (status) => resolve({ status }));
|
|
208
241
|
}).finally(() => process.off("SIGINT", ignore));
|
|
@@ -216,7 +249,6 @@ export function processIo(proc = process) {
|
|
|
216
249
|
stderr: proc.stderr,
|
|
217
250
|
env: proc.env,
|
|
218
251
|
home: homedir(),
|
|
219
|
-
platform: process.platform,
|
|
220
252
|
spawn: spawnChild,
|
|
221
253
|
exit: (code) => proc.exit(code),
|
|
222
254
|
};
|
|
@@ -243,18 +275,25 @@ export async function main(argv , io ) {
|
|
|
243
275
|
case "unknown-option":
|
|
244
276
|
io.stderr.write(`pi-space: Unknown option "${command.option}".\n${USAGE}`);
|
|
245
277
|
return io.exit(2);
|
|
278
|
+
case "replaced":
|
|
279
|
+
io.stderr.write(`pi-space: ${command.option} was replaced by: ${command.instead}\n`);
|
|
280
|
+
return io.exit(2);
|
|
246
281
|
case "usage":
|
|
247
282
|
io.stderr.write(USAGE);
|
|
248
283
|
return io.exit(2);
|
|
249
284
|
case "list": {
|
|
250
285
|
const spaces = listSpaces(dir);
|
|
251
286
|
const lines = [`Spaces in ${dir}:`, ` ${BASE}`, ...spaces.map((name) => ` ${name}`)];
|
|
252
|
-
if (spaces.length === 0) lines.push("No spaces yet. Create one with pi-space
|
|
287
|
+
if (spaces.length === 0) lines.push("No spaces yet. Create one with pi-space new <name>.");
|
|
253
288
|
io.stdout.write(`${lines.join("\n")}\n`);
|
|
254
289
|
return io.exit(0);
|
|
255
290
|
}
|
|
291
|
+
case "packages":
|
|
292
|
+
return runPackages(command.verb, command.target, command.args, dir, io, commandSource);
|
|
256
293
|
case "new":
|
|
257
294
|
return create(command.name, dir, io);
|
|
295
|
+
case "rename":
|
|
296
|
+
return rename(command.from, command.to, dir, io);
|
|
258
297
|
case "remove":
|
|
259
298
|
return remove(command.name, command.yes, command.backup, dir, io);
|
|
260
299
|
case "launch":
|
|
@@ -276,6 +315,19 @@ function create(name , dir , io ) {
|
|
|
276
315
|
}
|
|
277
316
|
}
|
|
278
317
|
|
|
318
|
+
/** 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 ) {
|
|
320
|
+
try {
|
|
321
|
+
const protect = io.env.PI_SPACES_BASE_HOME ? [io.env.PI_SPACES_BASE_HOME] : [];
|
|
322
|
+
renameSpace(dir, from, to, agentHome(io.env, io.home), protect, io.home);
|
|
323
|
+
io.stdout.write(`Renamed space "${from}" to "${to}".\n`);
|
|
324
|
+
return io.exit(0);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
io.stderr.write(`pi-space: ${error instanceof SpaceError ? error.message : `could not rename space "${from}": ${(error ).message}`}\n`);
|
|
327
|
+
return io.exit(1);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
279
331
|
/**
|
|
280
332
|
* Without --yes it asks to remove ([y/N]), then, without --no-backup, whether to keep a backup ([Y/n]).
|
|
281
333
|
* With --yes it asks nothing and keeps a backup unless --no-backup.
|
|
@@ -289,7 +341,7 @@ async function remove(name , yes , backup , dir ,
|
|
|
289
341
|
const path = checkRemovable(dir, name, current, protect, io.home);
|
|
290
342
|
let keep = backup;
|
|
291
343
|
if (!yes) {
|
|
292
|
-
if (!io.stdin.isTTY) throw new SpaceError(`Removing a space needs confirmation; run pi-space
|
|
344
|
+
if (!io.stdin.isTTY) throw new SpaceError(`Removing a space needs confirmation; run pi-space remove ${name} --yes.`);
|
|
293
345
|
// One reader for every question, so an answer typed ahead is not lost between them.
|
|
294
346
|
reader = lineReader(io.stdin);
|
|
295
347
|
io.stdout.write(`Remove space "${name}"? This deletes ${path} and everything in it: sign-ins, packages, settings and sessions. [y/N] `);
|
|
@@ -356,18 +408,35 @@ function lineReader(input ) {
|
|
|
356
408
|
};
|
|
357
409
|
}
|
|
358
410
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
411
|
+
/** Prints the reason and returns false when `name` is not base or an existing space. */
|
|
412
|
+
function checkSpace(name , dir , io ) {
|
|
413
|
+
if (name === BASE) return true;
|
|
414
|
+
const invalid = validateName(name);
|
|
415
|
+
if (invalid) {
|
|
416
|
+
io.stderr.write(`pi-space: ${invalid}\n`);
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
if (!listSpaces(dir).includes(name)) {
|
|
420
|
+
io.stderr.write(`pi-space: Space "${name}" does not exist.\n`);
|
|
421
|
+
return false;
|
|
370
422
|
}
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
|
|
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 ) {
|
|
428
|
+
const options = { stdio: "inherit" };
|
|
429
|
+
if (io.childEnv) options.env = io.childEnv;
|
|
430
|
+
const result = await io.spawn(program, programArgs, options);
|
|
431
|
+
if (result.error) {
|
|
432
|
+
io.stderr.write(`pi-space: could not start ${program}: ${result.error.message}\n`);
|
|
433
|
+
return 1;
|
|
434
|
+
}
|
|
435
|
+
return result.status ?? 1;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function launch(name , args , dir , io , commandSource ) {
|
|
439
|
+
if (!checkSpace(name, dir, io)) return io.exit(1);
|
|
371
440
|
let launched ;
|
|
372
441
|
try {
|
|
373
442
|
launched = launchCommand(io.env, name, args);
|
|
@@ -375,19 +444,37 @@ async function launch(name , args , dir , io , comm
|
|
|
375
444
|
io.stderr.write(`pi-space: ${commandSource}: ${(error ).message}.\n`);
|
|
376
445
|
return io.exit(2);
|
|
377
446
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
447
|
+
io.exit(await spawnPi(launched.program, launched.args, io));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Runs one of Pi's package commands in a space, or in base and then every space with --every, through the same
|
|
452
|
+
* Pi (`--space <name> <verb> [args...]`): the extension relaunches it in that home, so the home is never set here.
|
|
453
|
+
* With --every each run gets a `== <name>` header, one failure does not stop the rest, and the status is 1 if any failed.
|
|
454
|
+
*/
|
|
455
|
+
async function runPackages(verb , target , args , dir , io , commandSource ) {
|
|
456
|
+
const names = "every" in target ? [BASE, ...listSpaces(dir)] : [target.name];
|
|
457
|
+
if (!("every" in target) && !checkSpace(target.name, dir, io)) return io.exit(1);
|
|
458
|
+
const failed = [];
|
|
459
|
+
for (const name of names) {
|
|
460
|
+
let launched ;
|
|
461
|
+
try {
|
|
462
|
+
launched = launchCommand(io.env, name, [verb, ...args]);
|
|
463
|
+
} catch (error) {
|
|
464
|
+
// The same command for every space, so this is reported once, before any header.
|
|
465
|
+
io.stderr.write(`pi-space: ${commandSource}: ${(error ).message}.\n`);
|
|
466
|
+
return io.exit(2);
|
|
467
|
+
}
|
|
468
|
+
if ("every" in target) io.stdout.write(`== ${name}\n`);
|
|
469
|
+
const status = await spawnPi(launched.program, launched.args, io);
|
|
470
|
+
if (!("every" in target)) return io.exit(status);
|
|
471
|
+
if (status !== 0) failed.push(name);
|
|
472
|
+
}
|
|
473
|
+
if (failed.length > 0) {
|
|
474
|
+
io.stderr.write(`pi-space: ${verb} failed in: ${failed.join(", ")}\n`);
|
|
388
475
|
return io.exit(1);
|
|
389
476
|
}
|
|
390
|
-
io.exit(
|
|
477
|
+
io.exit(0);
|
|
391
478
|
}
|
|
392
479
|
|
|
393
480
|
async function pick(dir , io , commandSource ) {
|
|
@@ -395,6 +482,13 @@ async function pick(dir , io , commandSource )
|
|
|
395
482
|
io.stderr.write(USAGE);
|
|
396
483
|
return io.exit(2);
|
|
397
484
|
}
|
|
485
|
+
// A bad command is reported before the list, not after the user has chosen.
|
|
486
|
+
try {
|
|
487
|
+
splitCommand(io.env.PI_SPACES_COMMAND ?? "");
|
|
488
|
+
} catch (error) {
|
|
489
|
+
io.stderr.write(`pi-space: ${commandSource}: ${(error ).message}.\n`);
|
|
490
|
+
return io.exit(2);
|
|
491
|
+
}
|
|
398
492
|
const names = [BASE, ...listSpaces(dir)];
|
|
399
493
|
io.stdout.write(`${[`Spaces in ${dir}:`, ...names.map((name, index) => `${index + 1}) ${name}`)].join("\n")}\nSpace number: `);
|
|
400
494
|
const line = await readLine(io.stdin);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jals1212/pi-spaces-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|
|
@@ -35,10 +35,11 @@
|
|
|
35
35
|
"scripts": {
|
|
36
36
|
"build": "node --disable-warning=ExperimentalWarning scripts/build.mjs",
|
|
37
37
|
"prepack": "npm run build",
|
|
38
|
-
"test": "node ../scripts/link-self.mjs && npm run build --prefix .. && npm run build && node --test
|
|
38
|
+
"test": "node ../scripts/link-self.mjs && npm run build --prefix .. && npm run build && node --test \"test/*.test.ts\""
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"
|
|
41
|
+
"cross-spawn": "^7.0.6",
|
|
42
|
+
"@jals1212/pi-spaces": "^0.4.0"
|
|
42
43
|
},
|
|
43
44
|
"publishConfig": {
|
|
44
45
|
"access": "public",
|