Skip to content

Commit a922b65

Browse files
committed
1.3.9
1 parent 1a2a0d4 commit a922b65

1 file changed

Lines changed: 207 additions & 7 deletions

File tree

extensions/workspace.ts

Lines changed: 207 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,22 +30,40 @@ import { readSettings, writeSettings } from "../shared/config-io";
3030
const WORKSPACE_DIR = join(homedir(), ".pi", "agent", "workspaces");
3131
const WORKSPACE_EXT = ".ws.json";
3232

33+
// Maximum file size to archive (100KB)
34+
const MAX_FILE_SIZE = 100 * 1024;
35+
// File extensions to skip when archiving
36+
const SKIP_EXTENSIONS = [".log", ".tmp", ".cache", ".lock", ".swp", ".swo"];
37+
3338
// ============================================================================
3439
// Types
3540
// ============================================================================
3641

42+
interface WorkspaceExtension {
43+
name: string;
44+
source: "local" | "git" | "package";
45+
package?: string;
46+
}
47+
3748
interface WorkspaceState {
3849
name: string;
3950
savedAt: string;
4051
session: { sessionName?: string };
4152
skills: string[];
42-
extensions: string[];
53+
extensions: WorkspaceExtension[];
4354
configs: Record<string, unknown>;
4455
soul?: { name: string; level: number };
4556
cwd?: string;
57+
repo?: string;
58+
repos?: { path: string; remote: string | null }[];
59+
content?: WorkspaceContent;
4660
version: string;
4761
}
4862

63+
interface WorkspaceContent {
64+
files: { path: string; content: string }[];
65+
}
66+
4967
// ============================================================================
5068
// Helpers
5169
// ============================================================================
@@ -70,8 +88,8 @@ function saveWorkspaceState(name: string, state: WorkspaceState): boolean {
7088
} catch { return false; }
7189
}
7290

