@jals1212/pi-spaces-cli 0.2.0 → 0.3.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 +54 -0
- package/README.md +21 -5
- package/dist/cli.js +272 -26
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,60 @@ commands and options and a patch version fixes.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.3.1
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- On Windows the program is started through `cross-spawn`. npm shims such as
|
|
14
|
+
`pi.cmd`, paths with spaces, a trailing backslash, `%` and other `cmd.exe`
|
|
15
|
+
characters in the command or in Pi's arguments are passed as written.
|
|
16
|
+
- The picker reports a bad command (an unclosed quote) before it lists the
|
|
17
|
+
spaces, not after you have chosen one.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Depends on `cross-spawn`.
|
|
22
|
+
|
|
23
|
+
## 0.3.0
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- `pi-space --new <name>` creates a space from the shell, with the same
|
|
28
|
+
`settings.json` as `/space new <name>`: the base home's declaration of
|
|
29
|
+
`@jals1212/pi-spaces`, or `npm:@jals1212/pi-spaces`. It does not start Pi.
|
|
30
|
+
A local path in the base home is copied when it is a copy of the extension.
|
|
31
|
+
- `pi-space --remove <name>` deletes a space after asking in
|
|
32
|
+
the terminal, with the same safety checks as `/space remove`, then asks
|
|
33
|
+
whether to keep a backup in `.backups` (`[Y/n]`: Enter keeps one). `--yes`
|
|
34
|
+
skips the questions and keeps a backup, and is required without a terminal;
|
|
35
|
+
`--no-backup` deletes without one.
|
|
36
|
+
- `--command <cmd>` and `--dir <path>`, before the rest of the arguments, set
|
|
37
|
+
the program to run and the spaces folder for one call, instead of
|
|
38
|
+
`PI_SPACES_COMMAND` and `PI_SPACES_DIR`. With `--dir`, the Pi it starts gets
|
|
39
|
+
that folder as `PI_SPACES_DIR`.
|
|
40
|
+
- Quotes in `--command` and `PI_SPACES_COMMAND`: double or single quotes keep a
|
|
41
|
+
path with spaces in one word, as in `--command '"/opt/my launcher/run" --'`.
|
|
42
|
+
A quote that is never closed is an error (exit status 2) and starts nothing.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- `pi-space list` with no spaces suggests `pi-space --new <name>`.
|
|
47
|
+
- A `"` or `'` in `PI_SPACES_COMMAND` is now a quote, no longer part of a
|
|
48
|
+
word: a path with an apostrophe, such as `/home/o'neil/bin/run`, needs double
|
|
49
|
+
quotes around it.
|
|
50
|
+
- Needs `@jals1212/pi-spaces` 0.2.0 or later (for `backupSpace` and the
|
|
51
|
+
local-path rule of `--new`).
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- `pi-space --new` copies the base home's local path to this checkout of the
|
|
56
|
+
extension instead of writing `npm:@jals1212/pi-spaces`.
|
|
57
|
+
- `pi-space --remove` explains a backup that cannot be written (for example a
|
|
58
|
+
read-only spaces folder): the space was not removed, and `--no-backup`
|
|
59
|
+
removes it without one.
|
|
60
|
+
- A program whose path has spaces now starts, including on Windows, where
|
|
61
|
+
cmd.exe runs it and each word is now quoted for it.
|
|
62
|
+
|
|
9
63
|
## 0.2.0
|
|
10
64
|
|
|
11
65
|
### Added
|
package/README.md
CHANGED
|
@@ -17,12 +17,28 @@ Node 20 or later. The Pi extension `@jals1212/pi-spaces` must be installed in th
|
|
|
17
17
|
| `pi-space` | Shows a numbered list of spaces and starts Pi in the one you pick. |
|
|
18
18
|
| `pi-space <name> [pi args...]` | Starts Pi in that space (`base` for the home Pi starts with). |
|
|
19
19
|
| `pi-space list` | Lists the spaces. A space named `list` can still be opened from the picker. |
|
|
20
|
+
| `pi-space --new <name>` | Creates an empty space, as `/space new <name>` does in Pi. It does not start Pi. |
|
|
21
|
+
| `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. |
|
|
20
22
|
| `pi-space --help` | Shows the usage. |
|
|
21
23
|
| `pi-space --version` | Prints the version. |
|
|
22
24
|
|
|
25
|
+
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
|
+
|
|
27
|
+
`--remove` refuses `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
|
+
|
|
23
29
|
## Another program that runs Pi
|
|
24
30
|
|
|
25
|
-
To start Pi through another program,
|
|
31
|
+
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:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
pi-space --command "my-launcher --" --dir ~/.my-launcher/spaces work # → my-launcher -- --space work
|
|
35
|
+
pi-space --dir ~/.my-launcher/spaces --new work # create one in that folder
|
|
36
|
+
pi-space --command '"/opt/my launcher/run" --' work # a path with spaces, in quotes
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Inside quotes a backslash is a plain character, so `"C:\Program Files\my-launcher\run.cmd"` works as written. A quote that is never closed is an error (exit status 2), and nothing is started.
|
|
40
|
+
|
|
41
|
+
An alias keeps it short: `alias my-space='pi-space --command "my-launcher --" --dir ~/.my-launcher/spaces'`. To make it the default instead, set the variables in your shell profile:
|
|
26
42
|
|
|
27
43
|
```sh
|
|
28
44
|
export PI_SPACES_COMMAND="my-launcher --" # pi-space work → my-launcher -- --space work
|
|
@@ -31,8 +47,8 @@ export PI_SPACES_DIR=~/.my-launcher/spaces # the spaces next to that program's
|
|
|
31
47
|
|
|
32
48
|
| Variable | Meaning |
|
|
33
49
|
| --- | --- |
|
|
34
|
-
| `PI_SPACES_COMMAND` | The program to run, split on spaces, before `--space <name>`. Default: `pi`. |
|
|
35
|
-
| `PI_SPACES_DIR` | The spaces folder. A relative value is taken from your home folder. Default: `spaces` next to the base home. |
|
|
50
|
+
| `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. |
|
|
51
|
+
| `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. |
|
|
36
52
|
| `PI_CODING_AGENT_DIR` | Pi's agent home; its parent holds the default spaces folder. Default: `~/.pi/agent`. |
|
|
37
53
|
|
|
38
54
|
## Exit status
|
|
@@ -40,8 +56,8 @@ export PI_SPACES_DIR=~/.my-launcher/spaces # the spaces next to that program's
|
|
|
40
56
|
| Status | Meaning |
|
|
41
57
|
| --- | --- |
|
|
42
58
|
| 0 … | Pi's own exit status. |
|
|
43
|
-
| 1 | An unknown or invalid space, an out-of-range pick, or the program could not start. |
|
|
44
|
-
| 2 | No arguments and no terminal to pick in: the usage is printed. |
|
|
59
|
+
| 1 | An unknown or invalid space, a name `--new` cannot use (taken, `base` or invalid), a space `--remove` cannot delete, `--remove` without `--yes` and no terminal, an out-of-range pick, or the program could not start. |
|
|
60
|
+
| 2 | No arguments and no terminal to pick in, `--new` or `--remove` without exactly one name, or `--command` or `--dir` without a value or given twice: the usage is printed. A `--command` or `PI_SPACES_COMMAND` with an unclosed quote: the problem is printed. |
|
|
45
61
|
|
|
46
62
|
## More
|
|
47
63
|
|
package/dist/cli.js
CHANGED
|
@@ -1,51 +1,172 @@
|
|
|
1
1
|
// The optional `pi-space` command: pick a space and start Pi in it.
|
|
2
2
|
// It only passes `--space <name>`; the extension does the switch, so it never sets PI_CODING_AGENT_DIR.
|
|
3
|
-
import { spawn as nodeSpawn } from "node:child_process";
|
|
4
3
|
import { readFileSync } from "node:fs";
|
|
5
4
|
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
6
|
import { createInterface } from "node:readline";
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import crossSpawn from "cross-spawn";
|
|
9
|
+
import {
|
|
10
|
+
BACKUPS,
|
|
11
|
+
BASE,
|
|
12
|
+
backupSpace,
|
|
13
|
+
checkRemovable,
|
|
14
|
+
createSpace,
|
|
15
|
+
|
|
16
|
+
findOwnDeclaration,
|
|
17
|
+
listSpaces,
|
|
18
|
+
|
|
19
|
+
removeSpace,
|
|
20
|
+
SpaceError,
|
|
21
|
+
spacesDir,
|
|
22
|
+
validateName,
|
|
23
|
+
} from "@jals1212/pi-spaces/spaces";
|
|
24
|
+
|
|
25
|
+
export const USAGE = `Usage: pi-space [--command <cmd>] [--dir <path>] [<name> [pi args...] | list | --new <name> | --remove <name> [--yes] [--no-backup]]
|
|
26
|
+
pi-space --help | --version
|
|
27
|
+
pi-space pick a space and start Pi in it
|
|
28
|
+
pi-space <name> ... start Pi in a space ("base" for the home Pi starts with)
|
|
29
|
+
pi-space list list the spaces
|
|
30
|
+
pi-space --new <name> create a space
|
|
31
|
+
pi-space --remove <name> delete a space and everything in it, after asking, and keep a backup in .backups unless told not to
|
|
32
|
+
(--yes skips the questions; --no-backup deletes without a backup)
|
|
33
|
+
pi-space --version print the version
|
|
34
|
+
Options for this call only:
|
|
35
|
+
--command <cmd> the program to run instead of $PI_SPACES_COMMAND (default: pi), e.g. "my-launcher --"
|
|
36
|
+
--dir <path> the spaces folder instead of $PI_SPACES_DIR; the Pi it starts gets it too
|
|
15
37
|
`;
|
|
16
38
|
|
|
17
39
|
/** This package's version; package.json sits one folder above both src/ and dist/. */
|
|
18
40
|
export const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
19
41
|
|
|
20
|
-
|
|
42
|
+
/** --command and --dir: for this call only, so nothing has to be exported into the shell. */
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
21
49
|
|
|
22
50
|
|
|
23
51
|
|
|
24
52
|
|
|
53
|
+
|
|
54
|
+
|
|
25
55
|
|
|
26
56
|
|
|
27
|
-
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
const CALL_OPTIONS = { "--command": "command", "--dir": "dir" } ;
|
|
28
62
|
|
|
29
63
|
/**
|
|
64
|
+
* --command and --dir come first; after a space name every argument belongs to Pi.
|
|
30
65
|
* `list` is the subcommand, so a space named "list" can only be opened from the picker.
|
|
66
|
+
* `--new` and `--remove` are options, not subcommands, so they never take a space name.
|
|
31
67
|
* A space name never starts with "-", so any other option in first place is unknown, not a name.
|
|
32
68
|
*/
|
|
33
69
|
export function parseCli(args ) {
|
|
70
|
+
const options = {};
|
|
71
|
+
let index = 0;
|
|
72
|
+
while (Object.hasOwn(CALL_OPTIONS, args[index] ?? "")) {
|
|
73
|
+
const key = CALL_OPTIONS[args[index] ];
|
|
74
|
+
const value = args[index + 1];
|
|
75
|
+
if (!value || options[key] !== undefined) return { kind: "usage" };
|
|
76
|
+
options[key] = value;
|
|
77
|
+
index += 2;
|
|
78
|
+
}
|
|
79
|
+
const command = parseCommand(args.slice(index));
|
|
80
|
+
return command.kind === "usage" ? command : { ...command, ...options };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseCommand(args ) {
|
|
34
84
|
const [first, ...rest] = args;
|
|
35
85
|
if (first === undefined) return { kind: "picker" };
|
|
36
86
|
if (first === "--help" || first === "-h") return { kind: "help" };
|
|
37
87
|
if (first === "--version" || first === "-v") return { kind: "version" };
|
|
88
|
+
if (first === "--new") return rest.length === 1 ? { kind: "new", name: rest[0] } : { kind: "usage" };
|
|
89
|
+
if (first === "--remove") {
|
|
90
|
+
const flags = rest.filter((arg) => arg.startsWith("-"));
|
|
91
|
+
const names = rest.filter((arg) => !arg.startsWith("-"));
|
|
92
|
+
const yes = flags.filter((flag) => flag === "--yes" || flag === "-y").length;
|
|
93
|
+
const noBackup = flags.filter((flag) => flag === "--no-backup").length;
|
|
94
|
+
if (names.length !== 1 || yes > 1 || noBackup > 1 || yes + noBackup !== flags.length) return { kind: "usage" };
|
|
95
|
+
return { kind: "remove", name: names[0], yes: yes === 1, backup: noBackup === 0 };
|
|
96
|
+
}
|
|
38
97
|
if (first.startsWith("-")) return { kind: "unknown-option", option: first };
|
|
39
98
|
if (first === "list") return rest.length === 0 ? { kind: "list" } : { kind: "usage" };
|
|
40
99
|
return { kind: "launch", name: first, args: rest };
|
|
41
100
|
}
|
|
42
101
|
|
|
43
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* The program and arguments that start Pi in a space: $PI_SPACES_COMMAND (default pi) plus --space <name>.
|
|
104
|
+
* Throws when the command has an unclosed quote.
|
|
105
|
+
*/
|
|
44
106
|
export function launchCommand(env , name , args ) {
|
|
45
|
-
const [program = "pi", ...prefix] = (env.PI_SPACES_COMMAND ?? "")
|
|
107
|
+
const [program = "pi", ...prefix] = splitCommand(env.PI_SPACES_COMMAND ?? "");
|
|
46
108
|
return { program, args: [...prefix, "--space", name, ...args] };
|
|
47
109
|
}
|
|
48
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Splits a command into words on whitespace. Double or single quotes keep whitespace inside a word and are
|
|
113
|
+
* removed. There are no backslash escapes, so a Windows path in quotes keeps its backslashes.
|
|
114
|
+
*/
|
|
115
|
+
export function splitCommand(text ) {
|
|
116
|
+
const words = [];
|
|
117
|
+
let word = "";
|
|
118
|
+
// True from the first character or quote of a word, so "" alone is an empty word.
|
|
119
|
+
let started = false;
|
|
120
|
+
let quote ;
|
|
121
|
+
for (const char of text) {
|
|
122
|
+
if (quote) {
|
|
123
|
+
if (char === quote) quote = undefined;
|
|
124
|
+
else word += char;
|
|
125
|
+
} else if (char === '"' || char === "'") {
|
|
126
|
+
quote = char;
|
|
127
|
+
started = true;
|
|
128
|
+
} else if (/\s/.test(char)) {
|
|
129
|
+
if (started) words.push(word);
|
|
130
|
+
word = "";
|
|
131
|
+
started = false;
|
|
132
|
+
} else {
|
|
133
|
+
word += char;
|
|
134
|
+
started = true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (quote) throw new Error(`the command has an unclosed ${quote} quote`);
|
|
138
|
+
if (started) words.push(word);
|
|
139
|
+
return words;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const EXTENSION = "@jals1212/pi-spaces";
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The extension's declaration for a new space's settings.json: the one in the base home's settings.json
|
|
146
|
+
* (PI_SPACES_BASE_HOME, else PI_CODING_AGENT_DIR, else ~/.pi/agent), else npm:@jals1212/pi-spaces.
|
|
147
|
+
*/
|
|
148
|
+
export function newSpaceDeclaration(env , home ) {
|
|
149
|
+
const baseHome = env.PI_SPACES_BASE_HOME || agentHome(env, home);
|
|
150
|
+
let packageRoot ;
|
|
151
|
+
try {
|
|
152
|
+
packageRoot = dirname(fileURLToPath(import.meta.resolve(`${EXTENSION}/package.json`)));
|
|
153
|
+
} catch {
|
|
154
|
+
packageRoot = "";
|
|
155
|
+
}
|
|
156
|
+
return findOwnDeclaration(baseHome, EXTENSION, packageRoot, `npm:${EXTENSION}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The agent home Pi uses from this shell: PI_CODING_AGENT_DIR, else ~/.pi/agent. */
|
|
160
|
+
function agentHome(env , home ) {
|
|
161
|
+
return env.PI_CODING_AGENT_DIR ? env.PI_CODING_AGENT_DIR.replace(/^~(?=\/|$)/, home) : join(home, ".pi", "agent");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
|
|
49
170
|
|
|
50
171
|
|
|
51
172
|
|
|
@@ -56,18 +177,23 @@ export function launchCommand(env , name , args )
|
|
|
56
177
|
|
|
57
178
|
|
|
58
179
|
|
|
180
|
+
|
|
181
|
+
|
|
59
182
|
|
|
60
|
-
|
|
183
|
+
|
|
61
184
|
|
|
62
185
|
|
|
63
186
|
|
|
64
|
-
/**
|
|
65
|
-
|
|
187
|
+
/**
|
|
188
|
+
* The real child process: stdio inherited, the environment unchanged. cross-spawn is plain child_process.spawn
|
|
189
|
+
* except on Windows, where it finds .cmd and .bat shims such as pi.cmd and escapes the arguments for them.
|
|
190
|
+
*/
|
|
191
|
+
export function spawnChild(program , args , options ) {
|
|
66
192
|
// Ctrl-C belongs to Pi while it runs; this process waits for it instead of dying first.
|
|
67
193
|
const ignore = () => {};
|
|
68
194
|
process.on("SIGINT", ignore);
|
|
69
195
|
return new Promise ((resolve) => {
|
|
70
|
-
const child =
|
|
196
|
+
const child = crossSpawn(program, args, options);
|
|
71
197
|
child.once("error", (error) => resolve({ status: null, error }));
|
|
72
198
|
child.once("close", (status) => resolve({ status }));
|
|
73
199
|
}).finally(() => process.off("SIGINT", ignore));
|
|
@@ -87,8 +213,16 @@ export function processIo(proc = process) {
|
|
|
87
213
|
}
|
|
88
214
|
|
|
89
215
|
export async function main(argv , io ) {
|
|
90
|
-
const dir = spacesDir(io.env, io.home);
|
|
91
216
|
const command = parseCli(argv);
|
|
217
|
+
if (command.dir !== undefined || command.command !== undefined) {
|
|
218
|
+
// A relative --dir is relative to where the command runs; the child gets it absolute.
|
|
219
|
+
const env = { ...io.env };
|
|
220
|
+
if (command.dir !== undefined) env.PI_SPACES_DIR = resolve(command.dir);
|
|
221
|
+
if (command.command !== undefined) env.PI_SPACES_COMMAND = command.command;
|
|
222
|
+
io = { ...io, env, childEnv: command.dir !== undefined ? { ...io.env, PI_SPACES_DIR: env.PI_SPACES_DIR } : undefined };
|
|
223
|
+
}
|
|
224
|
+
const commandSource = command.command !== undefined ? "--command" : "PI_SPACES_COMMAND";
|
|
225
|
+
const dir = spacesDir(io.env, io.home);
|
|
92
226
|
switch (command.kind) {
|
|
93
227
|
case "help":
|
|
94
228
|
io.stdout.write(USAGE);
|
|
@@ -105,18 +239,114 @@ export async function main(argv , io ) {
|
|
|
105
239
|
case "list": {
|
|
106
240
|
const spaces = listSpaces(dir);
|
|
107
241
|
const lines = [`Spaces in ${dir}:`, ` ${BASE}`, ...spaces.map((name) => ` ${name}`)];
|
|
108
|
-
if (spaces.length === 0) lines.push("No spaces yet. Create one with
|
|
242
|
+
if (spaces.length === 0) lines.push("No spaces yet. Create one with pi-space --new <name>.");
|
|
109
243
|
io.stdout.write(`${lines.join("\n")}\n`);
|
|
110
244
|
return io.exit(0);
|
|
111
245
|
}
|
|
246
|
+
case "new":
|
|
247
|
+
return create(command.name, dir, io);
|
|
248
|
+
case "remove":
|
|
249
|
+
return remove(command.name, command.yes, command.backup, dir, io);
|
|
112
250
|
case "launch":
|
|
113
|
-
return launch(command.name, command.args, dir, io);
|
|
251
|
+
return launch(command.name, command.args, dir, io, commandSource);
|
|
114
252
|
case "picker":
|
|
115
|
-
return pick(dir, io);
|
|
253
|
+
return pick(dir, io, commandSource);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function create(name , dir , io ) {
|
|
258
|
+
try {
|
|
259
|
+
const path = createSpace(dir, name, newSpaceDeclaration(io.env, io.home));
|
|
260
|
+
io.stdout.write(`Created space "${name}" at ${path}.\nStart Pi in it with: pi-space ${name}\n`);
|
|
261
|
+
return io.exit(0);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
// A SpaceError is a rule (a bad or taken name); anything else is the filesystem, such as no permission.
|
|
264
|
+
io.stderr.write(`pi-space: ${error instanceof SpaceError ? error.message : `could not create space "${name}": ${(error ).message}`}\n`);
|
|
265
|
+
return io.exit(1);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Without --yes it asks to remove ([y/N]), then, without --no-backup, whether to keep a backup ([Y/n]).
|
|
271
|
+
* With --yes it asks nothing and keeps a backup unless --no-backup.
|
|
272
|
+
*/
|
|
273
|
+
async function remove(name , yes , backup , dir , io ) {
|
|
274
|
+
const current = agentHome(io.env, io.home);
|
|
275
|
+
const protect = io.env.PI_SPACES_BASE_HOME ? [io.env.PI_SPACES_BASE_HOME] : [];
|
|
276
|
+
let reader ;
|
|
277
|
+
try {
|
|
278
|
+
// Fail before asking when the space cannot be removed anyway.
|
|
279
|
+
const path = checkRemovable(dir, name, current, protect, io.home);
|
|
280
|
+
let keep = backup;
|
|
281
|
+
if (!yes) {
|
|
282
|
+
if (!io.stdin.isTTY) throw new SpaceError(`Removing a space needs confirmation; run pi-space --remove ${name} --yes.`);
|
|
283
|
+
// One reader for every question, so an answer typed ahead is not lost between them.
|
|
284
|
+
reader = lineReader(io.stdin);
|
|
285
|
+
io.stdout.write(`Remove space "${name}"? This deletes ${path} and everything in it: sign-ins, packages, settings and sessions. [y/N] `);
|
|
286
|
+
if (!isYes(await reader.next())) {
|
|
287
|
+
io.stdout.write(`Kept space "${name}".\n`);
|
|
288
|
+
return io.exit(0);
|
|
289
|
+
}
|
|
290
|
+
if (backup) {
|
|
291
|
+
io.stdout.write(`Keep a backup of "${name}" in ${join(dir, BACKUPS)}? [Y/n] `);
|
|
292
|
+
// Only a clear no skips the backup: Enter or the end of input keep one.
|
|
293
|
+
const answer = (await reader.next())?.trim().toLowerCase();
|
|
294
|
+
keep = answer !== "n" && answer !== "no";
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (keep) {
|
|
298
|
+
const saved = backupSpace(dir, name, current, protect, io.home);
|
|
299
|
+
io.stdout.write(`Removed space "${name}"; its backup is ${saved}.\n`);
|
|
300
|
+
} else {
|
|
301
|
+
removeSpace(dir, name, current, protect, io.home);
|
|
302
|
+
io.stdout.write(`Removed space "${name}".\n`);
|
|
303
|
+
}
|
|
304
|
+
return io.exit(0);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
io.stderr.write(`pi-space: ${error instanceof SpaceError ? error.message : `could not remove space "${name}": ${(error ).message}`}\n`);
|
|
307
|
+
return io.exit(1);
|
|
308
|
+
} finally {
|
|
309
|
+
reader?.close();
|
|
116
310
|
}
|
|
117
311
|
}
|
|
118
312
|
|
|
119
|
-
|
|
313
|
+
function isYes(answer ) {
|
|
314
|
+
const word = answer?.trim().toLowerCase();
|
|
315
|
+
return word === "y" || word === "yes";
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
/** Reads lines one at a time from one readline interface, keeping lines that arrive before they are asked for. */
|
|
325
|
+
function lineReader(input ) {
|
|
326
|
+
const rl = createInterface({ input });
|
|
327
|
+
const lines = [];
|
|
328
|
+
const waiting = [];
|
|
329
|
+
let ended = false;
|
|
330
|
+
rl.on("line", (line) => {
|
|
331
|
+
const resolve = waiting.shift();
|
|
332
|
+
if (resolve) resolve(line);
|
|
333
|
+
else lines.push(line);
|
|
334
|
+
});
|
|
335
|
+
rl.once("close", () => {
|
|
336
|
+
ended = true;
|
|
337
|
+
for (const resolve of waiting.splice(0)) resolve(undefined);
|
|
338
|
+
});
|
|
339
|
+
return {
|
|
340
|
+
next: () => {
|
|
341
|
+
if (lines.length > 0) return Promise.resolve(lines.shift());
|
|
342
|
+
if (ended) return Promise.resolve(undefined);
|
|
343
|
+
return new Promise((resolve) => waiting.push(resolve));
|
|
344
|
+
},
|
|
345
|
+
close: () => rl.close(),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function launch(name , args , dir , io , commandSource ) {
|
|
120
350
|
if (name !== BASE) {
|
|
121
351
|
const invalid = validateName(name);
|
|
122
352
|
if (invalid) {
|
|
@@ -128,8 +358,17 @@ async function launch(name , args , dir , io )
|
|
|
128
358
|
return io.exit(1);
|
|
129
359
|
}
|
|
130
360
|
}
|
|
131
|
-
|
|
132
|
-
|
|
361
|
+
let launched ;
|
|
362
|
+
try {
|
|
363
|
+
launched = launchCommand(io.env, name, args);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
io.stderr.write(`pi-space: ${commandSource}: ${(error ).message}.\n`);
|
|
366
|
+
return io.exit(2);
|
|
367
|
+
}
|
|
368
|
+
const { program, args: programArgs } = launched;
|
|
369
|
+
const options = { stdio: "inherit" };
|
|
370
|
+
if (io.childEnv) options.env = io.childEnv;
|
|
371
|
+
const result = await io.spawn(program, programArgs, options);
|
|
133
372
|
if (result.error) {
|
|
134
373
|
io.stderr.write(`pi-space: could not start ${program}: ${result.error.message}\n`);
|
|
135
374
|
return io.exit(1);
|
|
@@ -137,11 +376,18 @@ async function launch(name , args , dir , io )
|
|
|
137
376
|
io.exit(result.status ?? 1);
|
|
138
377
|
}
|
|
139
378
|
|
|
140
|
-
async function pick(dir , io ) {
|
|
379
|
+
async function pick(dir , io , commandSource ) {
|
|
141
380
|
if (!io.stdin.isTTY) {
|
|
142
381
|
io.stderr.write(USAGE);
|
|
143
382
|
return io.exit(2);
|
|
144
383
|
}
|
|
384
|
+
// A bad command is reported before the list, not after the user has chosen.
|
|
385
|
+
try {
|
|
386
|
+
splitCommand(io.env.PI_SPACES_COMMAND ?? "");
|
|
387
|
+
} catch (error) {
|
|
388
|
+
io.stderr.write(`pi-space: ${commandSource}: ${(error ).message}.\n`);
|
|
389
|
+
return io.exit(2);
|
|
390
|
+
}
|
|
145
391
|
const names = [BASE, ...listSpaces(dir)];
|
|
146
392
|
io.stdout.write(`${[`Spaces in ${dir}:`, ...names.map((name, index) => `${index + 1}) ${name}`)].join("\n")}\nSpace number: `);
|
|
147
393
|
const line = await readLine(io.stdin);
|
|
@@ -152,7 +398,7 @@ async function pick(dir , io ) {
|
|
|
152
398
|
io.stderr.write(`pi-space: Choose a number from 1 to ${names.length}.\n`);
|
|
153
399
|
return io.exit(1);
|
|
154
400
|
}
|
|
155
|
-
return launch(names[number - 1], [], dir, io);
|
|
401
|
+
return launch(names[number - 1], [], dir, io, commandSource);
|
|
156
402
|
}
|
|
157
403
|
|
|
158
404
|
/** One line from the stream, or undefined at end of input. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jals1212/pi-spaces-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -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.2.0"
|
|
42
43
|
},
|
|
43
44
|
"publishConfig": {
|
|
44
45
|
"access": "public",
|