@jals1212/pi-spaces 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,23 @@ commands and options and a patch version fixes.
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 0.4.0
10
+
11
+ ### Added
12
+
13
+ - `pi --space <name> <command> [args...]` runs one of Pi's package commands
14
+ (`install`, `remove`, `uninstall`, `update`, `list`) in a space, or in the
15
+ base home with `base`, through the same Pi: `pi --space work update
16
+ --extensions`. Options before `--space`, such as a launcher's own `-e`, are
17
+ left out, since Pi runs a package command only as its first argument.
18
+
19
+ ### Changed
20
+
21
+ - The command words `list`, `new`, `remove`, `rename`, `switch`, `update`,
22
+ `install` and `uninstall` are reserved and can no longer name a space, so
23
+ `pi-space <name>` never reads as a command. A folder with such a name in the
24
+ spaces folder is no longer listed as a space.
25
+
9
26
  ## 0.3.0
10
27
 
11
28
  ### Added
package/README.md CHANGED
@@ -45,8 +45,9 @@ From your shell:
45
45
  ```sh
46
46
  pi --space work # start Pi in "work"
47
47
  pi --space work "review this branch" # with a prompt
48
- pi-space --new work # create a space (optional CLI)
49
- pi-space --remove work # delete it, after asking; keeps a backup unless told not to (optional CLI)
48
+ pi-space new work # create a space (optional CLI)
49
+ pi-space remove work # delete it, after asking; keeps a backup unless told not to (optional CLI)
50
+ pi-space update --every # update the packages of base and of every space (optional CLI)
50
51
  pi-space # pick from a numbered list (optional CLI)
51
52
  ```
52
53
 
@@ -64,16 +65,17 @@ A new space starts empty except for a `settings.json` that declares this package
64
65
  | `/space rename <old> <new>` | Renames a space. The active space cannot be renamed. |
65
66
  | `/space remove <name> [--yes] [--no-backup]` | Deletes a space after asking, then asks whether to keep a backup in `<spaces folder>/.backups` (keeping one is first in the list; dismissing the question keeps the space). `--yes` skips the questions and keeps a backup; it is required without a dialog (print and JSON modes). `--no-backup` deletes without one. |
66
67
  | `pi --space <name>` | Starts Pi directly in a space, keeping the prompt and every other option. |
68
+ | `pi --space <name> <command> [args...]` | Runs one of Pi's package commands (`install`, `remove`, `uninstall`, `update`, `list`) in a space, or in the base home with `base`: `pi --space work update --extensions`, `pi --space work install npm:<package>`. |
67
69
 
68
70
  The status line shows `◆ <name>` while a space is active, and nothing in the base home.
69
71
 
70
72
  Choosing an argument such as `switch` from the `/space ` suggestions only fills the line; press Enter again to run it. `/space` and Enter opens the picker in one step.
71
73
 
72
- **Names**: lowercase letters, digits, `-` and `_`, starting with a letter or digit, at most 64 characters. `base` is reserved.
74
+ **Names**: lowercase letters, digits, `-` and `_`, starting with a letter or digit, at most 64 characters. `base` is reserved, and so are the command words `list`, `new`, `remove`, `rename`, `switch`, `update`, `install` and `uninstall`.
73
75
 
74
76
  ## The `pi-space` command
75
77
 
76
- The optional `@jals1212/pi-spaces-cli` package adds a `pi-space` command to your shell: `pi-space` picks a space from a numbered list and starts Pi in it, `pi-space <name> [pi args...]` starts Pi in a named space, `pi-space --new <name>` creates one, `pi-space --remove <name>` deletes one, and `pi-space list` lists them. `--command` and `--dir` (or `PI_SPACES_COMMAND` and `PI_SPACES_DIR`) make it run another program that runs Pi, with that program's spaces. It only runs `pi --space <name>`, so this extension must be installed in the home it opens. See [`cli/README.md`](cli/README.md) for the commands, the options and the exit status.
78
+ The optional `@jals1212/pi-spaces-cli` package adds a `pi-space` command to your shell: `pi-space` picks a space from a numbered list and starts Pi in it, `pi-space <name> [pi args...]` starts Pi in a named space, `pi-space new <name>` creates one, `pi-space rename <old> <new>` renames one, `pi-space remove <name>` deletes one, and `pi-space list` lists them. `pi-space list|update|install|uninstall <name|--every> ...` runs Pi's package commands in a space, or in every space. `--command` and `--dir` (or `PI_SPACES_COMMAND` and `PI_SPACES_DIR`) make it run another program that runs Pi, with that program's spaces. It only runs `pi --space <name> [command]`, so this extension must be installed in the home it opens. See [`cli/README.md`](cli/README.md) for the commands, the options and the exit status.
77
79
 
78
80
  ## Configuration reference
79
81
 