73-
function getCurrentExtensions(): string[] {
74-
const extensions: string[] = [];
91+
function getCurrentExtensions(): WorkspaceExtension[] {
92+
const extensions: WorkspaceExtension[] = [];
7593
const seen = new Set<string>();
7694

7795
// Check ~/.pi/agent/extensions (local extensions)
@@ -82,7 +100,7 @@ function getCurrentExtensions(): string[] {
82100
if (entry.endsWith(".js") || entry.endsWith(".mjs")) {
83101
const extName = entry.replace(/\.(js|mjs)$/, "");
84102
if (!seen.has(extName)) {
85-
extensions.push(extName);
103+
extensions.push({ name: extName, source: "local" });
86104
seen.add(extName);
87105
}
88106
}
@@ -114,12 +132,18 @@ function getCurrentExtensions(): string[] {
114132
const extPath = join(userDir, repoEntry.name, "extensions");
115133
if (!existsSync(extPath)) continue;
116134

135+
const repoUrl = `${hostEntry.name}/${userEntry.name}/${repoEntry.name}`;
136+
117137
const extFiles = readdirSync(extPath);
118138
for (const entry of extFiles) {
119139
if (entry.endsWith(".ts") || entry.endsWith(".js")) {
120140
const extName = entry.replace(/\.(ts|js)$/, "");
121141
if (!seen.has(extName)) {
122-
extensions.push(extName);
142+
extensions.push({
143+
name: extName,
144+
source: "git",
145+
package: repoUrl
146+
});
123147
seen.add(extName);
124148
}
125149
}
@@ -132,6 +156,40 @@ function getCurrentExtensions(): string[] {
132156
debugLog("workspace", "failed to scan git extensions", err);
133157
}
134158

159+
// Check git packages for extensions in package.json
160+
try {
161+
const packages = readSettings().packages || [];
162+
for (const pkg of packages) {
163+
// Handle git:github.com/VTSTech/pi-coding-agent format
164+
const gitMatch = pkg.match(/^git:(.+)$/);
165+
if (gitMatch) {
166+
const repoPath = gitMatch[1]; // e.g., github.com/VTSTech/pi-coding-agent
167+
const [hostUserRepo] = repoPath.split("/"); // Could be more complex
168+
169+
// Try to get extensions from the git repo
170+
const gitExtPath = join(homedir(), ".pi", "agent", "git", repoPath, "extensions");
171+
if (existsSync(gitExtPath)) {
172+
const extFiles = readdirSync(gitExtPath);
173+
for (const entry of extFiles) {
174+
if (entry.endsWith(".ts") || entry.endsWith(".js")) {
175+
const extName = entry.replace(/\.(ts|js)$/, "");
176+
if (!seen.has(extName)) {
177+
extensions.push({
178+
name: extName,
179+
source: "package",
180+
package: repoPath
181+
});
182+
seen.add(extName);
183+
}
184+
}
185+
}
186+
}
187+
}
188+
}
189+
} catch (err) {
190+
debugLog("workspace", "failed to scan package extensions", err);
191+
}
192+
135193
return extensions;
136194
}
137195

@@ -162,6 +220,112 @@ function getCurrentSoul(): { name: string; level: number } | null {
162220

163221
const BRANDING = `⚡ Pi Workspace Manager v1.3.5 - VTSTech`;
164222

223+
// ============================================================================
224+
// Git & Content Helpers
225+
// ============================================================================
226+
227+
/**
228+
* Check if a directory is a git repository
229+
*/
230+
function isGitRepo(dir: string): boolean {
231+
try {
232+
return existsSync(join(dir, ".git"));
233+
} catch { return false; }
234+
}
235+
236+
/**
237+
* Get the git remote URL for a directory, if it's a repo
238+
*/
239+
function getGitRemoteUrl(dir: string): string | null {
240+
try {
241+
const result = require("child_process").execSync(
242+
`git -C "${dir}" remote get-url origin 2>/dev/null`,
243+
{ encoding: "utf-8" }
244+
).trim();
245+
return result || null;
246+
} catch { return null; }
247+
}
248+
249+
/**
250+
* Find git repositories within a directory (up to 2 levels deep)
251+
* Skip !dirs (reference folders) but still scan .dirs for repos
252+
*/
253+
function findGitRepos(baseDir: string): { path: string; remote: string | null }[] {
254+
const repos: { path: string; remote: string | null }[] = [];
255+
256+
function scanDir(dir: string, depth: number) {
257+
if (depth > 2) return;
258+
259+
try {
260+
const entries = readdirSync(dir, { withFileTypes: true });
261+
for (const entry of entries) {
262+
// Skip .git directories and !dirs (reference folders)
263+
if (entry.name === ".git" || entry.name.startsWith("!")) continue;
264+
if (!entry.isDirectory()) continue;
265+
266+
const fullPath = join(dir, entry.name);
267+
if (isGitRepo(fullPath)) {
268+
repos.push({ path: fullPath, remote: getGitRemoteUrl(fullPath) });
269+
}
270+
scanDir(fullPath, depth + 1);
271+
}
272+
} catch (err) {
273+
debugLog("workspace", "failed to scan dir for repos", err);
274+
}
275+
}
276+
277+
scanDir(baseDir, 0);
278+
return repos;
279+
}
280+
281+
/**
282+
* Get workspace directory, skipping !dirs (reference folders)
283+
* .dirs ARE scanned for content as skills/extensions may live there
284+
*/
285+
function getWorkspaceContent(dir: string): WorkspaceContent {
286+
const files: { path: string; content: string }[] = [];
287+
288+
function scanForFiles(currentDir: string, basePath: string) {
289+
try {
290+
const entries = readdirSync(currentDir, { withFileTypes: true });
291+
for (const entry of entries) {
292+
// Skip .git directories and !dirs (reference folders)
293+
if (entry.name === ".git" || entry.name.startsWith("!")) continue;
294+
295+
const fullPath = join(currentDir, entry.name);
296+
const relativePath = join(basePath, entry.name);
297+
298+
if (entry.isDirectory()) {
299+
// Don't skip .dirs - they may contain skills/extensions
300+
scanForFiles(fullPath, relativePath);
301+
} else if (entry.isFile()) {
302+
const ext = "." + entry.name.split(".").pop();
303+
// Skip certain extensions and large files
304+
if (SKIP_EXTENSIONS.includes(ext) || entry.name.endsWith(".png") || entry.name.endsWith(".jpg") || entry.name.endsWith(".gif")) continue;
305+
306+
try {
307+
const stats = require("fs").statSync(fullPath);
308+
if (stats.size > MAX_FILE_SIZE) continue;
309+
310+
const content = readFileSync(fullPath, "utf-8");
311+
// Skip binary content (but allow .ts/.js even if they have nulls)
312+
if (content.includes("\x00") && ext !== ".ts" && ext !== ".js") continue;
313+
314+
files.push({ path: relativePath, content });
315+
} catch (err) {
316+
debugLog("workspace", `failed to read file ${fullPath}`, err);
317+
}
318+
}
319+
}
320+
} catch (err) {
321+
debugLog("workspace", "failed to scan workspace content", err);
322+
}
323+
}
324+
325+
scanForFiles(dir, "");
326+
return { files };
327+
}
328+
165329
// ============================================================================
166330
// Extension
167331
// ============================================================================
@@ -198,17 +362,36 @@ export default function (pi: ExtensionAPI) {
198362
});
199363

200364
async function handleSave(ctx: any, name: string) {
365+
const cwd = process.cwd();
366+
const repos = findGitRepos(cwd);
367+
const isCurrentDirRepo = isGitRepo(cwd);
368+
201369
const state: WorkspaceState = {
202370
name, savedAt: new Date().toISOString(),
203371
session: { sessionName: undefined },
204372
skills: getCurrentSkills(),
205373
extensions: getCurrentExtensions(),
206374
configs: readSettings(),
207375
soul: getCurrentSoul(),
208-
cwd: process.cwd(),
376+
cwd,
209377
version: "1.0.0",
210378
};
211379

380+
// If current directory is a git repo, save its URL
381+
if (isCurrentDirRepo) {
382+
state.repo = getGitRemoteUrl(cwd);
383+
}
384+
385+
// If git repos are found within the workspace, save their info
386+
if (repos.length > 0) {
387+
state.repos = repos;
388+
}
389+
390+
// Only archive content if there are no repos (git repos can be cloned)
391+
if (!isCurrentDirRepo && repos.length === 0) {
392+
state.content = getWorkspaceContent(cwd);
393+
}
394+
212395
if (saveWorkspaceState(name, state)) {
213396
ctx.ui.notify(`Saved workspace "${name}"`, "success");
214397
}
@@ -249,10 +432,27 @@ export default function (pi: ExtensionAPI) {
249432
async function handleCurrent(ctx: any) {
250433
const exts = getCurrentExtensions();
251434
const skills = getCurrentSkills();
435+
const cwd = process.cwd();
436+
const repos = findGitRepos(cwd);
437+
const isCurrentRepo = isGitRepo(cwd);
252438

253439
let output = `${BRANDING}\n\n`;
254440
output += `Extensions: ${exts.length}\n`;
255-
output += `Skills: ${skills.length}\n`;
441+
for (const ext of exts) {
442+
const sourceInfo = ext.source === "local" ? "(local)" : ext.package ? `(${ext.package})` : "";
443+
output += ` - ${ext.name} ${sourceInfo}\n`;
444+
}
445+
output += `\nSkills: ${skills.length}\n`;
446+
for (const skill of skills) {
447+
output += ` - ${skill}\n`;
448+
}
449+
output += `\nRepos found: ${repos.length}\n`;
450+
for (const repo of repos) {
451+
output += ` - ${repo.path} ${repo.remote ? `(${repo.remote})` : ""}\n`;
452+
}
453+
if (isCurrentRepo) {
454+
output += ` - (current dir) ${getGitRemoteUrl(cwd)}\n`;
455+
}
256456

257457
pi.sendMessage({
258458
customType: "workspace-current",

0 commit comments

Comments
 (0)