@jals1212/pi-spaces-cli 0.7.1 → 0.8.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 +69 -0
- package/README.md +116 -87
- package/dist/cli.js +441 -311
- 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,25 +747,27 @@ 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",
|
|
670
753
|
" done",
|
|
754
|
+
// The spaces come first, as an unsorted group in pi-space's own (alphabetical) order; with the `sort false` style
|
|
755
|
+
// below, zsh and fzf-tab show every group in the order it is added here.
|
|
756
|
+
" (( $#spaces )) && _describe -t spaces 'space' spaces -V pi-space-spaces && ret=0",
|
|
671
757
|
" (( $#cmds )) && _describe -t commands 'command' cmds && ret=0",
|
|
672
|
-
" (( $#spaces )) && _describe -t spaces 'space' spaces && ret=0",
|
|
673
758
|
" (( $#targets )) && _describe -t targets 'target' targets && ret=0",
|
|
674
|
-
" (( $#profiles )) && _describe -t profiles 'profile' profiles && ret=0",
|
|
675
759
|
" (( $#shells )) && _describe -t shells 'shell' shells && ret=0",
|
|
676
760
|
" (( $#opts )) && _describe -t options 'option' opts && ret=0",
|
|
677
761
|
" return ret",
|
|
678
762
|
"}",
|
|
679
763
|
"compdef _pi_space pi-space",
|
|
764
|
+
// pi-space already gives its candidates in order (spaces first, alphabetical); zsh and fzf-tab must not resort them.
|
|
765
|
+
"zstyle ':completion:*:pi-space:*' sort false",
|
|
680
766
|
"",
|
|
681
767
|
].join("\n");
|
|
682
768
|
|
|
683
769
|
/** The groups of a `__complete` line; `target` is --every, which stands in for a space name. */
|
|
684
|
-
|
|
770
|
+
|
|
685
771
|
|
|
686
772
|
/** One `__complete` line; the description never holds a tab or a newline. */
|
|
687
773
|
function candidate(group , value , text ) {
|
|
@@ -703,10 +789,11 @@ function complete(words , io ) {
|
|
|
703
789
|
return io.exit(0);
|
|
704
790
|
}
|
|
705
791
|
|
|
706
|
-
/** The commands with their summary; in first place `completion` is left out (it still works typed in full), after --help it is not. */
|
|
792
|
+
/** The commands with their summary, in alphabetical order; in first place `completion` is left out (it still works typed in full), after --help it is not. */
|
|
707
793
|
function commandCandidates(withCompletion ) {
|
|
708
794
|
return Object.entries(COMMANDS)
|
|
709
795
|
.filter(([name]) => withCompletion || name !== "completion")
|
|
796
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
710
797
|
.map(([name, help]) => candidate("command", name, help.summary));
|
|
711
798
|
}
|
|
712
799
|
|
|
@@ -719,26 +806,44 @@ function firstOptions(call , alone ) {
|
|
|
719
806
|
return [...calls, ...general];
|
|
720
807
|
}
|
|
721
808
|
|
|
722
|
-
/**
|
|
723
|
-
|
|
724
|
-
|
|
809
|
+
/** A profile and the names in its spaces folder, as `__complete` offers them. */
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
/** `<profile>/base`, if asked, then `<profile>/<name>`, for each profile in order. */
|
|
818
|
+
function spaceCandidates(profiles , withBase ) {
|
|
819
|
+
const spaces = profiles.flatMap((profile) => [
|
|
820
|
+
...(withBase ? [{ value: `${profile.name}/${BASE}`, text: `base home of ${profile.name} (${profile.command})` }] : []),
|
|
821
|
+
...profile.spaces.map((name) => ({ value: `${profile.name}/${name}`, text: `space of ${profile.name} (${profile.command})` })),
|
|
822
|
+
]);
|
|
823
|
+
// Alphabetical by the whole name, whatever the order of the profiles in the file.
|
|
824
|
+
spaces.sort((a, b) => (a.value < b.value ? -1 : a.value > b.value ? 1 : 0));
|
|
825
|
+
return spaces.map(({ value, text }) => candidate("space", value, text));
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/** The `--every` targets: one for every profile, then one `<profile>/--every` for each. */
|
|
829
|
+
function everyCandidates(profiles , every , each ) {
|
|
830
|
+
return [candidate("target", "--every", every), ...profiles.map((profile) => candidate("target", `${profile.name}/--every`, each(profile.name)))];
|
|
725
831
|
}
|
|
726
832
|
|
|
727
833
|
/** What follows a command word: its target, its own options and --help. */
|
|
728
|
-
function commandArguments(command ,
|
|
729
|
-
const every = (text ) => candidate("target", "--every", text);
|
|
834
|
+
function commandArguments(command , profiles ) {
|
|
730
835
|
const options = [...COMMANDS[command].options, HELP_OPTION].map((option) => candidate("option", option.flag, option.text));
|
|
731
836
|
switch (command) {
|
|
732
837
|
case "remove":
|
|
733
838
|
case "rename":
|
|
734
|
-
return [...spaceCandidates(
|
|
839
|
+
return [...spaceCandidates(profiles, false), ...options];
|
|
735
840
|
case "setup":
|
|
736
|
-
return [...spaceCandidates(
|
|
841
|
+
return [...spaceCandidates(profiles, false), ...everyCandidates(profiles, "every space of every profile with a setup command", (name) => `every space of ${name}`), ...options];
|
|
737
842
|
case "list":
|
|
738
843
|
case "update":
|
|
739
844
|
case "install":
|
|
740
845
|
case "uninstall":
|
|
741
|
-
return [...spaceCandidates(
|
|
846
|
+
return [...spaceCandidates(profiles, true), ...everyCandidates(profiles, "every profile, base first", (name) => `base and every space of ${name}`), ...options];
|
|
742
847
|
case "completion":
|
|
743
848
|
return [...SHELLS.map((shell) => candidate("shell", shell, `the ${shell} completion script`)), ...options];
|
|
744
849
|
default:
|
|
@@ -752,37 +857,29 @@ function candidates(words , io ) {
|
|
|
752
857
|
while (Object.hasOwn(CALL_OPTIONS, words[index] ?? "")) {
|
|
753
858
|
const option = words[index] ;
|
|
754
859
|
// The option is the last word: the cursor is on its value.
|
|
755
|
-
if (index + 1 >= words.length) return optionValues(option
|
|
860
|
+
if (index + 1 >= words.length) return optionValues(option);
|
|
756
861
|
call[CALL_OPTIONS[option]] = words[index + 1];
|
|
757
862
|
index += 2;
|
|
758
863
|
}
|
|
759
|
-
const
|
|
864
|
+
const configFile = call.config !== undefined ? resolve(call.config) : configPath(io.env, io.home);
|
|
865
|
+
const config = loadConfig(configFile);
|
|
866
|
+
// A --dir typed before the cursor is read as the spaces folder of every profile.
|
|
867
|
+
const dirOverride = call.dir === undefined ? undefined : resolve(call.dir);
|
|
868
|
+
const profiles = profileNames(config).map((name) => {
|
|
869
|
+
const profile = profileByName(config, name, io.env, io.home);
|
|
870
|
+
return { name, command: profile.command ?? "pi", spaces: listSpaces(dirOverride ?? profile.dir) };
|
|
871
|
+
});
|
|
760
872
|
const rest = words.slice(index);
|
|
761
|
-
|
|
762
|
-
if (rest.length === 0) return [...commandCandidates(false), ...spaceCandidates(spaces, true), ...firstOptions(call, index === 0)];
|
|
873
|
+
if (rest.length === 0) return [...spaceCandidates(profiles, true), ...commandCandidates(false), ...firstOptions(call, index === 0)];
|
|
763
874
|
if (rest.length > 1) return [];
|
|
764
875
|
if (isHelpFlag(rest[0])) return commandCandidates(true);
|
|
765
876
|
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 [];
|
|
877
|
+
return commandArguments(rest[0], profiles);
|
|
777
878
|
}
|
|
778
879
|
|
|
779
|
-
/**
|
|
780
|
-
function
|
|
781
|
-
|
|
782
|
-
return profileByName(config, name, io.env, io.home).command || "profile";
|
|
783
|
-
} catch {
|
|
784
|
-
return "profile";
|
|
785
|
-
}
|
|
880
|
+
/** The candidates for the value of a call option: the shell's files for a path, nothing for a command. */
|
|
881
|
+
function optionValues(option ) {
|
|
882
|
+
return option === "--config" || option === "--dir" ? [FILES] : [];
|
|
786
883
|
}
|
|
787
884
|
|
|
788
885
|
/** Prints the reason and returns false when the setup command has an unclosed quote. */
|
|
@@ -797,34 +894,35 @@ function checkSetup(io , source ) {
|
|
|
797
894
|
}
|
|
798
895
|
|
|
799
896
|
/** 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
|
|
897
|
+
function spawnSetup(setup , name , home , io , profile ) {
|
|
801
898
|
// 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 });
|
|
899
|
+
const spaceEnv = spaceEnvFor(profile, home, name);
|
|
900
|
+
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
901
|
}
|
|
805
902
|
|
|
806
|
-
/** The setup command for one space, from PI_SPACES_SETUP
|
|
903
|
+
/** The setup command for one space, from PI_SPACES_SETUP of this profile's io; checkSetup has accepted it. */
|
|
807
904
|
function setupFor(name , home , io ) {
|
|
808
905
|
return setupCommand(io.env.PI_SPACES_SETUP ?? "", home, name);
|
|
809
906
|
}
|
|
810
907
|
|
|
811
|
-
async function create(name ,
|
|
908
|
+
async function create(name , scope ) {
|
|
909
|
+
const { io, dir, profile } = scope;
|
|
812
910
|
const withSetup = hasSetupCommand(io.env);
|
|
813
911
|
// A bad setup command is reported before anything is created.
|
|
814
|
-
if (withSetup && !checkSetup(io, setupSource)) return io.exit(2);
|
|
912
|
+
if (withSetup && !checkSetup(io, scope.setupSource)) return io.exit(2);
|
|
815
913
|
try {
|
|
816
|
-
const path = createSpace(dir, name, newSpaceDeclaration(io.env, io.home, profile
|
|
914
|
+
const path = createSpace(dir, name, newSpaceDeclaration(io.env, io.home, profile.home));
|
|
817
915
|
io.stdout.write(`Created space "${name}" at ${path}.\n`);
|
|
818
916
|
if (withSetup) {
|
|
819
917
|
const setup = setupFor(name, path, io);
|
|
820
918
|
io.stdout.write(`Setting it up: ${[setup.program, ...setup.args].join(" ")}\n`);
|
|
821
919
|
const status = await spawnSetup(setup, name, path, io, profile);
|
|
822
920
|
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`);
|
|
921
|
+
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
922
|
return io.exit(status);
|
|
825
923
|
}
|
|
826
924
|
}
|
|
827
|
-
io.stdout.write(`Start Pi in it with: pi-space ${name}\n`);
|
|
925
|
+
io.stdout.write(`Start Pi in it with: pi-space ${profile.name}/${name}\n`);
|
|
828
926
|
return io.exit(0);
|
|
829
927
|
} catch (error) {
|
|
830
928
|
// A SpaceError is a rule (a bad or taken name); anything else is the filesystem, such as no permission.
|
|
@@ -833,13 +931,14 @@ async function create(name , dir , io , setupSource ,
|
|
|
833
931
|
}
|
|
834
932
|
}
|
|
835
933
|
|
|
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
|
|
934
|
+
/** The base homes a space must not be: the one Pi started with, and the profile's. */
|
|
935
|
+
function protectedHomes(io , profile ) {
|
|
936
|
+
return [io.env.PI_SPACES_BASE_HOME, profile.home].filter((home) => Boolean(home));
|
|
839
937
|
}
|
|
840
938
|
|
|
841
939
|
/** 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 ,
|
|
940
|
+
function rename(from , to , scope ) {
|
|
941
|
+
const { io, dir, profile } = scope;
|
|
843
942
|
try {
|
|
844
943
|
const protect = protectedHomes(io, profile);
|
|
845
944
|
renameSpace(dir, from, to, agentHome(io.env, io.home), protect, io.home);
|
|
@@ -855,7 +954,8 @@ function rename(from , to , dir , io , profile
|
|
|
855
954
|
* Without --yes it asks to remove ([y/N]), then, without --no-backup, whether to keep a backup ([Y/n]).
|
|
856
955
|
* With --yes it asks nothing and keeps a backup unless --no-backup.
|
|
857
956
|
*/
|
|
858
|
-
async function remove(name , yes , backup ,
|
|
957
|
+
async function remove(name , yes , backup , scope ) {
|
|
958
|
+
const { io, dir, profile } = scope;
|
|
859
959
|
const current = agentHome(io.env, io.home);
|
|
860
960
|
const protect = protectedHomes(io, profile);
|
|
861
961
|
let reader ;
|
|
@@ -864,7 +964,7 @@ async function remove(name , yes , backup , dir ,
|
|
|
864
964
|
const path = checkRemovable(dir, name, current, protect, io.home);
|
|
865
965
|
let keep = backup;
|
|
866
966
|
if (!yes) {
|
|
867
|
-
if (!io.stdin.isTTY) throw new SpaceError(`Removing a space needs confirmation; run pi-space remove ${name} --yes.`);
|
|
967
|
+
if (!io.stdin.isTTY) throw new SpaceError(`Removing a space needs confirmation; run pi-space remove ${profile.name}/${name} --yes.`);
|
|
868
968
|
// One reader for every question, so an answer typed ahead is not lost between them.
|
|
869
969
|
reader = lineReader(io.stdin);
|
|
870
970
|
io.stdout.write(`Remove space "${name}"? This deletes ${path} and everything in it: sign-ins, packages, settings and sessions. [y/N] `);
|
|
@@ -961,40 +1061,63 @@ async function spawnPi(program , programArgs , io , env
|
|
|
961
1061
|
return result.status ?? 1;
|
|
962
1062
|
}
|
|
963
1063
|
|
|
964
|
-
async function launch(name , args ,
|
|
965
|
-
|
|
1064
|
+
async function launch(name , args , scope ) {
|
|
1065
|
+
const { io } = scope;
|
|
1066
|
+
if (!checkSpace(name, scope.dir, io)) return io.exit(1);
|
|
966
1067
|
let launched ;
|
|
967
1068
|
try {
|
|
968
1069
|
launched = launchCommand(io.env, name, args);
|
|
969
1070
|
} catch (error) {
|
|
970
|
-
io.stderr.write(`pi-space: ${commandSource}: ${(error ).message}.\n`);
|
|
1071
|
+
io.stderr.write(`pi-space: ${scope.commandSource}: ${(error ).message}.\n`);
|
|
971
1072
|
return io.exit(2);
|
|
972
1073
|
}
|
|
973
1074
|
io.exit(await spawnPi(launched.program, launched.args, io, io.childEnv));
|
|
974
1075
|
}
|
|
975
1076
|
|
|
976
1077
|
/**
|
|
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
|
|
1078
|
+
* Runs one of Pi's package commands in a space, in base and then every space of a profile (`<profile>/--every`),
|
|
1079
|
+
* or in every profile (`--every`), through the same Pi (`--space <name> <verb> [args...]`): the extension relaunches
|
|
1080
|
+
* it in that home, so the home is never set here. With --every each run gets a `== <profile>/<name>` header, one
|
|
1081
|
+
* failure does not stop the rest, and the status is 1 if any failed.
|
|
980
1082
|
*/
|
|
981
|
-
async function runPackages(verb , target , args ,
|
|
982
|
-
const
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1083
|
+
async function runPackages(verb , target , args , call ) {
|
|
1084
|
+
const { io } = call;
|
|
1085
|
+
let scopes ;
|
|
1086
|
+
if ("every" in target) {
|
|
1087
|
+
scopes = allScopes(call);
|
|
1088
|
+
} else {
|
|
1089
|
+
const ref = qualify(target.name, call);
|
|
1090
|
+
if (!ref) return;
|
|
1091
|
+
const scope = scopeOf(call, ref.profile);
|
|
1092
|
+
if (ref.name !== EVERY) {
|
|
1093
|
+
if (!checkSpace(ref.name, scope.dir, io)) return io.exit(1);
|
|
1094
|
+
let launched ;
|
|
1095
|
+
try {
|
|
1096
|
+
launched = launchCommand(scope.io.env, ref.name, [verb, ...args]);
|
|
1097
|
+
} catch (error) {
|
|
1098
|
+
io.stderr.write(`pi-space: ${scope.commandSource}: ${(error ).message}.\n`);
|
|
1099
|
+
return io.exit(2);
|
|
1100
|
+
}
|
|
1101
|
+
return io.exit(await spawnPi(launched.program, launched.args, scope.io, scope.io.childEnv));
|
|
1102
|
+
}
|
|
1103
|
+
scopes = [scope];
|
|
1104
|
+
}
|
|
1105
|
+
// Every command is checked before the first one runs, so a bad one is reported once, before any header.
|
|
1106
|
+
for (const scope of scopes) {
|
|
987
1107
|
try {
|
|
988
|
-
|
|
1108
|
+
launchCommand(scope.io.env, BASE, []);
|
|
989
1109
|
} catch (error) {
|
|
990
|
-
|
|
991
|
-
io.stderr.write(`pi-space: ${commandSource}: ${(error ).message}.\n`);
|
|
1110
|
+
io.stderr.write(`pi-space: ${scope.commandSource}: ${(error ).message}.\n`);
|
|
992
1111
|
return io.exit(2);
|
|
993
1112
|
}
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1113
|
+
}
|
|
1114
|
+
const failed = [];
|
|
1115
|
+
for (const scope of scopes) {
|
|
1116
|
+
for (const name of [BASE, ...listSpaces(scope.dir)]) {
|
|
1117
|
+
const launched = launchCommand(scope.io.env, name, [verb, ...args]);
|
|
1118
|
+
io.stdout.write(`== ${scope.profile.name}/${name}\n`);
|
|
1119
|
+
if ((await spawnPi(launched.program, launched.args, scope.io, scope.io.childEnv)) !== 0) failed.push(`${scope.profile.name}/${name}`);
|
|
1120
|
+
}
|
|
998
1121
|
}
|
|
999
1122
|
if (failed.length > 0) {
|
|
1000
1123
|
io.stderr.write(`pi-space: ${verb} failed in: ${failed.join(", ")}\n`);
|
|
@@ -1003,36 +1126,67 @@ async function runPackages(verb , target , args
|
|
|
1003
1126
|
io.exit(0);
|
|
1004
1127
|
}
|
|
1005
1128
|
|
|
1129
|
+
/** Runs the setup command of a profile in each of `names`, with a `== <profile>/<name>` header; the failures are added to `failed`. */
|
|
1130
|
+
async function setupEach(scope , names , failed ) {
|
|
1131
|
+
for (const name of names) {
|
|
1132
|
+
scope.io.stdout.write(`== ${scope.profile.name}/${name}\n`);
|
|
1133
|
+
const home = join(scope.dir, name);
|
|
1134
|
+
if ((await spawnSetup(setupFor(name, home, scope.io), name, home, scope.io, scope.profile)) !== 0) failed.push(`${scope.profile.name}/${name}`);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1006
1138
|
/**
|
|
1007
|
-
* Runs the setup command in a space,
|
|
1008
|
-
* With --every each run gets a `== <name>`
|
|
1139
|
+
* Runs the setup command in a space, in every space of a profile (`<profile>/--every`), or in every space of every
|
|
1140
|
+
* profile that has a setup command (`--every`); never in base. With --every each run gets a `== <profile>/<name>`
|
|
1141
|
+
* header, one failure does not stop the rest, and the status is 1 if any failed.
|
|
1009
1142
|
*/
|
|
1010
|
-
async function runSetup(target ,
|
|
1011
|
-
|
|
1012
|
-
|
|
1143
|
+
async function runSetup(target , call ) {
|
|
1144
|
+
const { io } = call;
|
|
1145
|
+
const noSetup = (name ) => `pi-space: Profile "${name}" has no setup command; give it a "setup" in the config file, or give --setup <cmd>.\n`;
|
|
1146
|
+
if ("every" in target) {
|
|
1147
|
+
const scopes = allScopes(call).filter((scope) => hasSetupCommand(scope.io.env));
|
|
1148
|
+
if (scopes.length === 0) {
|
|
1149
|
+
io.stderr.write('pi-space: No profile has a setup command; give one a "setup" in the config file, or give --setup <cmd>.\n');
|
|
1150
|
+
return io.exit(2);
|
|
1151
|
+
}
|
|
1152
|
+
// Every setup command is checked before the first one runs.
|
|
1153
|
+
if (!scopes.every((scope) => checkSetup(scope.io, scope.setupSource))) return io.exit(2);
|
|
1154
|
+
if (scopes.every((scope) => listSpaces(scope.dir).length === 0)) {
|
|
1155
|
+
io.stdout.write("No spaces yet. Create one with pi-space new <profile>/<name>.\n");
|
|
1156
|
+
return io.exit(0);
|
|
1157
|
+
}
|
|
1158
|
+
const failed = [];
|
|
1159
|
+
for (const scope of scopes) await setupEach(scope, listSpaces(scope.dir), failed);
|
|
1160
|
+
return finishSetup(failed, io);
|
|
1161
|
+
}
|
|
1162
|
+
const ref = qualify(target.name, call);
|
|
1163
|
+
if (!ref) return;
|
|
1164
|
+
const scope = scopeOf(call, ref.profile);
|
|
1165
|
+
if (!hasSetupCommand(scope.io.env)) {
|
|
1166
|
+
io.stderr.write(noSetup(ref.profile));
|
|
1013
1167
|
return io.exit(2);
|
|
1014
1168
|
}
|
|
1015
|
-
if (!checkSetup(io, setupSource)) return io.exit(2);
|
|
1016
|
-
if (
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1169
|
+
if (!checkSetup(scope.io, scope.setupSource)) return io.exit(2);
|
|
1170
|
+
if (ref.name === EVERY) {
|
|
1171
|
+
const names = listSpaces(scope.dir);
|
|
1172
|
+
if (names.length === 0) {
|
|
1173
|
+
io.stdout.write(`No spaces yet. Create one with pi-space new ${ref.profile}/<name>.\n`);
|
|
1174
|
+
return io.exit(0);
|
|
1020
1175
|
}
|
|
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);
|
|
1176
|
+
const failed = [];
|
|
1177
|
+
await setupEach(scope, names, failed);
|
|
1178
|
+
return finishSetup(failed, io);
|
|
1029
1179
|
}
|
|
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);
|
|
1180
|
+
if (ref.name === BASE) {
|
|
1181
|
+
io.stderr.write("pi-space: setup runs only in spaces, not in base (the home Pi starts with).\n");
|
|
1182
|
+
return io.exit(1);
|
|
1035
1183
|
}
|
|
1184
|
+
if (!checkSpace(ref.name, scope.dir, io)) return io.exit(1);
|
|
1185
|
+
const home = join(scope.dir, ref.name);
|
|
1186
|
+
return io.exit(await spawnSetup(setupFor(ref.name, home, scope.io), ref.name, home, scope.io, scope.profile));
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function finishSetup(failed , io ) {
|
|
1036
1190
|
if (failed.length > 0) {
|
|
1037
1191
|
io.stderr.write(`pi-space: setup failed in: ${failed.join(", ")}\n`);
|
|
1038
1192
|
return io.exit(1);
|
|
@@ -1040,71 +1194,47 @@ async function runSetup(target , dir , io , setupSour
|
|
|
1040
1194
|
io.exit(0);
|
|
1041
1195
|
}
|
|
1042
1196
|
|
|
1043
|
-
/** A picker item
|
|
1197
|
+
/** A picker item: `<profile>/<name>` shown, and the profile and space it starts. */
|
|
1044
1198
|
|
|
1045
|
-
|
|
1199
|
+
|
|
1200
|
+
|
|
1046
1201
|
|
|
1047
1202
|
|
|
1048
1203
|
/**
|
|
1049
|
-
* The choices of the picker: base and the spaces of
|
|
1050
|
-
*
|
|
1051
|
-
* folder is
|
|
1204
|
+
* The choices of the picker: base and the spaces of every profile, as `<profile>/<name>`, in the order of
|
|
1205
|
+
* profileNames; under the profile's name as a header when there is more than one profile. A profile that shares a
|
|
1206
|
+
* 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
1207
|
*/
|
|
1053
|
-
function pickerChoices(call
|
|
1054
|
-
const
|
|
1055
|
-
const
|
|
1056
|
-
|
|
1057
|
-
[BASE, ...listSpaces(
|
|
1058
|
-
const home =
|
|
1208
|
+
function pickerChoices(call ) {
|
|
1209
|
+
const activeHome = call.io.env.PI_SPACE_HOME;
|
|
1210
|
+
const scopes = allScopes(call);
|
|
1211
|
+
return scopes.flatMap(({ profile, dir }) =>
|
|
1212
|
+
[BASE, ...listSpaces(dir)].map((space) => {
|
|
1213
|
+
const home = space === BASE ? profile.home : join(dir, space);
|
|
1059
1214
|
return {
|
|
1060
|
-
name
|
|
1061
|
-
...(
|
|
1062
|
-
|
|
1215
|
+
name: `${profile.name}/${space}`,
|
|
1216
|
+
...(scopes.length > 1 ? { group: profile.name } : {}),
|
|
1217
|
+
profile: profile.name,
|
|
1218
|
+
space,
|
|
1063
1219
|
...(activeHome && samePath(home, activeHome) ? { active: true } : {}),
|
|
1064
1220
|
};
|
|
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))];
|
|
1221
|
+
}),
|
|
1222
|
+
);
|
|
1076
1223
|
}
|
|
1077
1224
|
|
|
1078
1225
|
/**
|
|
1079
|
-
* A bare `pi-space` on a terminal: the picker, then Pi in the chosen space
|
|
1080
|
-
*
|
|
1226
|
+
* A bare `pi-space` on a terminal: the picker over every profile, then Pi in the chosen space with that profile's
|
|
1227
|
+
* settings (and the --setup and --config of this call). Esc or the end of input exit 0; Ctrl-C exits 130.
|
|
1081
1228
|
*/
|
|
1082
|
-
async function pick(call
|
|
1083
|
-
const {
|
|
1084
|
-
const io = settings.io;
|
|
1229
|
+
async function pick(call ) {
|
|
1230
|
+
const { io } = call;
|
|
1085
1231
|
if (!io.stdin.isTTY) {
|
|
1086
1232
|
io.stderr.write(USAGE);
|
|
1087
1233
|
return io.exit(2);
|
|
1088
1234
|
}
|
|
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);
|
|
1235
|
+
const { item, interrupted } = await runPicker(pickerChoices(call), io);
|
|
1097
1236
|
if (item === undefined) return io.exit(interrupted ? 130 : 0);
|
|
1098
1237
|
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
1238
|
// Its command was not checked before the picker: launch reports an unclosed quote.
|
|
1109
|
-
return launch(choice.
|
|
1239
|
+
return launch(choice.space, [], scopeOf(call, choice.profile));
|
|
1110
1240
|
}
|