@@ -135,13 +137,15 @@ Pi reads its agent home once, when it starts, so a running session cannot move t
135
137
 
136
138
  `pi --space <name>` acts before Pi opens its interface or runs anything, so it keeps the prompt and the session options too, in every mode.
137
139
 
140
+ `pi --space <name> <command> ...`, where `<command>` is one of Pi's package commands right after the name, runs Pi again in that home with only `<command> ...`: Pi runs a package command only as its first argument. Options before `--space`, such as a launcher's own `-e`, are left out. The same Pi runs it, so a launcher's own Pi manages the space's packages; `base` and the current home work too.
141
+
138
142
  ## What it leaves on your machine
139
143
 
140
144
  Only the spaces you create, each a folder in the spaces folder, and its `settings.json`. Pi fills a space as it would any home. Removing a space moves its folder to `.backups/<name>-<UTC time>` in the spaces folder, or deletes it with `--no-backup` or when you choose so; nothing else is written outside it. A backup is a whole space: move it back to restore it (`mv ~/.pi/spaces/.backups/work-2026-10-28T14-30-05Z ~/.pi/spaces/work`), or delete it when you no longer need it. Backups are never removed for you, and `.backups` is never listed as a space.
141
145
 
142
146
  ## Safety
143
147
 
144
- `/space remove`, `/space rename` and `pi-space --remove` refuse, before they ask:
148
+ `/space remove`, `/space rename`, `pi-space remove` and `pi-space rename` refuse, before they ask:
145
149
 
146
150
  - `base` and the active space;
147
151
  - symlinks, and anything that is not a real folder directly inside the spaces folder;
