pi-project-switcher 0.3.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/LICENSE +21 -0
- package/README.md +65 -0
- package/index.ts +362 -0
- package/package.json +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 stefclawd
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# pi-project-switcher
|
|
2
|
+
|
|
3
|
+
A [pi coding agent](https://github.com/earendil-works/pi) extension to switch between projects that live as direct subdirectories of a configurable base directory.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
- **`/project`** — list all projects (direct subdirectories of the base dir) with git branch info, mark the active one
|
|
8
|
+
- **`/project <name>`** — switch the active project:
|
|
9
|
+
- restores the project's last session if one is stored (see below)
|
|
10
|
+
- persists across reloads (session entry)
|
|
11
|
+
- sets the session display name
|
|
12
|
+
- injects the project path into every agent turn's system prompt, so file operations default to the active project
|
|
13
|
+
- **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
|
+
- **Auto-detection** — if pi starts inside `~/dev/<project>`, that project is active automatically
|
|
15
|
+
|
|
16
|
+
Every direct subdirectory of the base directory counts as a project. **Git is not required.** Hidden directories are ignored.
|
|
17
|
+
|
|
18
|
+
## Configuration
|
|
19
|
+
|
|
20
|
+
Precedence (first wins):
|
|
21
|
+
|
|
22
|
+
1. Settings file `~/.pi/agent/project-switcher.json`:
|
|
23
|
+
```json
|
|
24
|
+
{ "baseDir": "/home/you/dev" }
|
|
25
|
+
```
|
|
26
|
+
2. Environment variable `PI_PROJECT_SWITCHER_BASE`
|
|
27
|
+
3. Default: `~/dev`
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pi install npm:pi-project-switcher
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or from git:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pi install git:github.com/stefclawd/pi-project-switcher
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or from a local checkout:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pi install ./pi-project-switcher
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Development
|
|
48
|
+
|
|
49
|
+
Single-file TypeScript extension (`index.ts`), loaded directly by pi via jiti — no build step. Spec lives in `openspec/specs/project-switching/`.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm install
|
|
53
|
+
npm test # vitest (17 tests)
|
|
54
|
+
npm run typecheck
|
|
55
|
+
|
|
56
|
+
# Run once without installing
|
|
57
|
+
pi -e ./index.ts
|
|
58
|
+
|
|
59
|
+
# Verify
|
|
60
|
+
pi -p -e ./index.ts "/project"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT
|
package/index.ts
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-project-switcher
|
|
3
|
+
*
|
|
4
|
+
* Switch between projects under a configurable base directory.
|
|
5
|
+
* Every direct subdirectory of the base dir is a project (git optional).
|
|
6
|
+
*
|
|
7
|
+
* Commands:
|
|
8
|
+
* /project - Show active project + list all discovered projects
|
|
9
|
+
* /project <name> - Switch active project (restores its last session)
|
|
10
|
+
*
|
|
11
|
+
* Configuration (precedence):
|
|
12
|
+
* 1. ~/.pi/agent/project-switcher.json { "baseDir": "..." }
|
|
13
|
+
* 2. PI_PROJECT_SWITCHER_BASE env var
|
|
14
|
+
* 3. Default: ~/dev
|
|
15
|
+
*
|
|
16
|
+
* Session map (project -> session file, machine-local):
|
|
17
|
+
* ~/.pi/agent/project-switcher-sessions.json
|
|
18
|
+
* Session paths stored relative to ~/.pi/agent/sessions/ for portability.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
22
|
+
import { execSync } from "node:child_process";
|
|
23
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
24
|
+
import { homedir } from "node:os";
|
|
25
|
+
import { basename, join, relative, resolve } from "node:path";
|
|
26
|
+
|
|
27
|
+
const HOME = homedir();
|
|
28
|
+
const ENTRY_TYPE = "project-switcher-state";
|
|
29
|
+
const SESSIONS_DIR = join(HOME, ".pi", "agent", "sessions");
|
|
30
|
+
const SESSION_MAP_PATH = join(HOME, ".pi", "agent", "project-switcher-sessions.json");
|
|
31
|
+
|
|
32
|
+
interface Config {
|
|
33
|
+
baseDir: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** project name -> persisted session state */
|
|
37
|
+
interface SessionMap {
|
|
38
|
+
[project: string]: {
|
|
39
|
+
/** Session file path, relative to ~/.pi/agent/sessions/ */
|
|
40
|
+
sessionFile: string;
|
|
41
|
+
updatedAt: string;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function loadConfig(): Config {
|
|
46
|
+
// 1. settings file
|
|
47
|
+
const settingsPath = join(HOME, ".pi", "agent", "project-switcher.json");
|
|
48
|
+
if (existsSync(settingsPath)) {
|
|
49
|
+
try {
|
|
50
|
+
const raw = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
51
|
+
if (typeof raw.baseDir === "string" && raw.baseDir) {
|
|
52
|
+
return { baseDir: resolve(raw.baseDir) };
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// fall through to env/default
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// 2. env var
|
|
59
|
+
const env = process.env.PI_PROJECT_SWITCHER_BASE;
|
|
60
|
+
if (env) {
|
|
61
|
+
return { baseDir: resolve(env) };
|
|
62
|
+
}
|
|
63
|
+
// 3. default
|
|
64
|
+
return { baseDir: join(HOME, "dev") };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** All direct, non-hidden subdirectories of the base dir. */
|
|
68
|
+
function discoverProjects(baseDir: string): string[] {
|
|
69
|
+
if (!existsSync(baseDir)) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
return readdirSync(baseDir)
|
|
74
|
+
.filter((name) => !name.startsWith("."))
|
|
75
|
+
.filter((name) => {
|
|
76
|
+
try {
|
|
77
|
+
return statSync(join(baseDir, name)).isDirectory();
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
.sort((a, b) => a.localeCompare(b));
|
|
83
|
+
} catch {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getGitBranch(path: string): string {
|
|
89
|
+
try {
|
|
90
|
+
return execSync("git branch --show-current", { cwd: path, stdio: ["pipe", "pipe", "pipe"] })
|
|
91
|
+
.toString()
|
|
92
|
+
.trim();
|
|
93
|
+
} catch {
|
|
94
|
+
return "";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── Session map (project -> session file) ─────────────────────────────────
|
|
99
|
+
|
|
100
|
+
function loadSessionMap(): SessionMap {
|
|
101
|
+
try {
|
|
102
|
+
const raw = JSON.parse(readFileSync(SESSION_MAP_PATH, "utf8"));
|
|
103
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
104
|
+
// keep only well-formed entries
|
|
105
|
+
const map: SessionMap = {};
|
|
106
|
+
for (const [project, value] of Object.entries(raw)) {
|
|
107
|
+
if (
|
|
108
|
+
value &&
|
|
109
|
+
typeof value === "object" &&
|
|
110
|
+
typeof (value as any).sessionFile === "string" &&
|
|
111
|
+
(value as any).sessionFile
|
|
112
|
+
) {
|
|
113
|
+
map[project] = {
|
|
114
|
+
sessionFile: (value as any).sessionFile,
|
|
115
|
+
updatedAt: typeof (value as any).updatedAt === "string" ? (value as any).updatedAt : "",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return map;
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
// missing/corrupt file -> empty map
|
|
123
|
+
}
|
|
124
|
+
return {};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function saveSessionMap(map: SessionMap): void {
|
|
128
|
+
const dir = join(HOME, ".pi", "agent");
|
|
129
|
+
mkdirSync(dir, { recursive: true });
|
|
130
|
+
writeFileSync(SESSION_MAP_PATH, JSON.stringify(map, null, 2) + "\n", "utf8");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Resolve a stored relative session file to its absolute path. */
|
|
134
|
+
function resolveSessionFile(relPath: string): string {
|
|
135
|
+
const abs = resolve(SESSIONS_DIR, relPath);
|
|
136
|
+
// Guard: stored path must stay inside the sessions dir
|
|
137
|
+
const rel = relative(SESSIONS_DIR, abs);
|
|
138
|
+
if (rel.startsWith("..") || resolve(rel) === SESSIONS_DIR) {
|
|
139
|
+
return abs; // resolve() already clamps to sessions dir via resolve(), re-checked by caller via existsSync
|
|
140
|
+
}
|
|
141
|
+
return abs;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Convert an absolute session file path to the relative form we store. */
|
|
145
|
+
function toRelativeSessionFile(absPath: string): string | null {
|
|
146
|
+
const rel = relative(SESSIONS_DIR, resolve(absPath));
|
|
147
|
+
if (!rel || rel.startsWith("..")) {
|
|
148
|
+
return null; // session lives outside the default sessions dir -> don't persist
|
|
149
|
+
}
|
|
150
|
+
return rel;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function getMappedSessionFile(project: string): string | null {
|
|
154
|
+
const map = loadSessionMap();
|
|
155
|
+
const entry = map[project];
|
|
156
|
+
if (!entry) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
const abs = resolveSessionFile(entry.sessionFile);
|
|
160
|
+
return existsSync(abs) ? abs : null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function rememberSessionFile(project: string, absSessionFile: string): void {
|
|
164
|
+
const rel = toRelativeSessionFile(absSessionFile);
|
|
165
|
+
if (!rel) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const map = loadSessionMap();
|
|
169
|
+
map[project] = { sessionFile: rel, updatedAt: new Date().toISOString() };
|
|
170
|
+
saveSessionMap(map);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let activeProject: string | null = null;
|
|
174
|
+
let config: Config | null = null;
|
|
175
|
+
|
|
176
|
+
function getConfig(): Config {
|
|
177
|
+
if (!config) {
|
|
178
|
+
config = loadConfig();
|
|
179
|
+
}
|
|
180
|
+
return config;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function projectPath(name: string): string {
|
|
184
|
+
return join(getConfig().baseDir, name);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function isValidProject(name: string): boolean {
|
|
188
|
+
const projects = discoverProjects(getConfig().baseDir);
|
|
189
|
+
return projects.includes(name);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export default function (pi: ExtensionAPI) {
|
|
193
|
+
// ── Restore state on session start ──────────────────────────────────────
|
|
194
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
195
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
196
|
+
if (
|
|
197
|
+
entry.type === "custom" &&
|
|
198
|
+
(entry as any).customType === ENTRY_TYPE &&
|
|
199
|
+
(entry as any).data?.project
|
|
200
|
+
) {
|
|
201
|
+
activeProject = (entry as any).data.project;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Auto-detect from cwd if nothing persisted
|
|
206
|
+
if (!activeProject) {
|
|
207
|
+
const base = getConfig().baseDir;
|
|
208
|
+
const cwd = resolve(ctx.cwd);
|
|
209
|
+
if (cwd.startsWith(base + "/")) {
|
|
210
|
+
const candidate = cwd.slice(base.length + 1).split("/")[0];
|
|
211
|
+
if (isValidProject(candidate)) {
|
|
212
|
+
activeProject = candidate;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Keep the session map fresh: current session belongs to the active project
|
|
218
|
+
if (activeProject) {
|
|
219
|
+
pi.setSessionName(activeProject);
|
|
220
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
221
|
+
if (sessionFile) {
|
|
222
|
+
rememberSessionFile(activeProject, sessionFile);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// ── Inject project context into every agent turn ─────────────────────────
|
|
228
|
+
pi.on("before_agent_start", async (event, _ctx) => {
|
|
229
|
+
if (!activeProject || !isValidProject(activeProject)) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const path = projectPath(activeProject);
|
|
233
|
+
const branch = getGitBranch(path);
|
|
234
|
+
const branchInfo = branch ? ` (git: ${branch})` : "";
|
|
235
|
+
return {
|
|
236
|
+
systemPrompt:
|
|
237
|
+
event.systemPrompt +
|
|
238
|
+
`\n\n## Active Project\n` +
|
|
239
|
+
`Project: **${activeProject}**\n` +
|
|
240
|
+
`Path: \`${path}\`${branchInfo}\n` +
|
|
241
|
+
`All file operations and bash commands should default to this project path unless specified otherwise.`,
|
|
242
|
+
};
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// ── /project command ─────────────────────────────────────────────────────
|
|
246
|
+
pi.registerCommand("project", {
|
|
247
|
+
description: "Show or switch active project (/project [name])",
|
|
248
|
+
|
|
249
|
+
getArgumentCompletions: (prefix: string) => {
|
|
250
|
+
const projects = discoverProjects(getConfig().baseDir);
|
|
251
|
+
const filtered = projects.filter((p) => p.startsWith(prefix));
|
|
252
|
+
return filtered.length > 0
|
|
253
|
+
? filtered.map((p) => ({ value: p, label: p }))
|
|
254
|
+
: null;
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
handler: async (args, ctx) => {
|
|
258
|
+
const name = args.trim();
|
|
259
|
+
|
|
260
|
+
// ── No arg: show status ──────────────────────────────────────────────
|
|
261
|
+
if (!name) {
|
|
262
|
+
const projects = discoverProjects(getConfig().baseDir);
|
|
263
|
+
if (projects.length === 0) {
|
|
264
|
+
ctx.ui.notify(
|
|
265
|
+
`No projects found under ${getConfig().baseDir}. Set PI_PROJECT_SWITCHER_BASE or create the settings file.`,
|
|
266
|
+
"warning"
|
|
267
|
+
);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const current = activeProject ? `Active: ${activeProject}` : "No project active";
|
|
271
|
+
const lines = projects.map((p) => {
|
|
272
|
+
const branch = getGitBranch(projectPath(p));
|
|
273
|
+
const branchStr = branch ? ` [${branch}]` : "";
|
|
274
|
+
const marker = p === activeProject ? " ◀ active" : "";
|
|
275
|
+
return ` ${p}${branchStr}${marker}`;
|
|
276
|
+
});
|
|
277
|
+
ctx.ui.notify(`${current}\n\nProjects under ${getConfig().baseDir}:\n${lines.join("\n")}`, "info");
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ── Switch project ────────────────────────────────────────────────────
|
|
282
|
+
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;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (name === activeProject) {
|
|
292
|
+
ctx.ui.notify(`Already on project: ${name}`, "info");
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const previous = activeProject;
|
|
297
|
+
|
|
298
|
+
// Remember the current session under the PREVIOUS project before switching
|
|
299
|
+
const currentSessionFile = ctx.sessionManager.getSessionFile();
|
|
300
|
+
if (previous && currentSessionFile) {
|
|
301
|
+
rememberSessionFile(previous, currentSessionFile);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Try to restore the target project's last session
|
|
305
|
+
const targetSession = getMappedSessionFile(name);
|
|
306
|
+
|
|
307
|
+
if (targetSession) {
|
|
308
|
+
// Persist the switch in the OLD session before replacing it
|
|
309
|
+
pi.appendEntry(ENTRY_TYPE, { project: name, switchedAt: new Date().toISOString() });
|
|
310
|
+
|
|
311
|
+
const result = await ctx.switchSession(targetSession);
|
|
312
|
+
if (result.cancelled) {
|
|
313
|
+
// User cancelled; roll back in-memory state
|
|
314
|
+
activeProject = previous;
|
|
315
|
+
ctx.ui.notify(`Switch cancelled. Staying on ${previous ?? "no project"}.`, "info");
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// switchSession fires a new session_start, which restores state from
|
|
320
|
+
// the target session's entries (or auto-detects). Set it explicitly as
|
|
321
|
+
// a safety net in case the session has no project entry yet.
|
|
322
|
+
activeProject = name;
|
|
323
|
+
pi.setSessionName(name);
|
|
324
|
+
rememberSessionFile(name, targetSession);
|
|
325
|
+
|
|
326
|
+
const path = projectPath(name);
|
|
327
|
+
const branch = getGitBranch(path);
|
|
328
|
+
const branchStr = branch ? ` on branch \`${branch}\`` : "";
|
|
329
|
+
ctx.ui.notify(
|
|
330
|
+
`Switched to ${name} (session: ${basename(targetSession)})\n${path}${branchStr ? ` ${branchStr}` : ""}`,
|
|
331
|
+
"info"
|
|
332
|
+
);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ── Fallback: no stored session (or file gone) -> same-session switch ─
|
|
337
|
+
activeProject = name;
|
|
338
|
+
|
|
339
|
+
// Persist to session
|
|
340
|
+
pi.appendEntry(ENTRY_TYPE, { project: name, switchedAt: new Date().toISOString() });
|
|
341
|
+
|
|
342
|
+
// Update session name
|
|
343
|
+
pi.setSessionName(name);
|
|
344
|
+
|
|
345
|
+
const path = projectPath(name);
|
|
346
|
+
const branch = getGitBranch(path);
|
|
347
|
+
const branchStr = branch ? ` on branch \`${branch}\`` : "";
|
|
348
|
+
const fromStr = previous ? ` (was: ${previous})` : "";
|
|
349
|
+
|
|
350
|
+
ctx.ui.notify(`Switched to ${name}${fromStr}\n${path}${branchStr ? ` ${branchStr}` : ""}`, "info");
|
|
351
|
+
|
|
352
|
+
// Announce to the agent so it operates in the new context
|
|
353
|
+
await ctx.waitForIdle();
|
|
354
|
+
pi.sendUserMessage(
|
|
355
|
+
`[Project switched to **${name}**]\n` +
|
|
356
|
+
`Working directory: \`${path}\`${branchStr}\n` +
|
|
357
|
+
`Please keep all file operations within this project from now on.`,
|
|
358
|
+
{ deliverAs: "followUp" }
|
|
359
|
+
);
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name": "pi-project-switcher", "version": "0.3.1", "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"}}
|