@jals1212/pi-spaces-cli 0.7.1 → 0.8.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 +58 -0
- package/README.md +116 -87
- package/dist/cli.js +431 -309
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
// The optional `pi-space` command: pick a space and start Pi in it, and manage the spaces and their packages.
|
|
2
|
-
// It
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// It passes `--space <name>` (and, for a package command, Pi's command after it) and the extension does the switch,
|
|
3
|
+
// but the Pi it starts runs as if started from a plain terminal for the named profile, whatever Pi the caller is in:
|
|
4
|
+
// the caller's space (PI_SPACES_BASE_HOME, PI_SPACE, PI_SPACE_HOME, PI_SPACES_BASE_ENV, its spaceEnv values and the
|
|
5
|
+
// paths inside its folder) is unwound back to its base, and PI_CODING_AGENT_DIR is the profile's home. The setup
|
|
6
|
+
// command, which is not Pi and does no switch, gets the space's own home and variables on top of that same base.
|
|
7
|
+
// Every space is named <profile>/<name>; the profile of the config file stands in for the command, the spaces folder and the setup.
|
|
5
8
|
import { readFileSync } from "node:fs";
|
|
6
9
|
import { homedir } from "node:os";
|
|
7
|
-
import { dirname, join, resolve } from "node:path";
|
|
10
|
+
import { delimiter, dirname, join, resolve, sep } from "node:path";
|
|
8
11
|
import { createInterface } from "node:readline";
|
|
9
12
|
import { fileURLToPath } from "node:url";
|
|
10
13
|
import crossSpawn from "cross-spawn";
|
|
@@ -24,7 +27,6 @@ import {
|
|
|
24
27
|
renameSpace,
|
|
25
28
|
SpaceError,
|
|
26
29
|
samePath,
|
|
27
|
-
spacesDir,
|
|
28
30
|
validateName,
|
|
29
31
|
} from "@jals1212/pi-spaces/spaces";
|
|
30
32
|
import {
|
|
@@ -34,61 +36,63 @@ import {
|
|
|
34
36
|
loadConfig,
|
|
35
37
|
|
|
36
38
|
profileByName,
|
|
39
|
+
profileForHome,
|
|
37
40
|
profileNames,
|
|
38
41
|
spaceEnvFor,
|
|
39
42
|
} from "@jals1212/pi-spaces/config";
|
|
40
43
|
|
|
41
|
-
export const USAGE = `Usage: pi-space [--command <cmd>] [--dir <path>] [--setup <cmd>] [--
|
|
44
|
+
export const USAGE = `Usage: pi-space [--command <cmd>] [--dir <path>] [--setup <cmd>] [--config <path>] [<space> [pi args...] | <command> ...]
|
|
42
45
|
pi-space --help | --version
|
|
43
|
-
|
|
44
|
-
pi-space
|
|
45
|
-
pi-space
|
|
46
|
-
pi-space list
|
|
47
|
-
pi-space
|
|
48
|
-
pi-space
|
|
49
|
-
pi-space
|
|
50
|
-
pi-space
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
pi-space
|
|
54
|
-
pi-space
|
|
55
|
-
pi-space
|
|
56
|
-
pi-space
|
|
57
|
-
pi-space --help
|
|
58
|
-
pi-space --
|
|
59
|
-
|
|
46
|
+
A <space> is <profile>/<name>: a profile of the config file (pi is always there), then the space; base is a space too, as <profile>/base.
|
|
47
|
+
pi-space pick a space and start Pi in it
|
|
48
|
+
pi-space <space> [pi args...] start Pi in a space (<profile>/base is the home that profile's Pi starts with)
|
|
49
|
+
pi-space list list the spaces of every profile
|
|
50
|
+
pi-space list <space|--every> list the packages of a space
|
|
51
|
+
pi-space new <space> create a space, then run the profile's setup command in it when there is one
|
|
52
|
+
pi-space setup <space|--every> run the setup command in a space, or in every space (never in base)
|
|
53
|
+
pi-space rename <space> <new> rename a space; the new name is bare or keeps the profile (<profile>/<new>)
|
|
54
|
+
pi-space remove <space> delete a space and everything in it, after asking, and keep a backup in .backups unless told not to
|
|
55
|
+
(--yes skips the questions; --no-backup deletes without a backup)
|
|
56
|
+
pi-space update <space|--every> update the packages of a space (more words go to Pi: --extensions, --all, a source)
|
|
57
|
+
pi-space install <space|--every> <source> install a package in a space (more words go to Pi)
|
|
58
|
+
pi-space uninstall <space|--every> <source> remove a package from a space (more words go to Pi)
|
|
59
|
+
pi-space completion zsh print the zsh completion script (source <(pi-space completion zsh))
|
|
60
|
+
pi-space <command> --help the help of one command
|
|
61
|
+
pi-space --help print this help
|
|
62
|
+
pi-space --version print the version
|
|
63
|
+
--every takes the place of the space: <profile>/--every runs in that profile's base (not for setup) and spaces, --every alone in every profile;
|
|
64
|
+
a failure does not stop the rest, and the exit status is 1 if any failed.
|
|
60
65
|
Options before the command, for this call only:
|
|
61
|
-
--command <cmd>
|
|
62
|
-
--dir <path>
|
|
63
|
-
--setup <cmd>
|
|
64
|
-
--
|
|
65
|
-
--config <path> the config file instead of $PI_SPACES_CONFIG (default: ~/.pi-spaces/config.json); the Pi it starts gets it too
|
|
66
|
+
--command <cmd> the program to run instead of the profile's command, e.g. "my-launcher --" (for one profile)
|
|
67
|
+
--dir <path> the spaces folder instead of the profile's; the Pi it starts gets it too (for one profile)
|
|
68
|
+
--setup <cmd> the setup command instead of the profile's; {home} and {name} become the space's folder and name
|
|
69
|
+
--config <path> the config file instead of $PI_SPACES_CONFIG (default: ~/.pi-spaces/config.json); the Pi it starts gets it too
|
|
66
70
|
`;
|
|
67
71
|
|
|
68
72
|
/** This package's version; package.json sits one folder above both src/ and dist/. */
|
|
69
73
|
export const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
70
74
|
|
|
71
|
-
/** --command, --dir, --setup
|
|
75
|
+
/** --command, --dir, --setup and --config: for this call only, so nothing has to be exported into the shell. */
|
|
72
76
|
|
|
73
77
|
|
|
74
78
|
|
|
75
79
|
|
|
76
|
-
|
|
77
80
|
|
|
78
81
|
|
|
79
82
|
|
|
80
83
|
/** The Pi commands that pi-space runs in one space or in every space. */
|
|
81
84
|
|
|
82
85
|
|
|
83
|
-
/**
|
|
86
|
+
/** `<profile>/<name>` (or `<profile>/base`, or `<profile>/--every` for one profile), or every space of every profile: base first, then the spaces in order. */
|
|
84
87
|
|
|
85
88
|
|
|
86
89
|
const PACKAGE_VERBS = ["list", "update", "install", "uninstall"];
|
|
87
90
|
|
|
88
91
|
/** The old options, and the command that replaced each. */
|
|
89
92
|
const REPLACED = {
|
|
90
|
-
"--new": "pi-space new <name>",
|
|
91
|
-
"--remove": "pi-space remove <name> [--yes] [--no-backup]",
|
|
93
|
+
"--new": "pi-space new <profile>/<name>",
|
|
94
|
+
"--remove": "pi-space remove <profile>/<name> [--yes] [--no-backup]",
|
|
95
|
+
"--profile": "pi-space <profile>/<name>",
|
|
92
96
|
} ;
|
|
93
97
|
|
|
94
98
|
|
|
@@ -111,7 +115,7 @@ const REPLACED = {
|
|
|
111
115
|
|
|
112
116
|
|
|
113
117
|
|
|
114
|
-
const CALL_OPTIONS = { "--command": "command", "--dir": "dir", "--setup": "setup", "--
|
|
118
|
+
const CALL_OPTIONS = { "--command": "command", "--dir": "dir", "--setup": "setup", "--config": "config" } ;
|
|
115
119
|
|
|
116
120
|
function isHelpFlag(word ) {
|
|
117
121
|
return word === "--help" || word === "-h";
|
|
@@ -119,10 +123,9 @@ function isHelpFlag(word ) {
|
|
|
119
123
|
|
|
120
124
|
/** What each call option is, for the help of the commands it applies to. */
|
|
121
125
|
const CALL_HELP = {
|
|
122
|
-
command: ["--command <cmd>", 'the program to run instead of
|
|
123
|
-
dir: ["--dir <path>", "the spaces folder instead of
|
|
124
|
-
setup: ["--setup <cmd>", "the setup command instead of
|
|
125
|
-
profile: ["--profile <name>", 'the profile of the config file instead of its "default"'],
|
|
126
|
+
command: ["--command <cmd>", 'the program to run instead of the profile\'s command, e.g. "my-launcher --"; for one profile'],
|
|
127
|
+
dir: ["--dir <path>", "the spaces folder instead of the profile's; the Pi it starts gets it too; for one profile"],
|
|
128
|
+
setup: ["--setup <cmd>", "the setup command instead of the profile's; {home} and {name} become the space's folder and name"],
|
|
126
129
|
config: ["--config <path>", "the config file instead of $PI_SPACES_CONFIG (default: ~/.pi-spaces/config.json)"],
|
|
127
130
|
};
|
|
128
131
|
|
|
@@ -143,92 +146,94 @@ const CALL_HELP = {
|
|
|
143
146
|
|
|
144
147
|
const PI_ARGS = ["[pi args...]", "more words go to Pi, e.g. --extensions, --all or a source"];
|
|
145
148
|
const NAME_OR_EVERY = [
|
|
146
|
-
["<
|
|
147
|
-
["
|
|
149
|
+
["<space>", "<profile>/<name>: a space of a profile, or <profile>/base"],
|
|
150
|
+
["<profile>/--every", "that profile's base, then its spaces in turn; a failure does not stop the rest, and the exit status is 1 if any failed"],
|
|
151
|
+
["--every", "the same in every profile"],
|
|
148
152
|
];
|
|
149
153
|
|
|
150
154
|
/** The commands, in the order completion lists them. They are reserved words: the core never lets a space be named one. */
|
|
151
155
|
const COMMANDS = {
|
|
152
156
|
list: {
|
|
153
157
|
summary: "list the spaces, or the packages of one",
|
|
154
|
-
usage: "list [<
|
|
155
|
-
about: ["With a
|
|
158
|
+
usage: "list [<space|--every>]",
|
|
159
|
+
about: ["Alone it lists the spaces of every profile. With a space it runs Pi's list in it; with <profile>/--every, in that profile's base and then its spaces; with --every, in every profile's."],
|
|
156
160
|
args: NAME_OR_EVERY,
|
|
157
161
|
options: [],
|
|
158
|
-
calls: ["command", "dir", "
|
|
159
|
-
examples: ["pi-space list", "pi-space list work", "pi-space list --every"],
|
|
162
|
+
calls: ["command", "dir", "config"],
|
|
163
|
+
examples: ["pi-space list", "pi-space list pi/work", "pi-space list pi/--every", "pi-space list --every"],
|
|
160
164
|
},
|
|
161
165
|
new: {
|
|
162
166
|
summary: "create a space",
|
|
163
|
-
usage: "new <
|
|
164
|
-
about: ["Its settings.json declares the extension. It runs the setup command in it when there is one, and does not start Pi."],
|
|
165
|
-
args: [["<
|
|
167
|
+
usage: "new <space>",
|
|
168
|
+
about: ["Its settings.json declares the extension. It runs the profile's setup command in it when there is one, and does not start Pi."],
|
|
169
|
+
args: [["<space>", "<profile>/<name>: the profile and the name of the new space"]],
|
|
166
170
|
options: [],
|
|
167
|
-
calls: ["dir", "setup", "
|
|
168
|
-
examples: ["pi-space new work", "pi-space --setup 'my-launcher --home {home} setup' new work"],
|
|
171
|
+
calls: ["dir", "setup", "config"],
|
|
172
|
+
examples: ["pi-space new pi/work", "pi-space --setup 'my-launcher --home {home} setup' new pi/work"],
|
|
169
173
|
},
|
|
170
174
|
setup: {
|
|
171
175
|
summary: "run the setup command in a space",
|
|
172
|
-
usage: "setup <
|
|
173
|
-
about: ["It needs a setup command:
|
|
176
|
+
usage: "setup <space|--every>",
|
|
177
|
+
about: ["It needs a setup command: the profile's \"setup\" or --setup. It never runs in base."],
|
|
174
178
|
args: [
|
|
175
|
-
["<
|
|
176
|
-
["
|
|
179
|
+
["<space>", "<profile>/<name>: a space of a profile"],
|
|
180
|
+
["<profile>/--every", "every space of that profile, never base; a failure does not stop the rest, and the exit status is 1 if any failed"],
|
|
181
|
+
["--every", "every space of every profile that has a setup command (or of all, with --setup)"],
|
|
177
182
|
],
|
|
178
183
|
options: [],
|
|
179
|
-
calls: ["dir", "setup", "
|
|
180
|
-
examples: ["pi-space setup work", "pi-space setup --every", "pi-space --setup 'my-launcher --home {home} setup' setup --every"],
|
|
184
|
+
calls: ["dir", "setup", "config"],
|
|
185
|
+
examples: ["pi-space setup pi/work", "pi-space setup pi/--every", "pi-space setup --every", "pi-space --setup 'my-launcher --home {home} setup' setup --every"],
|
|
181
186
|
},
|
|
182
187
|
rename: {
|
|
183
188
|
summary: "rename a space",
|
|
184
|
-
usage: "rename <
|
|
185
|
-
about: ["It refuses what remove refuses: base, the space you are in, and a space that holds the base home."],
|
|
189
|
+
usage: "rename <space> <new>",
|
|
190
|
+
about: ["It refuses what remove refuses: base, the space you are in, and a space that holds the base home. A space keeps its profile."],
|
|
186
191
|
args: [
|
|
187
|
-
["<
|
|
188
|
-
["<new>", "its new name"],
|
|
192
|
+
["<space>", "<profile>/<name>: the space to rename"],
|
|
193
|
+
["<new>", "its new name, bare or with the same profile"],
|
|
189
194
|
],
|
|
190
195
|
options: [],
|
|
191
|
-
calls: ["dir", "
|
|
192
|
-
examples: ["pi-space rename work client"],
|
|
196
|
+
calls: ["dir", "config"],
|
|
197
|
+
examples: ["pi-space rename pi/work client", "pi-space rename pi/work pi/client"],
|
|
193
198
|
},
|
|
194
199
|
remove: {
|
|
195
200
|
summary: "delete a space, keeping a backup",
|
|
196
|
-
usage: "remove <
|
|
201
|
+
usage: "remove <space> [--yes|-y] [--no-backup]",
|
|
197
202
|
about: ["It deletes the space and everything in it, after asking, and keeps a backup in .backups unless told not to."],
|
|
198
|
-
args: [["<
|
|
203
|
+
args: [["<space>", "<profile>/<name>: the space to delete"]],
|
|
199
204
|
options: [
|
|
200
205
|
{ flag: "--yes", alias: "-y", text: "do not ask; keep a backup" },
|
|
201
206
|
{ flag: "--no-backup", text: "delete without a backup" },
|
|
202
207
|
],
|
|
203
|
-
calls: ["dir", "
|
|
204
|
-
examples: ["pi-space remove work", "pi-space remove work --yes", "pi-space remove work --yes --no-backup"],
|
|
208
|
+
calls: ["dir", "config"],
|
|
209
|
+
examples: ["pi-space remove pi/work", "pi-space remove pi/work --yes", "pi-space remove pi/work --yes --no-backup"],
|
|
205
210
|
},
|
|
206
211
|
update: {
|
|
207
212
|
summary: "update the packages of a space",
|
|
208
|
-
usage: "update <
|
|
209
|
-
about: ["It runs Pi's update in a space,
|
|
213
|
+
usage: "update <space|--every> [pi args...]",
|
|
214
|
+
about: ["It runs Pi's update in a space, in a profile's base and then its spaces, or in every profile's."],
|
|
210
215
|
args: [...NAME_OR_EVERY, PI_ARGS],
|
|
211
216
|
options: [],
|
|
212
|
-
calls: ["command", "dir", "
|
|
213
|
-
examples: ["pi-space update work", "pi-space update base --extensions", "pi-space --command 'my-launcher --' update --every"],
|
|
217
|
+
calls: ["command", "dir", "config"],
|
|
218
|
+
examples: ["pi-space update pi/work", "pi-space update pi/base --extensions", "pi-space --command 'my-launcher --' update pi/--every", "pi-space update --every"],
|
|
214
219
|
},
|
|
215
220
|
install: {
|
|
216
221
|
summary: "install a package in a space",
|
|
217
|
-
usage: "install <
|
|
218
|
-
about: ["It runs Pi's install in a space,
|
|
222
|
+
usage: "install <space|--every> <source> [pi args...]",
|
|
223
|
+
about: ["It runs Pi's install in a space, in a profile's base and then its spaces, or in every profile's."],
|
|
219
224
|
args: [...NAME_OR_EVERY, ["<source>", "the package to install, e.g. npm:some-package"], PI_ARGS],
|
|
220
225
|
options: [],
|
|
221
|
-
calls: ["command", "dir", "
|
|
222
|
-
examples: ["pi-space install work npm:some-package", "pi-space install --every npm:some-package"],
|
|
226
|
+
calls: ["command", "dir", "config"],
|
|
227
|
+
examples: ["pi-space install pi/work npm:some-package", "pi-space install pi/--every npm:some-package", "pi-space install --every npm:some-package"],
|
|
223
228
|
},
|
|
224
229
|
uninstall: {
|
|
225
230
|
summary: "remove a package from a space",
|
|
226
|
-
usage: "uninstall <
|
|
227
|
-
about: ["It runs Pi's uninstall (its alias of remove) in a space,
|
|
231
|
+
usage: "uninstall <space|--every> <source> [pi args...]",
|
|
232
|
+
about: ["It runs Pi's uninstall (its alias of remove) in a space, in a profile's base and then its spaces, or in every profile's."],
|
|
228
233
|
args: [...NAME_OR_EVERY, ["<source>", "the package to remove, e.g. npm:some-package"], PI_ARGS],
|
|
229
234
|
options: [],
|
|
230
|
-
calls: ["command", "dir", "
|
|
231
|
-
examples: ["pi-space uninstall work npm:some-package", "pi-space uninstall --every npm:some-package"],
|
|
235
|
+
calls: ["command", "dir", "config"],
|
|
236
|
+
examples: ["pi-space uninstall pi/work npm:some-package", "pi-space uninstall pi/--every npm:some-package", "pi-space uninstall --every npm:some-package"],
|
|
232
237
|
},
|
|
233
238
|
completion: {
|
|
234
239
|
summary: "print the shell completion script",
|
|
@@ -269,7 +274,7 @@ function commandHelp(name ) {
|
|
|
269
274
|
}
|
|
270
275
|
|
|
271
276
|
/**
|
|
272
|
-
* The call options (--command, --dir, --setup, --
|
|
277
|
+
* The call options (--command, --dir, --setup, --config) come first; after a space name every argument belongs to Pi.
|
|
273
278
|
* The commands (list, new, setup, remove, rename, update, install, uninstall, completion) are reserved words, so they
|
|
274
279
|
* are read before a name: the core never lets a space have one of them as its name. `__complete` is the hidden
|
|
275
280
|
* command the completion script calls; it starts with "_", which no space name does.
|
|
@@ -346,7 +351,7 @@ function parsePackages(verb , rest ) {
|
|
|
346
351
|
}
|
|
347
352
|
|
|
348
353
|
/**
|
|
349
|
-
* The program and arguments that start Pi in a space:
|
|
354
|
+
* The program and arguments that start Pi in a space: PI_SPACES_COMMAND of this env (default pi) plus --space <name>.
|
|
350
355
|
* Throws when the command has an unclosed quote.
|
|
351
356
|
*/
|
|
352
357
|
export function launchCommand(env , name , args ) {
|
|
@@ -414,6 +419,46 @@ export function newSpaceDeclaration(env , home , profileHome
|
|
|
414
419
|
return findOwnDeclaration(baseHome, EXTENSION, packageRoot, `npm:${EXTENSION}`);
|
|
415
420
|
}
|
|
416
421
|
|
|
422
|
+
// The variables unwinding never rewrites: PWD and OLDPWD describe the working directory, the others are PI_SPACES_* state.
|
|
423
|
+
const KEPT_AS_IS = new Set(["PI_SPACES_DIR", "PI_SPACES_BASE_HOME", "PI_SPACES_BASE_ENV", "PWD", "OLDPWD"]);
|
|
424
|
+
const SPACE_VARIABLES = ["PI_SPACE", "PI_SPACE_HOME", "PI_SPACES_BASE_HOME", "PI_SPACES_BASE_ENV"];
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* The environment of a plain terminal for `profile`, from the caller's: inside a space (PI_SPACES_BASE_HOME is set and
|
|
428
|
+
* is not the current home) the space is unwound as the extension's relaunch does on the way back to base: every value
|
|
429
|
+
* that is the space's folder or a path inside it (each entry of a list on its own) moves to the base home, and the
|
|
430
|
+
* caller profile's spaceEnv variables get their base values from PI_SPACES_BASE_ENV (removed when it has none).
|
|
431
|
+
* Then the variables of a space are removed and PI_CODING_AGENT_DIR is the profile's home.
|
|
432
|
+
* This reimplements the small relaunchEnv step instead of exporting it: the core's exports are a public contract.
|
|
433
|
+
*/
|
|
434
|
+
function plainEnv(call , profile ) {
|
|
435
|
+
const { config, io } = call;
|
|
436
|
+
const { env } = io;
|
|
437
|
+
const base = env.PI_SPACES_BASE_HOME;
|
|
438
|
+
const current = agentHome(env, io.home);
|
|
439
|
+
let next = { ...env };
|
|
440
|
+
if (base && !samePath(current, base)) {
|
|
441
|
+
const from = resolve(current);
|
|
442
|
+
const move = (part ) => (part === from || part.startsWith(from + sep) ? resolve(base) + part.slice(from.length) : part);
|
|
443
|
+
next = {};
|
|
444
|
+
for (const [key, value] of Object.entries(env)) {
|
|
445
|
+
if (value !== undefined) next[key] = KEPT_AS_IS.has(key) ? value : value.split(delimiter).map(move).join(delimiter);
|
|
446
|
+
}
|
|
447
|
+
let kept = {};
|
|
448
|
+
try {
|
|
449
|
+
kept = JSON.parse(env.PI_SPACES_BASE_ENV ?? "{}") ?? {};
|
|
450
|
+
} catch {}
|
|
451
|
+
for (const key of Object.keys(profileForHome(config, base, env, io.home)?.spaceEnv ?? {})) {
|
|
452
|
+
delete next[key];
|
|
453
|
+
const value = kept[key];
|
|
454
|
+
if (typeof value === "string") next[key] = value;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
for (const key of SPACE_VARIABLES) delete next[key];
|
|
458
|
+
next.PI_CODING_AGENT_DIR = profile.home;
|
|
459
|
+
return next;
|
|
460
|
+
}
|
|
461
|
+
|
|
417
462
|
/** The agent home Pi uses from this shell: PI_CODING_AGENT_DIR, else ~/.pi/agent. */
|
|
418
463
|
function agentHome(env , home ) {
|
|
419
464
|
return env.PI_CODING_AGENT_DIR ? env.PI_CODING_AGENT_DIR.replace(/^~(?=\/|$)/, home) : join(home, ".pi", "agent");
|
|
@@ -421,7 +466,7 @@ function agentHome(env , home ) {
|
|
|
421
466
|
|
|
422
467
|
|
|
423
468
|
|
|
424
|
-
|
|
469
|
+
|
|
425
470
|
|
|
426
471
|
|
|
427
472
|
|
|
@@ -435,7 +480,7 @@ function agentHome(env , home ) {
|
|
|
435
480
|
|
|
436
481
|
|
|
437
482
|
|
|
438
|
-
|
|
483
|
+
|
|
439
484
|
|
|
440
485
|
|
|
441
486
|
|
|
@@ -443,7 +488,7 @@ function agentHome(env , home ) {
|
|
|
443
488
|
|
|
444
489
|
|
|
445
490
|
/**
|
|
446
|
-
* The real child process: stdio inherited, the environment
|
|
491
|
+
* The real child process: stdio inherited, the environment as the CLI gives it. cross-spawn is plain child_process.spawn
|
|
447
492
|
* except on Windows, where it finds .cmd and .bat shims such as pi.cmd and escapes the arguments for them.
|
|
448
493
|
*/
|
|
449
494
|
export function spawnChild(program , args , options ) {
|
|
@@ -470,82 +515,92 @@ export function processIo(proc = process) {
|
|
|
470
515
|
};
|
|
471
516
|
}
|
|
472
517
|
|
|
473
|
-
/**
|
|
474
|
-
|
|
518
|
+
/** What one call settled before it looks at any profile: the caller's io, its options and the config file. */
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
|
|
475
527
|
|
|
476
|
-
/**
|
|
477
|
-
|
|
478
|
-
|
|
528
|
+
/** One profile under this call: its folder, the io of the Pi it starts, and where its command and setup came from. */
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
|
|
479
532
|
|
|
533
|
+
|
|
480
534
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
535
|
|
|
485
536
|
|
|
486
537
|
|
|
487
538
|
|
|
488
|
-
/**
|
|
489
|
-
|
|
490
|
-
return config?.profiles.find((spec) => spec.name === profile.name)?.dir !== undefined;
|
|
491
|
-
}
|
|
539
|
+
/** What stands for "every space of a profile" after the slash of `<profile>/--every`. */
|
|
540
|
+
const EVERY = "--every";
|
|
492
541
|
|
|
493
542
|
/**
|
|
494
|
-
* The
|
|
495
|
-
* the
|
|
543
|
+
* The profile `name` under this call's options: the command, spaces folder and setup are the option's, else the
|
|
544
|
+
* profile's; the caller's PI_SPACES_COMMAND, PI_SPACES_DIR and PI_SPACES_SETUP are never read. The started Pi gets
|
|
545
|
+
* the caller's environment with those three set to what was settled (the command and the setup removed when there
|
|
546
|
+
* is none), and PI_SPACES_CONFIG when --config was given. io.env stays the caller's (the protections of remove and
|
|
547
|
+
* rename read the Pi the caller is in); io.childEnv, what Pi and the setup command start from, is the caller's
|
|
548
|
+
* environment unwound to a plain terminal of the profile (see plainEnv). Throws a ConfigError for an unknown profile.
|
|
496
549
|
*/
|
|
497
|
-
function
|
|
498
|
-
const
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
550
|
+
function scopeOf(call , name ) {
|
|
551
|
+
const { options, config, configFile, io: caller } = call;
|
|
552
|
+
const profile = profileByName(config, name, caller.env, caller.home);
|
|
553
|
+
// Absolute: a relative one is relative to where the command runs.
|
|
554
|
+
const dir = options.dir !== undefined ? resolve(options.dir) : profile.dir;
|
|
555
|
+
const command = options.command ?? profile.command;
|
|
556
|
+
const setup = options.setup ?? profile.setup;
|
|
557
|
+
const handed = (from ) => {
|
|
558
|
+
const env = { ...from, PI_SPACES_DIR: dir };
|
|
559
|
+
if (command !== undefined) env.PI_SPACES_COMMAND = command;
|
|
560
|
+
else delete env.PI_SPACES_COMMAND;
|
|
561
|
+
if (setup !== undefined) env.PI_SPACES_SETUP = setup;
|
|
562
|
+
else delete env.PI_SPACES_SETUP;
|
|
563
|
+
if (options.config !== undefined) env.PI_SPACES_CONFIG = configFile;
|
|
564
|
+
return env;
|
|
565
|
+
};
|
|
566
|
+
return {
|
|
567
|
+
profile,
|
|
568
|
+
io: { ...caller, env: handed(caller.env), childEnv: handed(plainEnv(call, profile)) },
|
|
569
|
+
dir,
|
|
570
|
+
commandSource: options.command !== undefined ? "--command" : `profile "${name}" command`,
|
|
571
|
+
setupSource: options.setup !== undefined ? "--setup" : `profile "${name}" setup`,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** The scopes of every profile, in the order of profileNames. */
|
|
576
|
+
function allScopes(call ) {
|
|
577
|
+
return profileNames(call.config).map((name) => scopeOf(call, name));
|
|
508
578
|
}
|
|
509
579
|
|
|
510
580
|
/**
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
* PI_SPACES_COMMAND, PI_SPACES_DIR and PI_SPACES_SETUP as ever); the started Pi gets --dir, --setup, --config and
|
|
514
|
-
* what a profile supplies for the folder and the setup, but never --command. Throws a ConfigError.
|
|
581
|
+
* Splits `<profile>/<name>` at the first "/". A name without its profile exits 2 with the form to use, and gives
|
|
582
|
+
* undefined, so the caller returns.
|
|
515
583
|
*/
|
|
516
|
-
function
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
584
|
+
function qualify(text , call ) {
|
|
585
|
+
const slash = text.indexOf("/");
|
|
586
|
+
if (slash > 0) return { profile: text.slice(0, slash), name: text.slice(slash + 1) };
|
|
587
|
+
call.io.stderr.write(`pi-space: Name the space with its profile, as <profile>/${slash === 0 ? text.slice(1) : text} (profiles: ${profileNames(call.config).join(", ")}).\n`);
|
|
588
|
+
call.io.exit(2);
|
|
589
|
+
return undefined;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** Whether a command runs in more than one profile: the picker, `list` alone and --every alone. */
|
|
593
|
+
function spansProfiles(command ) {
|
|
594
|
+
switch (command.kind) {
|
|
595
|
+
case "picker":
|
|
596
|
+
case "list":
|
|
597
|
+
return true;
|
|
598
|
+
case "packages":
|
|
599
|
+
case "setup":
|
|
600
|
+
return "every" in command.target;
|
|
601
|
+
default:
|
|
602
|
+
return false;
|
|
534
603
|
}
|
|
535
|
-
const command = choose("command", call, asked, fallback, io.env);
|
|
536
|
-
const setup = choose("setup", call, asked, fallback, io.env);
|
|
537
|
-
if (command && command.origin !== "env") env.PI_SPACES_COMMAND = command.value;
|
|
538
|
-
if (setup && setup.origin !== "env") forChild.PI_SPACES_SETUP = env.PI_SPACES_SETUP = setup.value;
|
|
539
|
-
Object.assign(env, forChild);
|
|
540
|
-
const dir = spacesDir(env, io.home, fallback?.dir);
|
|
541
|
-
return {
|
|
542
|
-
io: { ...io, env, childEnv: Object.keys(forChild).length > 0 ? { ...io.env, ...forChild } : undefined },
|
|
543
|
-
dir,
|
|
544
|
-
config,
|
|
545
|
-
profile: asked ?? fallback,
|
|
546
|
-
commandSource: command?.source ?? "PI_SPACES_COMMAND",
|
|
547
|
-
setupSource: setup?.source ?? "PI_SPACES_SETUP",
|
|
548
|
-
};
|
|
549
604
|
}
|
|
550
605
|
|
|
551
606
|
export async function main(argv , io ) {
|
|
@@ -578,40 +633,69 @@ export async function main(argv , io ) {
|
|
|
578
633
|
case "complete":
|
|
579
634
|
return complete(command.words, io);
|
|
580
635
|
}
|
|
581
|
-
const
|
|
582
|
-
let settings ;
|
|
636
|
+
const configFile = command.config !== undefined ? resolve(command.config) : configPath(io.env, io.home);
|
|
583
637
|
try {
|
|
584
|
-
|
|
638
|
+
const config = loadConfig(configFile);
|
|
639
|
+
if (config?.default !== undefined) {
|
|
640
|
+
io.stderr.write(`pi-space: ${configFile}: "default" is no longer used; name each space with its profile (<profile>/<name>) and remove "default" from the file.\n`);
|
|
641
|
+
}
|
|
642
|
+
const call = { options: command, config, configFile, io };
|
|
643
|
+
if (spansProfiles(command) && (command.command !== undefined || command.dir !== undefined)) {
|
|
644
|
+
io.stderr.write("pi-space: --command and --dir apply to one profile; name it: <profile>/<name> or <profile>/--every.\n");
|
|
645
|
+
return io.exit(2);
|
|
646
|
+
}
|
|
647
|
+
switch (command.kind) {
|
|
648
|
+
case "list":
|
|
649
|
+
return listProfiles(call);
|
|
650
|
+
case "packages":
|
|
651
|
+
return await runPackages(command.verb, command.target, command.args, call);
|
|
652
|
+
case "new": {
|
|
653
|
+
const ref = qualify(command.name, call);
|
|
654
|
+
return ref && (await create(ref.name, scopeOf(call, ref.profile)));
|
|
655
|
+
}
|
|
656
|
+
case "setup":
|
|
657
|
+
return await runSetup(command.target, call);
|
|
658
|
+
case "rename": {
|
|
659
|
+
const ref = qualify(command.from, call);
|
|
660
|
+
if (!ref) return;
|
|
661
|
+
// The new name is bare or carries the same profile: a space never changes profile.
|
|
662
|
+
const slash = command.to.indexOf("/");
|
|
663
|
+
if (slash >= 0 && command.to.slice(0, slash) !== ref.profile) {
|
|
664
|
+
io.stderr.write(`pi-space: A space keeps its profile: rename ${ref.profile}/${ref.name} to ${ref.profile}/<name>.\n`);
|
|
665
|
+
return io.exit(2);
|
|
666
|
+
}
|
|
667
|
+
return rename(ref.name, slash >= 0 ? command.to.slice(slash + 1) : command.to, scopeOf(call, ref.profile));
|
|
668
|
+
}
|
|
669
|
+
case "remove": {
|
|
670
|
+
const ref = qualify(command.name, call);
|
|
671
|
+
return ref && (await remove(ref.name, command.yes, command.backup, scopeOf(call, ref.profile)));
|
|
672
|
+
}
|
|
673
|
+
case "launch": {
|
|
674
|
+
const ref = qualify(command.name, call);
|
|
675
|
+
return ref && (await launch(ref.name, command.args, scopeOf(call, ref.profile)));
|
|
676
|
+
}
|
|
677
|
+
case "picker":
|
|
678
|
+
return await pick(call);
|
|
679
|
+
}
|
|
585
680
|
} catch (error) {
|
|
586
681
|
if (!(error instanceof ConfigError)) throw error;
|
|
587
682
|
io.stderr.write(`pi-space: ${error.message}\n`);
|
|
588
683
|
return io.exit(2);
|
|
589
684
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
}
|
|
600
|
-
case "packages":
|
|
601
|
-
return runPackages(command.verb, command.target, command.args, dir, io, commandSource);
|
|
602
|
-
case "new":
|
|
603
|
-
return create(command.name, dir, io, setupSource, profile);
|
|
604
|
-
case "setup":
|
|
605
|
-
return runSetup(command.target, dir, io, setupSource, profile);
|
|
606
|
-
case "rename":
|
|
607
|
-
return rename(command.from, command.to, dir, io, profile);
|
|
608
|
-
case "remove":
|
|
609
|
-
return remove(command.name, command.yes, command.backup, dir, io, profile);
|
|
610
|
-
case "launch":
|
|
611
|
-
return launch(command.name, command.args, dir, io, commandSource);
|
|
612
|
-
case "picker":
|
|
613
|
-
return pick(command, settings, callerIo);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/** `pi-space list`: every profile, its folder, then its base and spaces as `<profile>/<name>`. */
|
|
688
|
+
function listProfiles(call ) {
|
|
689
|
+
const lines = [];
|
|
690
|
+
let any = false;
|
|
691
|
+
for (const { profile, dir } of allScopes(call)) {
|
|
692
|
+
const spaces = listSpaces(dir);
|
|
693
|
+
any ||= spaces.length > 0;
|
|
694
|
+
lines.push(`Spaces of ${profile.name} in ${dir}:`, ` ${profile.name}/${BASE}`, ...spaces.map((name) => ` ${profile.name}/${name}`));
|
|
614
695
|
}
|
|
696
|
+
if (!any) lines.push("No spaces yet. Create one with pi-space new <profile>/<name>.");
|
|
697
|
+
call.io.stdout.write(`${lines.join("\n")}\n`);
|
|
698
|
+
return call.io.exit(0);
|
|
615
699
|
}
|
|
616
700
|
|
|
617
701
|
const SHELLS = ["zsh"];
|
|
@@ -644,7 +728,7 @@ const ZSH_COMPLETION = [
|
|
|
644
728
|
"#compdef pi-space",
|
|
645
729
|
"# zsh completion for pi-space. Load it from .zshrc with: source <(pi-space completion zsh)",
|
|
646
730
|
"_pi_space() {",
|
|
647
|
-
" local -a lines cmds spaces targets
|
|
731
|
+
" local -a lines cmds spaces targets shells opts",
|
|
648
732
|
" local line group value text ret=1",
|
|
649
733
|
' lines=("${(@f)$(command "${words[1]}" __complete "${(@)words[2,CURRENT-1]}" 2>/dev/null)}")',
|
|
650
734
|
` if [[ "\${lines[1]}" == ${FILES} ]]; then`,
|
|
@@ -663,7 +747,6 @@ const ZSH_COMPLETION = [
|
|
|
663
747
|
' command) cmds+=("$value:$text") ;;',
|
|
664
748
|
' space) spaces+=("$value:$text") ;;',
|
|
665
749
|
' target) targets+=("$value:$text") ;;',
|
|
666
|
-
' profile) profiles+=("$value:$text") ;;',
|
|
667
750
|
' shell) shells+=("$value:$text") ;;',
|
|
668
751
|
' option) [[ "$PREFIX" == -* ]] && opts+=("$value:$text") ;;',
|
|
669
752
|
" esac",
|
|
@@ -671,7 +754,6 @@ const ZSH_COMPLETION = [
|
|
|
671
754
|
" (( $#cmds )) && _describe -t commands 'command' cmds && ret=0",
|
|
672
755
|
" (( $#spaces )) && _describe -t spaces 'space' spaces && ret=0",
|
|
673
756
|
" (( $#targets )) && _describe -t targets 'target' targets && ret=0",
|
|
674
|
-
" (( $#profiles )) && _describe -t profiles 'profile' profiles && ret=0",
|
|
675
757
|
" (( $#shells )) && _describe -t shells 'shell' shells && ret=0",
|
|
676
758
|
" (( $#opts )) && _describe -t options 'option' opts && ret=0",
|
|
677
759
|
" return ret",
|
|
@@ -681,7 +763,7 @@ const ZSH_COMPLETION = [
|
|
|
681
763
|
].join("\n");
|
|
682
764
|
|
|
683
765
|
/** The groups of a `__complete` line; `target` is --every, which stands in for a space name. */
|
|
684
|
-
|
|
766
|
+
|
|
685
767
|
|
|
686
768
|
/** One `__complete` line; the description never holds a tab or a newline. */
|
|
687
769
|
function candidate(group , value , text ) {
|
|
@@ -719,26 +801,41 @@ function firstOptions(call , alone ) {
|
|
|
719
801
|
return [...calls, ...general];
|
|
720
802
|
}
|
|
721
803
|
|
|
722
|
-
/**
|
|
723
|
-
|
|
724
|
-
|
|
804
|
+
/** A profile and the names in its spaces folder, as `__complete` offers them. */
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
/** `<profile>/base`, if asked, then `<profile>/<name>`, for each profile in order. */
|
|
813
|
+
function spaceCandidates(profiles , withBase ) {
|
|
814
|
+
return profiles.flatMap((profile) => [
|
|
815
|
+
...(withBase ? [candidate("space", `${profile.name}/${BASE}`, `the home ${profile.name} starts with`)] : []),
|
|
816
|
+
...profile.spaces.map((name) => candidate("space", `${profile.name}/${name}`, profile.command)),
|
|
817
|
+
]);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/** The `--every` targets: one for every profile, then one `<profile>/--every` for each. */
|
|
821
|
+
function everyCandidates(profiles , every , each ) {
|
|
822
|
+
return [candidate("target", "--every", every), ...profiles.map((profile) => candidate("target", `${profile.name}/--every`, each(profile.name)))];
|
|
725
823
|
}
|
|
726
824
|
|
|
727
825
|
/** What follows a command word: its target, its own options and --help. */
|
|
728
|
-
function commandArguments(command ,
|
|
729
|
-
const every = (text ) => candidate("target", "--every", text);
|
|
826
|
+
function commandArguments(command , profiles ) {
|
|
730
827
|
const options = [...COMMANDS[command].options, HELP_OPTION].map((option) => candidate("option", option.flag, option.text));
|
|
731
828
|
switch (command) {
|
|
732
829
|
case "remove":
|
|
733
830
|
case "rename":
|
|
734
|
-
return [...spaceCandidates(
|
|
831
|
+
return [...spaceCandidates(profiles, false), ...options];
|
|
735
832
|
case "setup":
|
|
736
|
-
return [...spaceCandidates(
|
|
833
|
+
return [...spaceCandidates(profiles, false), ...everyCandidates(profiles, "every space of every profile with a setup command", (name) => `every space of ${name}`), ...options];
|
|
737
834
|
case "list":
|
|
738
835
|
case "update":
|
|
739
836
|
case "install":
|
|
740
837
|
case "uninstall":
|
|
741
|
-
return [...spaceCandidates(
|
|
838
|
+
return [...spaceCandidates(profiles, true), ...everyCandidates(profiles, "every profile, base first", (name) => `base and every space of ${name}`), ...options];
|
|
742
839
|
case "completion":
|
|
743
840
|
return [...SHELLS.map((shell) => candidate("shell", shell, `the ${shell} completion script`)), ...options];
|
|
744
841
|
default:
|
|
@@ -752,37 +849,29 @@ function candidates(words , io ) {
|
|
|
752
849
|
while (Object.hasOwn(CALL_OPTIONS, words[index] ?? "")) {
|
|
753
850
|
const option = words[index] ;
|
|
754
851
|
// The option is the last word: the cursor is on its value.
|
|
755
|
-
if (index + 1 >= words.length) return optionValues(option
|
|
852
|
+
if (index + 1 >= words.length) return optionValues(option);
|
|
756
853
|
call[CALL_OPTIONS[option]] = words[index + 1];
|
|
757
854
|
index += 2;
|
|
758
855
|
}
|
|
759
|
-
const
|
|
856
|
+
const configFile = call.config !== undefined ? resolve(call.config) : configPath(io.env, io.home);
|
|
857
|
+
const config = loadConfig(configFile);
|
|
858
|
+
// A --dir typed before the cursor is read as the spaces folder of every profile.
|
|
859
|
+
const dirOverride = call.dir === undefined ? undefined : resolve(call.dir);
|
|
860
|
+
const profiles = profileNames(config).map((name) => {
|
|
861
|
+
const profile = profileByName(config, name, io.env, io.home);
|
|
862
|
+
return { name, command: profile.command ?? "pi", spaces: listSpaces(dirOverride ?? profile.dir) };
|
|
863
|
+
});
|
|
760
864
|
const rest = words.slice(index);
|
|
761
|
-
|
|
762
|
-
if (rest.length === 0) return [...commandCandidates(false), ...spaceCandidates(spaces, true), ...firstOptions(call, index === 0)];
|
|
865
|
+
if (rest.length === 0) return [...commandCandidates(false), ...spaceCandidates(profiles, true), ...firstOptions(call, index === 0)];
|
|
763
866
|
if (rest.length > 1) return [];
|
|
764
867
|
if (isHelpFlag(rest[0])) return commandCandidates(true);
|
|
765
868
|
if (!Object.hasOwn(COMMANDS, rest[0])) return [];
|
|
766
|
-
return commandArguments(rest[0],
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
/** The candidates for the value of a call option: profile names with their command, the shell's files for a path, nothing for a command. */
|
|
770
|
-
function optionValues(option , call , io ) {
|
|
771
|
-
if (option === "--profile") {
|
|
772
|
-
const { config } = resolveSettings(call, io);
|
|
773
|
-
return profileNames(config).map((name) => candidate("profile", name, profileText(config, name, io)));
|
|
774
|
-
}
|
|
775
|
-
if (option === "--config" || option === "--dir") return [FILES];
|
|
776
|
-
return [];
|
|
869
|
+
return commandArguments(rest[0], profiles);
|
|
777
870
|
}
|
|
778
871
|
|
|
779
|
-
/**
|
|
780
|
-
function
|
|
781
|
-
|
|
782
|
-
return profileByName(config, name, io.env, io.home).command || "profile";
|
|
783
|
-
} catch {
|
|
784
|
-
return "profile";
|
|
785
|
-
}
|
|
872
|
+
/** The candidates for the value of a call option: the shell's files for a path, nothing for a command. */
|
|
873
|
+
function optionValues(option ) {
|
|
874
|
+
return option === "--config" || option === "--dir" ? [FILES] : [];
|
|
786
875
|
}
|
|
787
876
|
|
|
788
877
|
/** Prints the reason and returns false when the setup command has an unclosed quote. */
|
|
@@ -797,34 +886,35 @@ function checkSetup(io , source ) {
|
|
|
797
886
|
}
|
|
798
887
|
|
|
799
888
|
/** Runs a setup command in a space, as that space's agent home (with the profile's spaceEnv), and returns its exit status. */
|
|
800
|
-
function spawnSetup(setup , name , home , io , profile
|
|
889
|
+
function spawnSetup(setup , name , home , io , profile ) {
|
|
801
890
|
// The profile's spaceEnv, for this space, then the three variables of the space, which a profile cannot set.
|
|
802
|
-
const spaceEnv =
|
|
803
|
-
return spawnPi(setup.program, setup.args, io, { ...io.env, ...spaceEnv, PI_CODING_AGENT_DIR: home, PI_SPACE: name, PI_SPACE_HOME: home });
|
|
891
|
+
const spaceEnv = spaceEnvFor(profile, home, name);
|
|
892
|
+
return spawnPi(setup.program, setup.args, io, { ...(io.childEnv ?? io.env), ...spaceEnv, PI_CODING_AGENT_DIR: home, PI_SPACE: name, PI_SPACE_HOME: home });
|
|
804
893
|
}
|
|
805
894
|
|
|
806
|
-
/** The setup command for one space, from PI_SPACES_SETUP
|
|
895
|
+
/** The setup command for one space, from PI_SPACES_SETUP of this profile's io; checkSetup has accepted it. */
|
|
807
896
|
function setupFor(name , home , io ) {
|
|
808
897
|
return setupCommand(io.env.PI_SPACES_SETUP ?? "", home, name);
|
|
809
898
|
}
|
|
810
899
|
|
|
811
|
-
async function create(name ,
|
|
900
|
+
async function create(name , scope ) {
|
|
901
|
+
const { io, dir, profile } = scope;
|
|
812
902
|
const withSetup = hasSetupCommand(io.env);
|
|
813
903
|
// A bad setup command is reported before anything is created.
|
|
814
|
-
if (withSetup && !checkSetup(io, setupSource)) return io.exit(2);
|
|
904
|
+
if (withSetup && !checkSetup(io, scope.setupSource)) return io.exit(2);
|
|
815
905
|
try {
|
|
816
|
-
const path = createSpace(dir, name, newSpaceDeclaration(io.env, io.home, profile
|
|
906
|
+
const path = createSpace(dir, name, newSpaceDeclaration(io.env, io.home, profile.home));
|
|
817
907
|
io.stdout.write(`Created space "${name}" at ${path}.\n`);
|
|
818
908
|
if (withSetup) {
|
|
819
909
|
const setup = setupFor(name, path, io);
|
|
820
910
|
io.stdout.write(`Setting it up: ${[setup.program, ...setup.args].join(" ")}\n`);
|
|
821
911
|
const status = await spawnSetup(setup, name, path, io, profile);
|
|
822
912
|
if (status !== 0) {
|
|
823
|
-
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`);
|
|
913
|
+
io.stderr.write(`pi-space: Setup failed in space "${name}" (exit status ${status}); the space is kept. Run it again with: pi-space setup ${profile.name}/${name}\n`);
|
|
824
914
|
return io.exit(status);
|
|
825
915
|
}
|
|
826
916
|
}
|
|
827
|
-
io.stdout.write(`Start Pi in it with: pi-space ${name}\n`);
|
|
917
|
+
io.stdout.write(`Start Pi in it with: pi-space ${profile.name}/${name}\n`);
|
|
828
918
|
return io.exit(0);
|
|
829
919
|
} catch (error) {
|
|
830
920
|
// A SpaceError is a rule (a bad or taken name); anything else is the filesystem, such as no permission.
|
|
@@ -833,13 +923,14 @@ async function create(name , dir , io , setupSource ,
|
|
|
833
923
|
}
|
|
834
924
|
}
|
|
835
925
|
|
|
836
|
-
/** The base homes a space must not be: the one Pi started with, and the
|
|
837
|
-
function protectedHomes(io , profile
|
|
838
|
-
return [io.env.PI_SPACES_BASE_HOME, profile
|
|
926
|
+
/** The base homes a space must not be: the one Pi started with, and the profile's. */
|
|
927
|
+
function protectedHomes(io , profile ) {
|
|
928
|
+
return [io.env.PI_SPACES_BASE_HOME, profile.home].filter((home) => Boolean(home));
|
|
839
929
|
}
|
|
840
930
|
|
|
841
931
|
/** Renames a space after the same checks as remove: not base, not the active space, not one that holds the base home. */
|
|
842
|
-
function rename(from , to ,
|
|
932
|
+
function rename(from , to , scope ) {
|
|
933
|
+
const { io, dir, profile } = scope;
|
|
843
934
|
try {
|
|
844
935
|
const protect = protectedHomes(io, profile);
|
|
845
936
|
renameSpace(dir, from, to, agentHome(io.env, io.home), protect, io.home);
|
|
@@ -855,7 +946,8 @@ function rename(from , to , dir , io , profile
|
|
|
855
946
|
* Without --yes it asks to remove ([y/N]), then, without --no-backup, whether to keep a backup ([Y/n]).
|
|
856
947
|
* With --yes it asks nothing and keeps a backup unless --no-backup.
|
|
857
948
|
*/
|
|
858
|
-
async function remove(name , yes , backup ,
|
|
949
|
+
async function remove(name , yes , backup , scope ) {
|
|
950
|
+
const { io, dir, profile } = scope;
|
|
859
951
|
const current = agentHome(io.env, io.home);
|
|
860
952
|
const protect = protectedHomes(io, profile);
|
|
861
953
|
let reader ;
|
|
@@ -864,7 +956,7 @@ async function remove(name , yes , backup , dir ,
|
|
|
864
956
|
const path = checkRemovable(dir, name, current, protect, io.home);
|
|
865
957
|
let keep = backup;
|
|
866
958
|
if (!yes) {
|
|
867
|
-
if (!io.stdin.isTTY) throw new SpaceError(`Removing a space needs confirmation; run pi-space remove ${name} --yes.`);
|
|
959
|
+
if (!io.stdin.isTTY) throw new SpaceError(`Removing a space needs confirmation; run pi-space remove ${profile.name}/${name} --yes.`);
|
|
868
960
|
// One reader for every question, so an answer typed ahead is not lost between them.
|
|
869
961
|
reader = lineReader(io.stdin);
|
|
870
962
|
io.stdout.write(`Remove space "${name}"? This deletes ${path} and everything in it: sign-ins, packages, settings and sessions. [y/N] `);
|
|
@@ -961,40 +1053,63 @@ async function spawnPi(program , programArgs , io , env
|
|
|
961
1053
|
return result.status ?? 1;
|
|
962
1054
|
}
|
|
963
1055
|
|
|
964
|
-
async function launch(name , args ,
|
|
965
|
-
|
|
1056
|
+
async function launch(name , args , scope ) {
|
|
1057
|
+
const { io } = scope;
|
|
1058
|
+
if (!checkSpace(name, scope.dir, io)) return io.exit(1);
|
|
966
1059
|
let launched ;
|
|
967
1060
|
try {
|
|
968
1061
|
launched = launchCommand(io.env, name, args);
|
|
969
1062
|
} catch (error) {
|
|
970
|
-
io.stderr.write(`pi-space: ${commandSource}: ${(error ).message}.\n`);
|
|
1063
|
+
io.stderr.write(`pi-space: ${scope.commandSource}: ${(error ).message}.\n`);
|
|
971
1064
|
return io.exit(2);
|
|
972
1065
|
}
|
|
973
1066
|
io.exit(await spawnPi(launched.program, launched.args, io, io.childEnv));
|
|
974
1067
|
}
|
|
975
1068
|
|
|
976
1069
|
/**
|
|
977
|
-
* Runs one of Pi's package commands in a space,
|
|
978
|
-
* Pi (`--space <name> <verb> [args...]`): the extension relaunches
|
|
979
|
-
* With --every each run gets a `== <name>` header, one
|
|
1070
|
+
* Runs one of Pi's package commands in a space, in base and then every space of a profile (`<profile>/--every`),
|
|
1071
|
+
* or in every profile (`--every`), through the same Pi (`--space <name> <verb> [args...]`): the extension relaunches
|
|
1072
|
+
* it in that home, so the home is never set here. With --every each run gets a `== <profile>/<name>` header, one
|
|
1073
|
+
* failure does not stop the rest, and the status is 1 if any failed.
|
|
980
1074
|
*/
|
|
981
|
-
async function runPackages(verb , target , args ,
|
|
982
|
-
const
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1075
|
+
async function runPackages(verb , target , args , call ) {
|
|
1076
|
+
const { io } = call;
|
|
1077
|
+
let scopes ;
|
|
1078
|
+
if ("every" in target) {
|
|
1079
|
+
scopes = allScopes(call);
|
|
1080
|
+
} else {
|
|
1081
|
+
const ref = qualify(target.name, call);
|
|
1082
|
+
if (!ref) return;
|
|
1083
|
+
const scope = scopeOf(call, ref.profile);
|
|
1084
|
+
if (ref.name !== EVERY) {
|
|
1085
|
+
if (!checkSpace(ref.name, scope.dir, io)) return io.exit(1);
|
|
1086
|
+
let launched ;
|
|
1087
|
+
try {
|
|
1088
|
+
launched = launchCommand(scope.io.env, ref.name, [verb, ...args]);
|
|
1089
|
+
} catch (error) {
|
|
1090
|
+
io.stderr.write(`pi-space: ${scope.commandSource}: ${(error ).message}.\n`);
|
|
1091
|
+
return io.exit(2);
|
|
1092
|
+
}
|
|
1093
|
+
return io.exit(await spawnPi(launched.program, launched.args, scope.io, scope.io.childEnv));
|
|
1094
|
+
}
|
|
1095
|
+
scopes = [scope];
|
|
1096
|
+
}
|
|
1097
|
+
// Every command is checked before the first one runs, so a bad one is reported once, before any header.
|
|
1098
|
+
for (const scope of scopes) {
|
|
987
1099
|
try {
|
|
988
|
-
|
|
1100
|
+
launchCommand(scope.io.env, BASE, []);
|
|
989
1101
|
} catch (error) {
|
|
990
|
-
|
|
991
|
-
io.stderr.write(`pi-space: ${commandSource}: ${(error ).message}.\n`);
|
|
1102
|
+
io.stderr.write(`pi-space: ${scope.commandSource}: ${(error ).message}.\n`);
|
|
992
1103
|
return io.exit(2);
|
|
993
1104
|
}
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1105
|
+
}
|
|
1106
|
+
const failed = [];
|
|
1107
|
+
for (const scope of scopes) {
|
|
1108
|
+
for (const name of [BASE, ...listSpaces(scope.dir)]) {
|
|
1109
|
+
const launched = launchCommand(scope.io.env, name, [verb, ...args]);
|
|
1110
|
+
io.stdout.write(`== ${scope.profile.name}/${name}\n`);
|
|
1111
|
+
if ((await spawnPi(launched.program, launched.args, scope.io, scope.io.childEnv)) !== 0) failed.push(`${scope.profile.name}/${name}`);
|
|
1112
|
+
}
|
|
998
1113
|
}
|
|
999
1114
|
if (failed.length > 0) {
|
|
1000
1115
|
io.stderr.write(`pi-space: ${verb} failed in: ${failed.join(", ")}\n`);
|
|
@@ -1003,36 +1118,67 @@ async function runPackages(verb , target , args
|
|
|
1003
1118
|
io.exit(0);
|
|
1004
1119
|
}
|
|
1005
1120
|
|
|
1121
|
+
/** Runs the setup command of a profile in each of `names`, with a `== <profile>/<name>` header; the failures are added to `failed`. */
|
|
1122
|
+
async function setupEach(scope , names , failed ) {
|
|
1123
|
+
for (const name of names) {
|
|
1124
|
+
scope.io.stdout.write(`== ${scope.profile.name}/${name}\n`);
|
|
1125
|
+
const home = join(scope.dir, name);
|
|
1126
|
+
if ((await spawnSetup(setupFor(name, home, scope.io), name, home, scope.io, scope.profile)) !== 0) failed.push(`${scope.profile.name}/${name}`);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1006
1130
|
/**
|
|
1007
|
-
* Runs the setup command in a space,
|
|
1008
|
-
* With --every each run gets a `== <name>`
|
|
1131
|
+
* Runs the setup command in a space, in every space of a profile (`<profile>/--every`), or in every space of every
|
|
1132
|
+
* profile that has a setup command (`--every`); never in base. With --every each run gets a `== <profile>/<name>`
|
|
1133
|
+
* header, one failure does not stop the rest, and the status is 1 if any failed.
|
|
1009
1134
|
*/
|
|
1010
|
-
async function runSetup(target ,
|
|
1011
|
-
|
|
1012
|
-
|
|
1135
|
+
async function runSetup(target , call ) {
|
|
1136
|
+
const { io } = call;
|
|
1137
|
+
const noSetup = (name ) => `pi-space: Profile "${name}" has no setup command; give it a "setup" in the config file, or give --setup <cmd>.\n`;
|
|
1138
|
+
if ("every" in target) {
|
|
1139
|
+
const scopes = allScopes(call).filter((scope) => hasSetupCommand(scope.io.env));
|
|
1140
|
+
if (scopes.length === 0) {
|
|
1141
|
+
io.stderr.write('pi-space: No profile has a setup command; give one a "setup" in the config file, or give --setup <cmd>.\n');
|
|
1142
|
+
return io.exit(2);
|
|
1143
|
+
}
|
|
1144
|
+
// Every setup command is checked before the first one runs.
|
|
1145
|
+
if (!scopes.every((scope) => checkSetup(scope.io, scope.setupSource))) return io.exit(2);
|
|
1146
|
+
if (scopes.every((scope) => listSpaces(scope.dir).length === 0)) {
|
|
1147
|
+
io.stdout.write("No spaces yet. Create one with pi-space new <profile>/<name>.\n");
|
|
1148
|
+
return io.exit(0);
|
|
1149
|
+
}
|
|
1150
|
+
const failed = [];
|
|
1151
|
+
for (const scope of scopes) await setupEach(scope, listSpaces(scope.dir), failed);
|
|
1152
|
+
return finishSetup(failed, io);
|
|
1153
|
+
}
|
|
1154
|
+
const ref = qualify(target.name, call);
|
|
1155
|
+
if (!ref) return;
|
|
1156
|
+
const scope = scopeOf(call, ref.profile);
|
|
1157
|
+
if (!hasSetupCommand(scope.io.env)) {
|
|
1158
|
+
io.stderr.write(noSetup(ref.profile));
|
|
1013
1159
|
return io.exit(2);
|
|
1014
1160
|
}
|
|
1015
|
-
if (!checkSetup(io, setupSource)) return io.exit(2);
|
|
1016
|
-
if (
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1161
|
+
if (!checkSetup(scope.io, scope.setupSource)) return io.exit(2);
|
|
1162
|
+
if (ref.name === EVERY) {
|
|
1163
|
+
const names = listSpaces(scope.dir);
|
|
1164
|
+
if (names.length === 0) {
|
|
1165
|
+
io.stdout.write(`No spaces yet. Create one with pi-space new ${ref.profile}/<name>.\n`);
|
|
1166
|
+
return io.exit(0);
|
|
1020
1167
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
return
|
|
1024
|
-
}
|
|
1025
|
-
const names = listSpaces(dir);
|
|
1026
|
-
if (names.length === 0) {
|
|
1027
|
-
io.stdout.write("No spaces yet. Create one with pi-space new <name>.\n");
|
|
1028
|
-
return io.exit(0);
|
|
1168
|
+
const failed = [];
|
|
1169
|
+
await setupEach(scope, names, failed);
|
|
1170
|
+
return finishSetup(failed, io);
|
|
1029
1171
|
}
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
io.
|
|
1033
|
-
const home = join(dir, name);
|
|
1034
|
-
if ((await spawnSetup(setupFor(name, home, io), name, home, io, profile)) !== 0) failed.push(name);
|
|
1172
|
+
if (ref.name === BASE) {
|
|
1173
|
+
io.stderr.write("pi-space: setup runs only in spaces, not in base (the home Pi starts with).\n");
|
|
1174
|
+
return io.exit(1);
|
|
1035
1175
|
}
|
|
1176
|
+
if (!checkSpace(ref.name, scope.dir, io)) return io.exit(1);
|
|
1177
|
+
const home = join(scope.dir, ref.name);
|
|
1178
|
+
return io.exit(await spawnSetup(setupFor(ref.name, home, scope.io), ref.name, home, scope.io, scope.profile));
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function finishSetup(failed , io ) {
|
|
1036
1182
|
if (failed.length > 0) {
|
|
1037
1183
|
io.stderr.write(`pi-space: setup failed in: ${failed.join(", ")}\n`);
|
|
1038
1184
|
return io.exit(1);
|
|
@@ -1040,71 +1186,47 @@ async function runSetup(target , dir , io , setupSour
|
|
|
1040
1186
|
io.exit(0);
|
|
1041
1187
|
}
|
|
1042
1188
|
|
|
1043
|
-
/** A picker item
|
|
1189
|
+
/** A picker item: `<profile>/<name>` shown, and the profile and space it starts. */
|
|
1044
1190
|
|
|
1045
|
-
|
|
1191
|
+
|
|
1192
|
+
|
|
1046
1193
|
|
|
1047
1194
|
|
|
1048
1195
|
/**
|
|
1049
|
-
* The choices of the picker: base and the spaces of
|
|
1050
|
-
*
|
|
1051
|
-
* folder is
|
|
1196
|
+
* The choices of the picker: base and the spaces of every profile, as `<profile>/<name>`, in the order of
|
|
1197
|
+
* profileNames; under the profile's name as a header when there is more than one profile. A profile that shares a
|
|
1198
|
+
* folder with another is listed too (each runs its own command). The space this Pi runs in (PI_SPACE_HOME) is the active one.
|
|
1052
1199
|
*/
|
|
1053
|
-
function pickerChoices(call
|
|
1054
|
-
const
|
|
1055
|
-
const
|
|
1056
|
-
|
|
1057
|
-
[BASE, ...listSpaces(
|
|
1058
|
-
const home =
|
|
1200
|
+
function pickerChoices(call ) {
|
|
1201
|
+
const activeHome = call.io.env.PI_SPACE_HOME;
|
|
1202
|
+
const scopes = allScopes(call);
|
|
1203
|
+
return scopes.flatMap(({ profile, dir }) =>
|
|
1204
|
+
[BASE, ...listSpaces(dir)].map((space) => {
|
|
1205
|
+
const home = space === BASE ? profile.home : join(dir, space);
|
|
1059
1206
|
return {
|
|
1060
|
-
name
|
|
1061
|
-
...(
|
|
1062
|
-
|
|
1207
|
+
name: `${profile.name}/${space}`,
|
|
1208
|
+
...(scopes.length > 1 ? { group: profile.name } : {}),
|
|
1209
|
+
profile: profile.name,
|
|
1210
|
+
space,
|
|
1063
1211
|
...(activeHome && samePath(home, activeHome) ? { active: true } : {}),
|
|
1064
1212
|
};
|
|
1065
|
-
})
|
|
1066
|
-
|
|
1067
|
-
const others = config !== undefined && call.profile === undefined && call.dir === undefined && call.command === undefined
|
|
1068
|
-
? profileNames(config)
|
|
1069
|
-
.map((name) => profileByName(config, name, callerIo.env, callerIo.home))
|
|
1070
|
-
.filter((other, index, all) => !samePath(other.dir, dir) && all.findIndex((first) => samePath(first.dir, other.dir)) === index)
|
|
1071
|
-
: [];
|
|
1072
|
-
if (others.length === 0) return group(undefined, undefined, dir, baseHome);
|
|
1073
|
-
// The current one keeps the settings of this call; its header is its profile's name.
|
|
1074
|
-
const current = group(profile?.name ?? "current", undefined, dir, baseHome);
|
|
1075
|
-
return [...current, ...others.flatMap((other) => group(other.name, other.name, other.dir, other.home))];
|
|
1213
|
+
}),
|
|
1214
|
+
);
|
|
1076
1215
|
}
|
|
1077
1216
|
|
|
1078
1217
|
/**
|
|
1079
|
-
* A bare `pi-space` on a terminal: the picker, then Pi in the chosen space
|
|
1080
|
-
*
|
|
1218
|
+
* A bare `pi-space` on a terminal: the picker over every profile, then Pi in the chosen space with that profile's
|
|
1219
|
+
* settings (and the --setup and --config of this call). Esc or the end of input exit 0; Ctrl-C exits 130.
|
|
1081
1220
|
*/
|
|
1082
|
-
async function pick(call
|
|
1083
|
-
const {
|
|
1084
|
-
const io = settings.io;
|
|
1221
|
+
async function pick(call ) {
|
|
1222
|
+
const { io } = call;
|
|
1085
1223
|
if (!io.stdin.isTTY) {
|
|
1086
1224
|
io.stderr.write(USAGE);
|
|
1087
1225
|
return io.exit(2);
|
|
1088
1226
|
}
|
|
1089
|
-
|
|
1090
|
-
try {
|
|
1091
|
-
splitCommand(io.env.PI_SPACES_COMMAND ?? "");
|
|
1092
|
-
} catch (error) {
|
|
1093
|
-
io.stderr.write(`pi-space: ${commandSource}: ${(error ).message}.\n`);
|
|
1094
|
-
return io.exit(2);
|
|
1095
|
-
}
|
|
1096
|
-
const { item, interrupted } = await runPicker(pickerChoices(call, settings, callerIo), io);
|
|
1227
|
+
const { item, interrupted } = await runPicker(pickerChoices(call), io);
|
|
1097
1228
|
if (item === undefined) return io.exit(interrupted ? 130 : 0);
|
|
1098
1229
|
const choice = item ;
|
|
1099
|
-
if (choice.profile === undefined) return launch(choice.name, [], dir, io, commandSource);
|
|
1100
|
-
let chosen ;
|
|
1101
|
-
try {
|
|
1102
|
-
chosen = resolveSettings({ ...(call.config !== undefined ? { config: call.config } : {}), ...(call.setup !== undefined ? { setup: call.setup } : {}), profile: choice.profile }, callerIo);
|
|
1103
|
-
} catch (error) {
|
|
1104
|
-
if (!(error instanceof ConfigError)) throw error;
|
|
1105
|
-
io.stderr.write(`pi-space: ${error.message}\n`);
|
|
1106
|
-
return io.exit(2);
|
|
1107
|
-
}
|
|
1108
1230
|
// Its command was not checked before the picker: launch reports an unclosed quote.
|
|
1109
|
-
return launch(choice.
|
|
1231
|
+
return launch(choice.space, [], scopeOf(call, choice.profile));
|
|
1110
1232
|
}
|