package/dist/relaunch.js CHANGED
@@ -123,10 +123,14 @@ export function relaunchCommand(proc = process, args = filterArg
123
123
 
124
124
  const SPACE_FLAG_ERROR = "--space needs a space name: pi --space <name>.";
125
125
 
126
- /** Finds --space <name> or --space=<name> before "--", and returns the other arguments unchanged. */
127
- export function parseSpaceFlag(args ) {
126
+ /**
127
+ * Finds --space <name> or --space=<name> before "--", and returns the other arguments unchanged,
128
+ * and the arguments that follow the name.
129
+ */
130
+ export function parseSpaceFlag(args ) {
128
131
  const rest = [];
129
132
  let name ;
133
+ let afterName = [];
130
134
  for (let i = 0; i < args.length; i++) {
131
135
  const arg = args[i];
132
136
  if (arg === "--") {
@@ -138,36 +142,48 @@ export function parseSpaceFlag(args )
138
142
  if (value === undefined || value === "" || value.startsWith("-")) return { error: SPACE_FLAG_ERROR };
139
143
  name = value;
140
144
  i++;
145
+ afterName = args.slice(i + 1);
141
146
  } else if (arg.startsWith("--space=")) {
142
147
  name = arg.slice("--space=".length);
143
148
  if (name === "") return { error: SPACE_FLAG_ERROR };
149
+ afterName = args.slice(i + 1);
144
150
  } else {
145
151
  rest.push(arg);
146
152
  }
147
153
  }
148
- return { name, rest };
154
+ return { name, rest, afterName };
149
155
  }
150
156
 
157
+ // Pi runs these only as its first argument, before extensions load.
158
+ const PACKAGE_COMMANDS = new Set(["install", "remove", "uninstall", "update", "list"]);
159
+
151
160
 
152
161
 
153
162
  /**
154
163
  * What `pi --space <name>` does at startup: nothing without the flag or when Pi already runs in that
155
164
  * space; otherwise run Pi again in the space with every other argument, since nothing has run yet.
165
+ * `pi --space <name> <package command> ...` always runs Pi again, even in the current home, because
166
+ * Pi runs a package command (install, remove, uninstall, update, list) only as its first argument.
156
167
  */
157
168
  export function planStartup(input ) {
158
169
  const { proc, dir, currentHome, env } = input;
159
170
  const parsed = parseSpaceFlag(proc.argv.slice(2));
160
171
  if ("error" in parsed) return { kind: "error", message: parsed.error };
161
172
  if (parsed.name === undefined) return { kind: "none" };
173
+ // A package command runs alone: options before --space (a launcher may add its own) are for a session.
174
+ const packageCommand = PACKAGE_COMMANDS.has(parsed.afterName[0] ?? "");
175
+ const args = packageCommand ? parsed.afterName : parsed.rest;
162
176
  let target ;
163
177
  try {
164
178
  target = resolveSwitch({ dir, name: parsed.name, currentHome, env });
165
179
  } catch (error) {
166
180
  if (!(error instanceof SpaceError)) throw error;
167
- if (error.message.startsWith("Already in")) return { kind: "none" };
181
+ if (error.message.startsWith("Already in")) {
182
+ return packageCommand ? { kind: "relaunch", command: relaunchCommand(proc, args), env } : { kind: "none" };
183
+ }
168
184
  return { kind: "error", message: error.message };
169
185
  }
170
- return { kind: "relaunch", command: relaunchCommand(proc, parsed.rest), env: relaunchEnv(env, { ...target, currentHome }) };
186
+ return { kind: "relaunch", command: relaunchCommand(proc, args), env: relaunchEnv(env, { ...target, currentHome }) };
171
187
  }
172
188
 
173
189
  /** Runs the command now with inherited stdio and returns its exit code (1 when it cannot start). */
package/dist/spaces.js CHANGED
@@ -37,9 +37,13 @@ export function spacesDir(env = process.env, home = osHomedir())
37
37
  return join(dirname(resolve(baseHome)), "spaces");
38
38
  }
39
39
 
40
+ // The words of the pi-space and /space commands, so a space name never reads as one.
41
+ const COMMANDS = new Set(["list", "new", "remove", "rename", "switch", "update", "install", "uninstall"]);
42
+
40
43
  /** Returns the error message for an unusable name, or undefined when the name is valid. */
41
44
  export function validateName(name ) {
42
45
  if (name === BASE) return `"${BASE}" is reserved for the home Pi started with.`;
46
+ if (COMMANDS.has(name)) return `"${name}" is reserved: it is a pi-space or /space command.`;
43
47
  if (!NAME.test(name)) {
44
48
  return `Invalid space name "${name}": use lowercase letters, digits, "-" and "_", starting with a letter or digit (at most 64 characters).`;
45
49
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jals1212/pi-spaces",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Named spaces for Pi: keep several agent homes (sign-ins, packages, settings) and pick one by name.",
5
5
  "license": "MIT",
6
6
  "author": "jals1212",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: pi-spaces
3
- description: "Trigger: pi-spaces, space, spaces, /space, pi-space, pi --space, PI_SPACE, PI_SPACE_HOME, PI_SPACES_COMMAND, which space, agent home, base home. Know which Pi space this session runs in, its home and start command, and how spaces are listed, created, switched and removed."
3
+ description: "Trigger: pi-spaces, space, spaces, /space, pi-space, pi --space, PI_SPACE, PI_SPACE_HOME, PI_SPACES_COMMAND, which space, agent home, base home, update, install, uninstall. Know which Pi space this session runs in, its home and start command, and how spaces and their packages are listed, created, updated, switched and removed."
4
4
  license: MIT
5
5
  metadata:
6
6
  author: jals1212
@@ -27,11 +27,12 @@ Use when a task depends on which Pi agent home this session uses (settings, sign
27
27
  | Which space, home, command | `<pi_space>` section, or `echo "$PI_SPACE|$PI_SPACE_HOME|$PI_SPACES_COMMAND"` |
28
28
  | List spaces from a shell | `pi-space list` (optional `@jals1212/pi-spaces-cli`) |
29
29
  | Spaces folder | `PI_SPACES_DIR`, else `spaces` next to the base home (`PI_SPACES_BASE_HOME`, else this home) |
30
- | Create a space | `pi-space --new <name>`, or ask the user for `/space new <name>` |
30
+ | Create a space | `pi-space new <name>`, or ask the user for `/space new <name>` |
31
31
  | Work in another space | User runs `/space switch <name>`, or `$PI_SPACES_COMMAND --space <name> "<prompt>"` |
32
- | Remove a space | User request only: `pi-space --remove <name> --yes` keeps a backup in `.backups` |
32
+ | Remove a space | User request only: `pi-space remove <name> --yes` keeps a backup in `.backups` |
33
+ | Packages of a space | `pi-space update <name|base|--every> --extensions`, `pi-space install <name> <source>`, `pi-space uninstall <name> <source>`, `pi-space list <name>`; without the CLI, `$PI_SPACES_COMMAND --space <name|base> update --extensions` |
33
34
 
34
- Names: lowercase letters, digits, `-` and `_`, starting with a letter or digit, at most 64 characters; `base` is reserved for the base home.
35
+ Names: lowercase letters, digits, `-` and `_`, starting with a letter or digit, at most 64 characters; `base` (the base home) and the command words (`list`, `new`, `remove`, `rename`, `switch`, `update`, `install`, `uninstall`) are reserved.
35
36
 
36
37
  ## Execution Steps
37
38
 
package/src/relaunch.ts CHANGED
@@ -123,10 +123,14 @@ export function relaunchCommand(proc: Proc = process, args: string[] = filterArg
123
123
 
124
124
  const SPACE_FLAG_ERROR = "--space needs a space name: pi --space <name>.";
125
125
 
126
- /** Finds --space <name> or --space=<name> before "--", and returns the other arguments unchanged. */
127
- export function parseSpaceFlag(args: string[]): { name: string | undefined; rest: string[] } | { error: string } {
126
+ /**
127
+ * Finds --space <name> or --space=<name> before "--", and returns the other arguments unchanged,
128
+ * and the arguments that follow the name.
129
+ */
130
+ export function parseSpaceFlag(args: string[]): { name: string | undefined; rest: string[]; afterName: string[] } | { error: string } {
128
131
  const rest: string[] = [];
129
132
  let name: string | undefined;
133
+ let afterName: string[] = [];
130
134
  for (let i = 0; i < args.length; i++) {
131
135
  const arg = args[i];
132
136
  if (arg === "--") {
@@ -138,36 +142,48 @@ export function parseSpaceFlag(args: string[]): { name: string | undefined; rest
138
142
  if (value === undefined || value === "" || value.startsWith("-")) return { error: SPACE_FLAG_ERROR };
139
143
  name = value;
140
144
  i++;
145
+ afterName = args.slice(i + 1);
141
146
  } else if (arg.startsWith("--space=")) {
142
147
  name = arg.slice("--space=".length);
143
148
  if (name === "") return { error: SPACE_FLAG_ERROR };
149
+ afterName = args.slice(i + 1);
144
150
  } else {
145
151
  rest.push(arg);
146
152
  }
147
153
  }
148
- return { name, rest };
154
+ return { name, rest, afterName };
149
155
  }
150
156
 
157
+ // Pi runs these only as its first argument, before extensions load.
158
+ const PACKAGE_COMMANDS = new Set(["install", "remove", "uninstall", "update", "list"]);
159
+
151
160
  export type StartupPlan = { kind: "none" } | { kind: "error"; message: string } | { kind: "relaunch"; command: Command; env: Env };
152
161
 
153
162
  /**
154
163
  * What `pi --space <name>` does at startup: nothing without the flag or when Pi already runs in that
155
164
  * space; otherwise run Pi again in the space with every other argument, since nothing has run yet.
165
+ * `pi --space <name> <package command> ...` always runs Pi again, even in the current home, because
166
+ * Pi runs a package command (install, remove, uninstall, update, list) only as its first argument.
156
167
  */
157
168
  export function planStartup(input: { proc: Proc; dir: string; currentHome: string; env: Env }): StartupPlan {
158
169
  const { proc, dir, currentHome, env } = input;
159
170
  const parsed = parseSpaceFlag(proc.argv.slice(2));
160
171
  if ("error" in parsed) return { kind: "error", message: parsed.error };
161
172
  if (parsed.name === undefined) return { kind: "none" };
173
+ // A package command runs alone: options before --space (a launcher may add its own) are for a session.
174
+ const packageCommand = PACKAGE_COMMANDS.has(parsed.afterName[0] ?? "");
175
+ const args = packageCommand ? parsed.afterName : parsed.rest;
162
176
  let target: SwitchTarget;
163
177
  try {
164
178
  target = resolveSwitch({ dir, name: parsed.name, currentHome, env });
165
179
  } catch (error) {
166
180
  if (!(error instanceof SpaceError)) throw error;
167
- if (error.message.startsWith("Already in")) return { kind: "none" };
181
+ if (error.message.startsWith("Already in")) {
182
+ return packageCommand ? { kind: "relaunch", command: relaunchCommand(proc, args), env } : { kind: "none" };
183
+ }
168
184
  return { kind: "error", message: error.message };
169
185
  }
170
- return { kind: "relaunch", command: relaunchCommand(proc, parsed.rest), env: relaunchEnv(env, { ...target, currentHome }) };
186
+ return { kind: "relaunch", command: relaunchCommand(proc, args), env: relaunchEnv(env, { ...target, currentHome }) };
171
187
  }
172
188
 
173
189
  /** Runs the command now with inherited stdio and returns its exit code (1 when it cannot start). */
package/src/spaces.ts CHANGED
@@ -37,9 +37,13 @@ export function spacesDir(env: Env = process.env, home: string = osHomedir()): s
37
37
  return join(dirname(resolve(baseHome)), "spaces");
38
38
  }
39
39
 
40
+ // The words of the pi-space and /space commands, so a space name never reads as one.
41
+ const COMMANDS = new Set(["list", "new", "remove", "rename", "switch", "update", "install", "uninstall"]);
42
+
40
43
  /** Returns the error message for an unusable name, or undefined when the name is valid. */
41
44
  export function validateName(name: string): string | undefined {
42
45
  if (name === BASE) return `"${BASE}" is reserved for the home Pi started with.`;
46
+ if (COMMANDS.has(name)) return `"${name}" is reserved: it is a pi-space or /space command.`;
43
47
  if (!NAME.test(name)) {
44
48
  return `Invalid space name "${name}": use lowercase letters, digits, "-" and "_", starting with a letter or digit (at most 64 characters).`;
45
49
  }