@jmfederico/pi-web 1.202605.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 +321 -0
- package/dist/cli.js +256 -0
- package/dist/cli.js.map +1 -0
- package/dist/client/assets/CodeViewer-DsXI9VCn.js +4 -0
- package/dist/client/assets/TerminalPanel-CpzJEFv1.js +47 -0
- package/dist/client/assets/index-Cbr8EG8h.js +687 -0
- package/dist/client/assets/vendor-editor-core-hulUn3GY.js +12 -0
- package/dist/client/assets/vendor-editor-languages-Cjllm-a8.js +26 -0
- package/dist/client/assets/vendor-editor-legacy-B4QLsWF8.js +1 -0
- package/dist/client/assets/vendor-terminal-DDGTF8rc.css +1 -0
- package/dist/client/assets/vendor-terminal-DjQ08hXu.js +16 -0
- package/dist/client/index.html +16 -0
- package/dist/config.js +92 -0
- package/dist/config.js.map +1 -0
- package/dist/server/app.js +80 -0
- package/dist/server/app.js.map +1 -0
- package/dist/server/git/gitService.js +118 -0
- package/dist/server/git/gitService.js.map +1 -0
- package/dist/server/gitRoutes.js +23 -0
- package/dist/server/gitRoutes.js.map +1 -0
- package/dist/server/index.js +7 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/projects/directorySuggestions.js +37 -0
- package/dist/server/projects/directorySuggestions.js.map +1 -0
- package/dist/server/projects/projectService.js +31 -0
- package/dist/server/projects/projectService.js.map +1 -0
- package/dist/server/realtime/sessionEventHub.js +36 -0
- package/dist/server/realtime/sessionEventHub.js.map +1 -0
- package/dist/server/sessiond/config.js +9 -0
- package/dist/server/sessiond/config.js.map +1 -0
- package/dist/server/sessiond/sessionDaemonClient.js +65 -0
- package/dist/server/sessiond/sessionDaemonClient.js.map +1 -0
- package/dist/server/sessiond/sessionProxyRoutes.js +63 -0
- package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -0
- package/dist/server/sessiond.js +45 -0
- package/dist/server/sessiond.js.map +1 -0
- package/dist/server/sessions/builtinCommands.js +27 -0
- package/dist/server/sessions/builtinCommands.js.map +1 -0
- package/dist/server/sessions/piSessionService.js +517 -0
- package/dist/server/sessions/piSessionService.js.map +1 -0
- package/dist/server/sessions/sessionArchiveStore.js +68 -0
- package/dist/server/sessions/sessionArchiveStore.js.map +1 -0
- package/dist/server/sessions/sessionCommandService.js +134 -0
- package/dist/server/sessions/sessionCommandService.js.map +1 -0
- package/dist/server/sessions/sessionNameGenerator.js +68 -0
- package/dist/server/sessions/sessionNameGenerator.js.map +1 -0
- package/dist/server/sessions/sessionRoutes.js +116 -0
- package/dist/server/sessions/sessionRoutes.js.map +1 -0
- package/dist/server/sessions/sessionRuntimeStore.js +2 -0
- package/dist/server/sessions/sessionRuntimeStore.js.map +1 -0
- package/dist/server/storage/projectStore.js +88 -0
- package/dist/server/storage/projectStore.js.map +1 -0
- package/dist/server/terminalProxyRoutes.js +70 -0
- package/dist/server/terminalProxyRoutes.js.map +1 -0
- package/dist/server/terminals/terminalRoutes.js +70 -0
- package/dist/server/terminals/terminalRoutes.js.map +1 -0
- package/dist/server/terminals/terminalService.js +115 -0
- package/dist/server/terminals/terminalService.js.map +1 -0
- package/dist/server/types.js +2 -0
- package/dist/server/types.js.map +1 -0
- package/dist/server/workspaceExplorerRoutes.js +24 -0
- package/dist/server/workspaceExplorerRoutes.js.map +1 -0
- package/dist/server/workspaces/fileContentService.js +50 -0
- package/dist/server/workspaces/fileContentService.js.map +1 -0
- package/dist/server/workspaces/fileSuggestions.js +84 -0
- package/dist/server/workspaces/fileSuggestions.js.map +1 -0
- package/dist/server/workspaces/fileTreeService.js +26 -0
- package/dist/server/workspaces/fileTreeService.js.map +1 -0
- package/dist/server/workspaces/gitWorktreeDiscovery.js +33 -0
- package/dist/server/workspaces/gitWorktreeDiscovery.js.map +1 -0
- package/dist/server/workspaces/pathSafety.js +38 -0
- package/dist/server/workspaces/pathSafety.js.map +1 -0
- package/dist/server/workspaces/workspaceContext.js +8 -0
- package/dist/server/workspaces/workspaceContext.js.map +1 -0
- package/dist/server/workspaces/workspaceService.js +39 -0
- package/dist/server/workspaces/workspaceService.js.map +1 -0
- package/dist/shared/apiTypes.js +2 -0
- package/dist/shared/apiTypes.js.map +1 -0
- package/extensions/pi-web.ts +144 -0
- package/install.sh +5 -0
- package/package.json +107 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import * as pty from "node-pty";
|
|
4
|
+
const MAX_REPLAY_BUFFER = 200_000;
|
|
5
|
+
export class TerminalService {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.terminals = new Map();
|
|
8
|
+
}
|
|
9
|
+
list(cwd) {
|
|
10
|
+
return [...this.terminals.values()]
|
|
11
|
+
.filter((terminal) => terminal.cwd === cwd)
|
|
12
|
+
.map(toInfo);
|
|
13
|
+
}
|
|
14
|
+
create(options) {
|
|
15
|
+
if (options.cwd === "")
|
|
16
|
+
throw new Error("cwd is required");
|
|
17
|
+
const id = randomUUID();
|
|
18
|
+
const createdAt = new Date().toISOString();
|
|
19
|
+
const shell = process.env["SHELL"] ?? "/bin/bash";
|
|
20
|
+
const terminal = pty.spawn(shell, [], {
|
|
21
|
+
name: "xterm-256color",
|
|
22
|
+
cwd: options.cwd,
|
|
23
|
+
cols: options.cols ?? 100,
|
|
24
|
+
rows: options.rows ?? 30,
|
|
25
|
+
env: { ...process.env, TERM: "xterm-256color" },
|
|
26
|
+
});
|
|
27
|
+
const requestedName = options.name?.trim();
|
|
28
|
+
const record = {
|
|
29
|
+
id,
|
|
30
|
+
cwd: options.cwd,
|
|
31
|
+
name: requestedName !== undefined && requestedName !== "" ? requestedName : `Shell ${String(this.list(options.cwd).length + 1)}`,
|
|
32
|
+
createdAt,
|
|
33
|
+
exited: false,
|
|
34
|
+
pty: terminal,
|
|
35
|
+
buffer: "",
|
|
36
|
+
events: new EventEmitter(),
|
|
37
|
+
};
|
|
38
|
+
terminal.onData((data) => {
|
|
39
|
+
record.buffer = trimReplayBuffer(record.buffer + data);
|
|
40
|
+
record.events.emit("output", data);
|
|
41
|
+
});
|
|
42
|
+
terminal.onExit(({ exitCode }) => {
|
|
43
|
+
record.exited = true;
|
|
44
|
+
record.exitCode = exitCode;
|
|
45
|
+
record.events.emit("exit", exitCode);
|
|
46
|
+
});
|
|
47
|
+
this.terminals.set(id, record);
|
|
48
|
+
return toInfo(record);
|
|
49
|
+
}
|
|
50
|
+
get(id) {
|
|
51
|
+
const terminal = this.terminals.get(id);
|
|
52
|
+
return terminal === undefined ? undefined : toInfo(terminal);
|
|
53
|
+
}
|
|
54
|
+
attach(id, handlers) {
|
|
55
|
+
const terminal = this.require(id);
|
|
56
|
+
if (terminal.buffer !== "")
|
|
57
|
+
handlers.output(terminal.buffer);
|
|
58
|
+
if (terminal.exited)
|
|
59
|
+
handlers.exit(terminal.exitCode);
|
|
60
|
+
const onOutput = (data) => { handlers.output(data); };
|
|
61
|
+
const onExit = (exitCode) => { handlers.exit(exitCode); };
|
|
62
|
+
terminal.events.on("output", onOutput);
|
|
63
|
+
terminal.events.on("exit", onExit);
|
|
64
|
+
return () => {
|
|
65
|
+
terminal.events.off("output", onOutput);
|
|
66
|
+
terminal.events.off("exit", onExit);
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
write(id, data) {
|
|
70
|
+
const terminal = this.require(id);
|
|
71
|
+
if (!terminal.exited)
|
|
72
|
+
terminal.pty.write(data);
|
|
73
|
+
}
|
|
74
|
+
resize(id, cols, rows) {
|
|
75
|
+
const terminal = this.require(id);
|
|
76
|
+
if (!terminal.exited && Number.isFinite(cols) && Number.isFinite(rows) && cols > 0 && rows > 0) {
|
|
77
|
+
terminal.pty.resize(Math.floor(cols), Math.floor(rows));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
close(id) {
|
|
81
|
+
const terminal = this.terminals.get(id);
|
|
82
|
+
if (terminal === undefined)
|
|
83
|
+
return;
|
|
84
|
+
this.terminals.delete(id);
|
|
85
|
+
terminal.events.removeAllListeners();
|
|
86
|
+
if (!terminal.exited)
|
|
87
|
+
terminal.pty.kill();
|
|
88
|
+
}
|
|
89
|
+
dispose() {
|
|
90
|
+
for (const id of [...this.terminals.keys()])
|
|
91
|
+
this.close(id);
|
|
92
|
+
}
|
|
93
|
+
require(id) {
|
|
94
|
+
const terminal = this.terminals.get(id);
|
|
95
|
+
if (terminal === undefined)
|
|
96
|
+
throw new Error("Terminal not found");
|
|
97
|
+
return terminal;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function toInfo(record) {
|
|
101
|
+
return {
|
|
102
|
+
id: record.id,
|
|
103
|
+
cwd: record.cwd,
|
|
104
|
+
name: record.name,
|
|
105
|
+
createdAt: record.createdAt,
|
|
106
|
+
exited: record.exited,
|
|
107
|
+
...(record.exitCode === undefined ? {} : { exitCode: record.exitCode }),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function trimReplayBuffer(buffer) {
|
|
111
|
+
if (buffer.length <= MAX_REPLAY_BUFFER)
|
|
112
|
+
return buffer;
|
|
113
|
+
return buffer.slice(buffer.length - MAX_REPLAY_BUFFER);
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=terminalService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terminalService.js","sourceRoot":"","sources":["../../../src/server/terminals/terminalService.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/server/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { resolveWorkspaceContext } from "./workspaces/workspaceContext.js";
|
|
2
|
+
import { listWorkspaceTree } from "./workspaces/fileTreeService.js";
|
|
3
|
+
import { readWorkspaceFile } from "./workspaces/fileContentService.js";
|
|
4
|
+
export function registerWorkspaceExplorerRoutes(app, projects, workspaces) {
|
|
5
|
+
app.get("/api/projects/:projectId/workspaces/:workspaceId/tree", async (request, reply) => {
|
|
6
|
+
try {
|
|
7
|
+
const context = await resolveWorkspaceContext(projects, workspaces, request.params.projectId, request.params.workspaceId);
|
|
8
|
+
return await listWorkspaceTree(context.root, request.query.path);
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
return reply.code(400).send({ error: error instanceof Error ? error.message : String(error) });
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
app.get("/api/projects/:projectId/workspaces/:workspaceId/file", async (request, reply) => {
|
|
15
|
+
try {
|
|
16
|
+
const context = await resolveWorkspaceContext(projects, workspaces, request.params.projectId, request.params.workspaceId);
|
|
17
|
+
return await readWorkspaceFile(context.root, request.query.path);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
return reply.code(400).send({ error: error instanceof Error ? error.message : String(error) });
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=workspaceExplorerRoutes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspaceExplorerRoutes.js","sourceRoot":"","sources":["../../src/server/workspaceExplorerRoutes.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,uBAAuB,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AAEvE,MAAM,UAAU,+BAA+B,CAAC,GAAoB,EAAE,QAAwB,EAAE,UAA4B;IAC1H,GAAG,CAAC,GAAG,CAAyF,uDAAuD,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QAChL,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,uBAAuB,CAAC,QAAQ,EAAE,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;YAC1H,OAAO,MAAM,iBAAiB,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACnE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACjG,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,GAAG,CAAyF,uDAAuD,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QAChL,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,uBAAuB,CAAC,QAAQ,EAAE,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;YAC1H,OAAO,MAAM,iBAAiB,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACnE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACjG,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { resolveInsideWorkspace } from "./pathSafety.js";
|
|
3
|
+
const MAX_BYTES = 512 * 1024;
|
|
4
|
+
export async function readWorkspaceFile(rootPath, path) {
|
|
5
|
+
if (path === undefined || path === "")
|
|
6
|
+
throw new Error("path query parameter is required");
|
|
7
|
+
const { target, relativePath } = await resolveInsideWorkspace(rootPath, path);
|
|
8
|
+
const s = await stat(target);
|
|
9
|
+
if (!s.isFile())
|
|
10
|
+
throw new Error("Path is not a file");
|
|
11
|
+
const bytesToRead = Math.min(s.size, MAX_BYTES);
|
|
12
|
+
const buffer = (await readFile(target)).subarray(0, bytesToRead);
|
|
13
|
+
const binary = isProbablyBinary(buffer);
|
|
14
|
+
return {
|
|
15
|
+
path: relativePath,
|
|
16
|
+
...languageForPath(relativePath),
|
|
17
|
+
encoding: "utf8",
|
|
18
|
+
size: s.size,
|
|
19
|
+
modifiedAt: s.mtime.toISOString(),
|
|
20
|
+
content: binary ? "" : buffer.toString("utf8"),
|
|
21
|
+
truncated: s.size > MAX_BYTES,
|
|
22
|
+
binary,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function isProbablyBinary(buffer) {
|
|
26
|
+
const sample = buffer.subarray(0, Math.min(buffer.length, 8192));
|
|
27
|
+
return sample.includes(0);
|
|
28
|
+
}
|
|
29
|
+
function languageForPath(path) {
|
|
30
|
+
const ext = path.split(".").pop()?.toLowerCase();
|
|
31
|
+
const languages = {
|
|
32
|
+
ts: "typescript",
|
|
33
|
+
tsx: "typescript",
|
|
34
|
+
js: "javascript",
|
|
35
|
+
jsx: "javascript",
|
|
36
|
+
json: "json",
|
|
37
|
+
md: "markdown",
|
|
38
|
+
css: "css",
|
|
39
|
+
html: "html",
|
|
40
|
+
py: "python",
|
|
41
|
+
rs: "rust",
|
|
42
|
+
go: "go",
|
|
43
|
+
sh: "shell",
|
|
44
|
+
yml: "yaml",
|
|
45
|
+
yaml: "yaml",
|
|
46
|
+
};
|
|
47
|
+
const language = ext === undefined ? undefined : languages[ext];
|
|
48
|
+
return language === undefined ? {} : { language };
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=fileContentService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileContentService.js","sourceRoot":"","sources":["../../../src/server/workspaces/fileContentService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAElD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,MAAM,SAAS,GAAG,GAAG,GAAG,IAAI,CAAC;AAE7B,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,QAAgB,EAAE,IAAwB;IAChF,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;IAC3F,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,sBAAsB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC9E,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,CAAC;IAC7B,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC;IACvD,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,CAAC,MAAM,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC;IACjE,MAAM,MAAM,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC;IACxC,OAAO;QACL,IAAI,EAAE,YAAY;QAClB,GAAG,eAAe,CAAC,YAAY,CAAC;QAChC,QAAQ,EAAE,MAAM;QAChB,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC,WAAW,EAAE;QACjC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;QAC9C,SAAS,EAAE,CAAC,CAAC,IAAI,GAAG,SAAS;QAC7B,MAAM;KACP,CAAC;AACJ,CAAC;AAED,SAAS,gBAAgB,CAAC,MAAc;IACtC,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC;IACjE,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;AAC5B,CAAC;AAED,SAAS,eAAe,CAAC,IAAY;IACnC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,WAAW,EAAE,CAAC;IACjD,MAAM,SAAS,GAAuC;QACpD,EAAE,EAAE,YAAY;QAChB,GAAG,EAAE,YAAY;QACjB,EAAE,EAAE,YAAY;QAChB,GAAG,EAAE,YAAY;QACjB,IAAI,EAAE,MAAM;QACZ,EAAE,EAAE,UAAU;QACd,GAAG,EAAE,KAAK;QACV,IAAI,EAAE,MAAM;QACZ,EAAE,EAAE,QAAQ;QACZ,EAAE,EAAE,MAAM;QACV,EAAE,EAAE,IAAI;QACR,EAAE,EAAE,OAAO;QACX,GAAG,EAAE,MAAM;QACX,IAAI,EAAE,MAAM;KACb,CAAC;IACF,MAAM,QAAQ,GAAG,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IAChE,OAAO,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC;AACpD,CAAC"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { readdir, stat } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
export async function listFileSuggestions(cwd, query = "", kind) {
|
|
7
|
+
const normalizedQuery = query.replace(/^@/, "").toLowerCase();
|
|
8
|
+
const files = await listGitFiles(cwd).catch(() => listPlainFiles(cwd));
|
|
9
|
+
return files
|
|
10
|
+
.filter((file) => !kind || file.kind === kind)
|
|
11
|
+
.filter((file) => !normalizedQuery || file.path.toLowerCase().includes(normalizedQuery))
|
|
12
|
+
.sort((a, b) => Number(!a.path.endsWith("/")) - Number(!b.path.endsWith("/")) || a.path.localeCompare(b.path))
|
|
13
|
+
.slice(0, 80);
|
|
14
|
+
}
|
|
15
|
+
export async function listPathSuggestions(cwd, prefix = "") {
|
|
16
|
+
const normalizedPrefix = prefix.replace(/^@/, "").replace(/\\/g, "/");
|
|
17
|
+
const directoryPrefix = normalizedPrefix.endsWith("/") ? normalizedPrefix : dirname(normalizedPrefix) === "." ? "" : `${dirname(normalizedPrefix)}/`;
|
|
18
|
+
const searchPrefix = normalizedPrefix.endsWith("/") ? "" : basename(normalizedPrefix);
|
|
19
|
+
const entries = await readdir(join(cwd, directoryPrefix), { withFileTypes: true });
|
|
20
|
+
const suggestions = [];
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
if (!entry.name.toLowerCase().startsWith(searchPrefix.toLowerCase()))
|
|
23
|
+
continue;
|
|
24
|
+
let isDirectory = entry.isDirectory();
|
|
25
|
+
if (!isDirectory && entry.isSymbolicLink()) {
|
|
26
|
+
try {
|
|
27
|
+
isDirectory = (await stat(join(cwd, directoryPrefix, entry.name))).isDirectory();
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
isDirectory = false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
suggestions.push({ path: `${directoryPrefix}${entry.name}${isDirectory ? "/" : ""}`, kind: "other" });
|
|
34
|
+
}
|
|
35
|
+
return suggestions
|
|
36
|
+
.sort((a, b) => Number(!a.path.endsWith("/")) - Number(!b.path.endsWith("/")) || a.path.localeCompare(b.path))
|
|
37
|
+
.slice(0, 80);
|
|
38
|
+
}
|
|
39
|
+
async function listGitFiles(cwd) {
|
|
40
|
+
const [tracked, untracked] = await Promise.all([
|
|
41
|
+
git(cwd, ["ls-files"]),
|
|
42
|
+
git(cwd, ["ls-files", "--others", "--exclude-standard"]),
|
|
43
|
+
]);
|
|
44
|
+
return [
|
|
45
|
+
...withDirectories(lines(tracked), "tracked"),
|
|
46
|
+
...withDirectories(lines(untracked), "untracked"),
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
async function listPlainFiles(cwd) {
|
|
50
|
+
const { stdout } = await execFileAsync("rg", ["--files"], { cwd, maxBuffer: 1024 * 1024 * 8 });
|
|
51
|
+
return withDirectories(lines(stdout), "other");
|
|
52
|
+
}
|
|
53
|
+
async function git(cwd, args) {
|
|
54
|
+
const { stdout } = await execFileAsync("git", args, { cwd, maxBuffer: 1024 * 1024 * 8 });
|
|
55
|
+
return stdout;
|
|
56
|
+
}
|
|
57
|
+
function lines(text) {
|
|
58
|
+
return text.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
59
|
+
}
|
|
60
|
+
function withDirectories(paths, kind) {
|
|
61
|
+
const seen = new Set();
|
|
62
|
+
const suggestions = [];
|
|
63
|
+
for (const path of paths) {
|
|
64
|
+
for (const directory of parentDirectories(path))
|
|
65
|
+
add(`${directory}/`);
|
|
66
|
+
add(path);
|
|
67
|
+
}
|
|
68
|
+
return suggestions;
|
|
69
|
+
function add(path) {
|
|
70
|
+
if (seen.has(path))
|
|
71
|
+
return;
|
|
72
|
+
seen.add(path);
|
|
73
|
+
suggestions.push({ path, kind });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function parentDirectories(path) {
|
|
77
|
+
const parts = path.split("/").filter(Boolean);
|
|
78
|
+
const directories = [];
|
|
79
|
+
for (let index = 1; index < parts.length; index++) {
|
|
80
|
+
directories.push(parts.slice(0, index).join("/"));
|
|
81
|
+
}
|
|
82
|
+
return directories;
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=fileSuggestions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileSuggestions.js","sourceRoot":"","sources":["../../../src/server/workspaces/fileSuggestions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { lstat, readdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { resolveInsideWorkspace } from "./pathSafety.js";
|
|
4
|
+
const MAX_ENTRIES = 1000;
|
|
5
|
+
export async function listWorkspaceTree(rootPath, path) {
|
|
6
|
+
const { target, relativePath } = await resolveInsideWorkspace(rootPath, path);
|
|
7
|
+
const stat = await lstat(target);
|
|
8
|
+
if (!stat.isDirectory())
|
|
9
|
+
throw new Error("Path is not a directory");
|
|
10
|
+
const dirents = await readdir(target, { withFileTypes: true });
|
|
11
|
+
const visible = dirents.filter((entry) => entry.name !== ".git" && entry.name !== "node_modules").sort((a, b) => {
|
|
12
|
+
if (a.isDirectory() !== b.isDirectory())
|
|
13
|
+
return a.isDirectory() ? -1 : 1;
|
|
14
|
+
return a.name.localeCompare(b.name);
|
|
15
|
+
});
|
|
16
|
+
const selected = visible.slice(0, MAX_ENTRIES);
|
|
17
|
+
const entries = await Promise.all(selected.map(async (entry) => {
|
|
18
|
+
const absolute = join(target, entry.name);
|
|
19
|
+
const childRelative = relativePath === "" ? entry.name : `${relativePath}/${entry.name}`;
|
|
20
|
+
const childStat = await lstat(absolute);
|
|
21
|
+
const type = entry.isDirectory() ? "directory" : entry.isSymbolicLink() ? "symlink" : "file";
|
|
22
|
+
return { name: entry.name, path: childRelative, type, size: childStat.size, modifiedAt: childStat.mtime.toISOString() };
|
|
23
|
+
}));
|
|
24
|
+
return { path: relativePath, entries, scannedAt: new Date().toISOString(), truncated: visible.length > selected.length };
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=fileTreeService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileTreeService.js","sourceRoot":"","sources":["../../../src/server/workspaces/fileTreeService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,MAAM,WAAW,GAAG,IAAI,CAAC;AAEzB,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,QAAgB,EAAE,IAAwB;IAChF,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,sBAAsB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC9E,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC,CAAC;IACjC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAEpE,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,MAAM,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/D,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QAC9G,IAAI,CAAC,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,WAAW,EAAE;YAAE,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACzE,OAAO,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IACH,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC;IAC/C,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,EAA0B,EAAE;QACrF,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QAC1C,MAAM,aAAa,GAAG,YAAY,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,YAAY,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;QACzF,MAAM,SAAS,GAAG,MAAM,KAAK,CAAC,QAAQ,CAAC,CAAC;QACxC,MAAM,IAAI,GAA0B,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC;QACpH,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE,UAAU,EAAE,SAAS,CAAC,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;IAC1H,CAAC,CAAC,CAAC,CAAC;IAEJ,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC;AAC3H,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
export async function isGitRepository(path) {
|
|
5
|
+
try {
|
|
6
|
+
const { stdout } = await execFileAsync("git", ["-C", path, "rev-parse", "--is-inside-work-tree"]);
|
|
7
|
+
return stdout.trim() === "true";
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export async function discoverGitWorktrees(path) {
|
|
14
|
+
const { stdout } = await execFileAsync("git", ["-C", path, "worktree", "list", "--porcelain"]);
|
|
15
|
+
const chunks = stdout.trim().split(/\n\s*\n/).filter(Boolean);
|
|
16
|
+
return chunks.map((chunk) => {
|
|
17
|
+
const info = { path: "" };
|
|
18
|
+
for (const line of chunk.split("\n")) {
|
|
19
|
+
const [key, ...rest] = line.split(" ");
|
|
20
|
+
const value = rest.join(" ");
|
|
21
|
+
if (key === "worktree")
|
|
22
|
+
info.path = value;
|
|
23
|
+
if (key === "branch")
|
|
24
|
+
info.branch = value.replace(/^refs\/heads\//, "");
|
|
25
|
+
if (key === "bare")
|
|
26
|
+
info.bare = true;
|
|
27
|
+
if (key === "detached")
|
|
28
|
+
info.detached = true;
|
|
29
|
+
}
|
|
30
|
+
return info;
|
|
31
|
+
}).filter((w) => w.path);
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=gitWorktreeDiscovery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gitWorktreeDiscovery.js","sourceRoot":"","sources":["../../../src/server/workspaces/gitWorktreeDiscovery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,MAAM,aAAa,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAS1C,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAY;IAChD,IAAI,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,aAAa,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,uBAAuB,CAAC,CAAC,CAAC;QAClG,OAAO,MAAM,CAAC,IAAI,EAAE,KAAK,MAAM,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,IAAY;IACrD,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,aAAa,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC;IAC/F,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAE9D,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;QAC1B,MAAM,IAAI,GAAoB,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;QAC3C,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACrC,MAAM,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACvC,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAC7B,IAAI,GAAG,KAAK,UAAU;gBAAE,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC;YAC1C,IAAI,GAAG,KAAK,QAAQ;gBAAE,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAC;YACxE,IAAI,GAAG,KAAK,MAAM;gBAAE,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;YACrC,IAAI,GAAG,KAAK,UAAU;gBAAE,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QAC/C,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AAC3B,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, join, relative, sep } from "node:path";
|
|
3
|
+
export async function resolveInsideWorkspace(rootPath, relativePath) {
|
|
4
|
+
const requested = normalizeRelativePath(relativePath);
|
|
5
|
+
const root = await realpath(rootPath);
|
|
6
|
+
const joined = join(root, requested);
|
|
7
|
+
const target = await realpath(joined);
|
|
8
|
+
ensureInside(root, target);
|
|
9
|
+
return { root, target, relativePath: requested };
|
|
10
|
+
}
|
|
11
|
+
export async function resolveParentInsideWorkspace(rootPath, relativePath) {
|
|
12
|
+
const requested = normalizeRelativePath(relativePath);
|
|
13
|
+
const root = await realpath(rootPath);
|
|
14
|
+
const target = join(root, requested);
|
|
15
|
+
ensureInside(root, target);
|
|
16
|
+
return { root, target, relativePath: requested };
|
|
17
|
+
}
|
|
18
|
+
export function normalizeRelativePath(input) {
|
|
19
|
+
const value = input ?? "";
|
|
20
|
+
if (value === "" || value === ".")
|
|
21
|
+
return "";
|
|
22
|
+
if (isAbsolute(value))
|
|
23
|
+
throw new Error("Absolute paths are not allowed");
|
|
24
|
+
const parts = value.split(/[\\/]+/).filter((part) => part !== "" && part !== ".");
|
|
25
|
+
if (parts.some((part) => part === ".."))
|
|
26
|
+
throw new Error("Path traversal is not allowed");
|
|
27
|
+
return parts.join("/");
|
|
28
|
+
}
|
|
29
|
+
function ensureInside(root, target) {
|
|
30
|
+
const rel = relative(root, target);
|
|
31
|
+
if (rel === "")
|
|
32
|
+
return;
|
|
33
|
+
if (rel.startsWith("..") || isAbsolute(rel))
|
|
34
|
+
throw new Error("Path escapes workspace");
|
|
35
|
+
if (sep !== "/" && rel.split(sep).includes(".."))
|
|
36
|
+
throw new Error("Path escapes workspace");
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=pathSafety.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pathSafety.js","sourceRoot":"","sources":["../../../src/server/workspaces/pathSafety.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAE5D,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,QAAgB,EAAE,YAAgC;IAC7F,MAAM,SAAS,GAAG,qBAAqB,CAAC,YAAY,CAAC,CAAC;IACtD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACtC,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IACrC,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,CAAC;IACtC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC3B,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,SAAS,EAAE,CAAC;AACnD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,4BAA4B,CAAC,QAAgB,EAAE,YAAoB;IACvF,MAAM,SAAS,GAAG,qBAAqB,CAAC,YAAY,CAAC,CAAC;IACtD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACtC,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IACrC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC3B,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,SAAS,EAAE,CAAC;AACnD,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,KAAyB;IAC7D,MAAM,KAAK,GAAG,KAAK,IAAI,EAAE,CAAC;IAC1B,IAAI,KAAK,KAAK,EAAE,IAAI,KAAK,KAAK,GAAG;QAAE,OAAO,EAAE,CAAC;IAC7C,IAAI,UAAU,CAAC,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;IACzE,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,EAAE,IAAI,IAAI,KAAK,GAAG,CAAC,CAAC;IAClF,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,IAAI,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;IAC1F,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACzB,CAAC;AAED,SAAS,YAAY,CAAC,IAAY,EAAE,MAAc;IAChD,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACnC,IAAI,GAAG,KAAK,EAAE;QAAE,OAAO;IACvB,IAAI,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,GAAG,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC;IACvF,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC;AAC9F,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export async function resolveWorkspaceContext(projects, workspaces, projectId, workspaceId) {
|
|
2
|
+
const project = await projects.requireProject(projectId);
|
|
3
|
+
const workspace = (await workspaces.list(project)).find((candidate) => candidate.id === workspaceId);
|
|
4
|
+
if (!workspace)
|
|
5
|
+
throw new Error("Workspace not found");
|
|
6
|
+
return { project, workspace, root: workspace.path };
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=workspaceContext.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspaceContext.js","sourceRoot":"","sources":["../../../src/server/workspaces/workspaceContext.ts"],"names":[],"mappings":"AAUA,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,QAAwB,EAAE,UAA4B,EAAE,SAAiB,EAAE,WAAmB;IAC1I,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC;IACzD,MAAM,SAAS,GAAG,CAAC,MAAM,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,EAAE,KAAK,WAAW,CAAC,CAAC;IACrG,IAAI,CAAC,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;IACvD,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE,CAAC;AACtD,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { discoverGitWorktrees, isGitRepository } from "./gitWorktreeDiscovery.js";
|
|
3
|
+
const idFor = (value) => createHash("sha1").update(value).digest("hex").slice(0, 12);
|
|
4
|
+
export class WorkspaceService {
|
|
5
|
+
async list(project) {
|
|
6
|
+
const isGitRepo = await isGitRepository(project.path);
|
|
7
|
+
if (!isGitRepo) {
|
|
8
|
+
return [this.single(project, false)];
|
|
9
|
+
}
|
|
10
|
+
const worktrees = await discoverGitWorktrees(project.path);
|
|
11
|
+
if (worktrees.length === 0)
|
|
12
|
+
return [this.single(project, true)];
|
|
13
|
+
return worktrees.map((worktree) => {
|
|
14
|
+
const leafName = worktree.path.split("/").filter((part) => part !== "").at(-1);
|
|
15
|
+
return {
|
|
16
|
+
id: idFor(`${project.id}:${worktree.path}`),
|
|
17
|
+
projectId: project.id,
|
|
18
|
+
path: worktree.path,
|
|
19
|
+
label: worktree.branch ?? (worktree.detached === true ? "detached" : leafName ?? worktree.path),
|
|
20
|
+
...(worktree.branch === undefined ? {} : { branch: worktree.branch }),
|
|
21
|
+
isMain: worktree.path === project.path,
|
|
22
|
+
isGitRepo: true,
|
|
23
|
+
isGitWorktree: true,
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
single(project, isGitRepo) {
|
|
28
|
+
return {
|
|
29
|
+
id: idFor(`${project.id}:${project.path}`),
|
|
30
|
+
projectId: project.id,
|
|
31
|
+
path: project.path,
|
|
32
|
+
label: project.name,
|
|
33
|
+
isMain: true,
|
|
34
|
+
isGitRepo,
|
|
35
|
+
isGitWorktree: false,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=workspaceService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspaceService.js","sourceRoot":"","sources":["../../../src/server/workspaces/workspaceService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAGzC,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAElF,MAAM,KAAK,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAE7F,MAAM,OAAO,gBAAgB;IAC3B,KAAK,CAAC,IAAI,CAAC,OAAgB;QACzB,MAAM,SAAS,GAAG,MAAM,eAAe,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACtD,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;QACvC,CAAC;QAED,MAAM,SAAS,GAAG,MAAM,oBAAoB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC3D,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC;QAEhE,OAAO,SAAS,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE;YAChC,MAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;YAC/E,OAAO;gBACL,EAAE,EAAE,KAAK,CAAC,GAAG,OAAO,CAAC,EAAE,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC;gBAC3C,SAAS,EAAE,OAAO,CAAC,EAAE;gBACrB,IAAI,EAAE,QAAQ,CAAC,IAAI;gBACnB,KAAK,EAAE,QAAQ,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ,IAAI,QAAQ,CAAC,IAAI,CAAC;gBAC/F,GAAG,CAAC,QAAQ,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC;gBACrE,MAAM,EAAE,QAAQ,CAAC,IAAI,KAAK,OAAO,CAAC,IAAI;gBACtC,SAAS,EAAE,IAAI;gBACf,aAAa,EAAE,IAAI;aACpB,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,MAAM,CAAC,OAAgB,EAAE,SAAkB;QACjD,OAAO;YACL,EAAE,EAAE,KAAK,CAAC,GAAG,OAAO,CAAC,EAAE,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;YAC1C,SAAS,EAAE,OAAO,CAAC,EAAE;YACrB,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,KAAK,EAAE,OAAO,CAAC,IAAI;YACnB,MAAM,EAAE,IAAI;YACZ,SAAS;YACT,aAAa,EAAE,KAAK;SACrB,CAAC;IACJ,CAAC;CACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apiTypes.js","sourceRoot":"","sources":["../../src/shared/apiTypes.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
8
|
+
const cliPath = join(packageRoot, "dist", "cli.js");
|
|
9
|
+
const serverPath = join(packageRoot, "dist", "server", "index.js");
|
|
10
|
+
const sessiondPath = join(packageRoot, "dist", "server", "sessiond.js");
|
|
11
|
+
const serviceNames = ["pi-web-sessiond.service", "pi-web.service"];
|
|
12
|
+
|
|
13
|
+
const subcommands = [
|
|
14
|
+
"install",
|
|
15
|
+
"status",
|
|
16
|
+
"logs",
|
|
17
|
+
"restart",
|
|
18
|
+
"start",
|
|
19
|
+
"stop",
|
|
20
|
+
"doctor",
|
|
21
|
+
"uninstall",
|
|
22
|
+
"open",
|
|
23
|
+
"help",
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
type Subcommand = (typeof subcommands)[number];
|
|
27
|
+
|
|
28
|
+
function shellSingleQuote(value: string): string {
|
|
29
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function nodeCommand(scriptPath: string): string {
|
|
33
|
+
return `${shellSingleQuote(process.execPath)} ${shellSingleQuote(scriptPath)}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseArgs(args: string): string[] {
|
|
37
|
+
return args.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g)?.map((part) => {
|
|
38
|
+
if ((part.startsWith('"') && part.endsWith('"')) || (part.startsWith("'") && part.endsWith("'"))) {
|
|
39
|
+
return part.slice(1, -1);
|
|
40
|
+
}
|
|
41
|
+
return part;
|
|
42
|
+
}) ?? [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function truncateOutput(output: string): string {
|
|
46
|
+
const trimmed = output.trim();
|
|
47
|
+
if (trimmed.length <= 3_500) return trimmed;
|
|
48
|
+
return `${trimmed.slice(0, 3_500)}\n… output truncated`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function run(command: string, args: string[], env: NodeJS.ProcessEnv = {}): Promise<{ code: number; output: string }> {
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
const child = spawn(command, args, {
|
|
54
|
+
env: { ...process.env, ...env },
|
|
55
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
let output = "";
|
|
59
|
+
child.stdout.setEncoding("utf8");
|
|
60
|
+
child.stderr.setEncoding("utf8");
|
|
61
|
+
child.stdout.on("data", (chunk: string) => { output += chunk; });
|
|
62
|
+
child.stderr.on("data", (chunk: string) => { output += chunk; });
|
|
63
|
+
child.on("error", (error) => {
|
|
64
|
+
resolve({ code: 1, output: error.message });
|
|
65
|
+
});
|
|
66
|
+
child.on("close", (code) => {
|
|
67
|
+
resolve({ code: code ?? 1, output });
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function runPiWeb(args: string[], env: NodeJS.ProcessEnv = {}): Promise<{ code: number; output: string }> {
|
|
73
|
+
if (existsSync(cliPath)) {
|
|
74
|
+
return run(process.execPath, [cliPath, ...args], env);
|
|
75
|
+
}
|
|
76
|
+
return run("pi-web", args, env);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function showResult(ctx: { ui: { notify(message: string, type?: "info" | "warning" | "error" | "success"): void } }, title: string, result: { code: number; output: string }): void {
|
|
80
|
+
const body = truncateOutput(result.output) || (result.code === 0 ? "Done." : `Command failed with exit code ${String(result.code)}.`);
|
|
81
|
+
ctx.ui.notify(`${title}\n\n${body}`, result.code === 0 ? "info" : "error");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isSubcommand(value: string): value is Subcommand {
|
|
85
|
+
return subcommands.some((command) => command === value);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function installEnv(): NodeJS.ProcessEnv {
|
|
89
|
+
if (!existsSync(serverPath) || !existsSync(sessiondPath)) return {};
|
|
90
|
+
return {
|
|
91
|
+
PI_WEB_SERVER_EXEC: nodeCommand(serverPath),
|
|
92
|
+
PI_WEB_SESSIOND_EXEC: nodeCommand(sessiondPath),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function boundedLogs(): Promise<{ code: number; output: string }> {
|
|
97
|
+
return run("journalctl", ["--user", "-u", serviceNames[0] ?? "", "-u", serviceNames[1] ?? "", "-n", "100", "--no-pager"]);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export default function piWebExtension(pi: ExtensionAPI): void {
|
|
101
|
+
pi.registerCommand("pi-web", {
|
|
102
|
+
description: "Manage Pi Web services: install, status, logs, restart, start, stop, doctor, open",
|
|
103
|
+
getArgumentCompletions(prefix: string): { value: string; label: string }[] | null {
|
|
104
|
+
const [first = ""] = parseArgs(prefix);
|
|
105
|
+
const items = subcommands
|
|
106
|
+
.filter((command) => command.startsWith(first))
|
|
107
|
+
.map((command) => ({ value: command, label: command }));
|
|
108
|
+
return items.length > 0 ? items : null;
|
|
109
|
+
},
|
|
110
|
+
async handler(args, ctx) {
|
|
111
|
+
const parsedArgs = parseArgs(args);
|
|
112
|
+
const subcommand = parsedArgs[0] ?? "help";
|
|
113
|
+
const rest = parsedArgs.slice(1);
|
|
114
|
+
|
|
115
|
+
if (subcommand === "help") {
|
|
116
|
+
ctx.ui.notify(`Pi Web commands:\n\n${subcommands.map((command) => `/pi-web ${command}`).join("\n")}\n\nLogs are bounded to the last 100 journal lines in the Pi command. Use \`pi-web logs\` in a shell to follow logs.`, "info");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (subcommand === "open") {
|
|
121
|
+
ctx.ui.notify("Pi Web default URL: http://127.0.0.1:8504", "info");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!isSubcommand(subcommand)) {
|
|
126
|
+
ctx.ui.notify(`Unknown pi-web command: ${subcommand}. Try /pi-web help.`, "error");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (subcommand === "stop" || subcommand === "uninstall") {
|
|
131
|
+
const ok = await ctx.ui.confirm(`pi-web ${subcommand}`, `Run pi-web ${subcommand}?`);
|
|
132
|
+
if (!ok) return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (subcommand === "logs") {
|
|
136
|
+
showResult(ctx, "pi-web logs", await boundedLogs());
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const env = subcommand === "install" ? installEnv() : {};
|
|
141
|
+
showResult(ctx, `pi-web ${subcommand}`, await runPiWeb([subcommand, ...rest], env));
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
}
|