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.
- package/README.md +1 -0
- package/index.ts +68 -8
- 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
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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.
|
|
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"}}
|