pi-project-switcher 0.4.0 → 0.5.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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/index.ts +68 -8
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -10,6 +10,7 @@ A [pi coding agent](https://github.com/earendil-works/pi) extension to switch be
10
10
  - persists across reloads (session entry)
11
11
  - sets the session display name
12
12
  - injects the project path into every agent turn's system prompt, so file operations default to the active project
13
+ - **if the project doesn't exist yet**, offers to create the folder and switch to it (confirmation dialog on dialog-capable surfaces; use `/project <name>!` to skip the dialog — e.g. on headless/RPC surfaces). Unsafe names (path segments, `..`, hidden, absolute) are never created.
13
14
  - **Session restore** — a machine-local map (`~/.pi/agent/project-switcher-sessions.json`) remembers the most recent session per project. Switching projects returns you to that project's last session; if none exists (or the file is gone), the switch happens in the current session.
14
15
  - **Auto-detection** — if pi starts inside `~/dev/<project>`, that project is active automatically
15
16
 
package/index.ts CHANGED
@@ -22,7 +22,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
22
22
  import { execSync } from "node:child_process";
23
23
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
24
24
  import { homedir } from "node:os";
25
- import { basename, join, relative, resolve } from "node:path";
25
+ import { basename, isAbsolute, join, relative, resolve } from "node:path";
26
26
 
27
27
  const HOME = homedir();
28
28
  const ENTRY_TYPE = "project-switcher-state";
@@ -189,6 +189,26 @@ function isValidProject(name: string): boolean {
189
189
  return projects.includes(name);
190
190
  }
191
191
 
192
+ /** True when a name is safe to materialize as a single directory under the base dir. */
193
+ function isSafeProjectName(name: string): boolean {
194
+ if (!name || name === "." || name === "..") {
195
+ return false;
196
+ }
197
+ if (name.startsWith(".")) {
198
+ return false; // hidden
199
+ }
200
+ if (name.includes("/") || name.includes("\\")) {
201
+ return false; // no path segments
202
+ }
203
+ if (name.includes("..")) {
204
+ return false; // no traversal
205
+ }
206
+ if (isAbsolute(name)) {
207
+ return false; // absolute paths
208
+ }
209
+ return true;
210
+ }
211
+
192
212
  export default function (pi: ExtensionAPI) {
193
213
  // ── Restore state on session start ──────────────────────────────────────
194
214
  pi.on("session_start", async (_event, ctx) => {
@@ -255,7 +275,13 @@ export default function (pi: ExtensionAPI) {
255
275
  },
256
276
 
257
277
  handler: async (args, ctx) => {
258
- const name = args.trim();
278
+ let name = args.trim();
279
+
280
+ // ── Explicit create opt-in: trailing "!" on the project name ────────
281
+ const createOptIn = name.endsWith("!");
282
+ if (createOptIn) {
283
+ name = name.slice(0, -1).trim();
284
+ }
259
285
 
260
286
  // ── No arg: show status ──────────────────────────────────────────────
261
287
  if (!name) {
@@ -280,12 +306,46 @@ export default function (pi: ExtensionAPI) {
280
306
 
281
307
  // ── Switch project ────────────────────────────────────────────────────
282
308
  if (!isValidProject(name)) {
283
- const available = discoverProjects(getConfig().baseDir).join(", ");
284
- ctx.ui.notify(
285
- `Unknown project: "${name}".\nAvailable: ${available || "(none)"}`,
286
- "warning"
287
- );
288
- return;
309
+ // Offer to create the folder and switch to it (never silently)
310
+ if (isSafeProjectName(name)) {
311
+ let create = false;
312
+ if (createOptIn) {
313
+ create = true;
314
+ } else if (ctx.hasUI) {
315
+ try {
316
+ create = await ctx.ui.confirm(
317
+ "Create project?",
318
+ `Project "${name}" does not exist. Create ${join(getConfig().baseDir, name)} and switch to it?`
319
+ );
320
+ } catch {
321
+ create = false;
322
+ }
323
+ }
324
+
325
+ if (create) {
326
+ const newPath = projectPath(name);
327
+ try {
328
+ mkdirSync(newPath, { recursive: false });
329
+ ctx.ui.notify(`Created project folder: ${newPath}`, "info");
330
+ } catch (err: any) {
331
+ ctx.ui.notify(
332
+ `Failed to create project folder ${newPath}: ${err?.message ?? err}`,
333
+ "error"
334
+ );
335
+ return;
336
+ }
337
+ // fall through: the folder now exists and the switch proceeds below
338
+ }
339
+ }
340
+
341
+ if (!isValidProject(name)) {
342
+ const available = discoverProjects(getConfig().baseDir).join(", ");
343
+ ctx.ui.notify(
344
+ `Unknown project: "${name}".\nAvailable: ${available || "(none)"}`,
345
+ "warning"
346
+ );
347
+ return;
348
+ }
289
349
  }
290
350
 
291
351
  if (name === activeProject) {
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name": "pi-project-switcher", "version": "0.4.0", "description": "pi coding agent extension: switch between projects under a configurable base directory via /project", "main": "index.ts", "type": "module", "scripts": {"test": "vitest run", "test:watch": "vitest", "typecheck": "tsc --noEmit"}, "keywords": ["pi", "pi-package", "pi-extension", "project", "switcher", "project-switching"], "author": "stefclawd", "license": "MIT", "repository": {"type": "git", "url": "git+https://github.com/stefclawd/pi-project-switcher.git"}, "bugs": {"url": "https://github.com/stefclawd/pi-project-switcher/issues"}, "homepage": "https://github.com/stefclawd/pi-project-switcher#readme", "files": ["index.ts", "README.md", "LICENSE"], "engines": {"node": ">=22.19.0"}, "pi": {"extensions": ["./index.ts"]}, "devDependencies": {"@earendil-works/pi-coding-agent": "^0.85.1", "@types/node": "^24.0.0", "typescript": "^5.7.0", "vitest": "^3.0.0"}}
1
+ {"name": "pi-project-switcher", "version": "0.5.0", "description": "pi coding agent extension: switch between projects under a configurable base directory via /project", "main": "index.ts", "type": "module", "scripts": {"test": "vitest run", "test:watch": "vitest", "typecheck": "tsc --noEmit"}, "keywords": ["pi", "pi-package", "pi-extension", "project", "switcher", "project-switching"], "author": "stefclawd", "license": "MIT", "repository": {"type": "git", "url": "git+https://github.com/stefclawd/pi-project-switcher.git"}, "bugs": {"url": "https://github.com/stefclawd/pi-project-switcher/issues"}, "homepage": "https://github.com/stefclawd/pi-project-switcher#readme", "files": ["index.ts", "README.md", "LICENSE"], "engines": {"node": ">=22.19.0"}, "pi": {"extensions": ["./index.ts"]}, "devDependencies": {"@earendil-works/pi-coding-agent": "^0.85.1", "@types/node": "^24.0.0", "typescript": "^5.7.0", "vitest": "^3.0.0"}}