@pasko70/pibo 1.2.0 → 1.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/README.md +9 -0
- package/dist/apps/chat/static-assets.js +1 -1
- package/dist/apps/chat-ui/assets/{dist-CrBzZkUp.js → dist-7YaJd19a.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DfcIkFSN.js → dist-8Noo5eCN.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-B4lw9z4F.js → dist-BIvPnn_C.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DGLWQLX8.js → dist-BbNE72h8.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CGINd7XU.js → dist-CVQU42Fn.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CyyISotB.js → dist-ChSZNqKE.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-y9D6la48.js → dist-CiYeO8nN.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DFbnFNpX.js → dist-CjSI6y5z.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BcT_c1EJ.js → dist-D7TCkoFT.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-sKDHTwaT.js → dist-KXCMNKIL.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BbTaHWON.js → dist-Ubstha8t.js} +1 -1
- package/dist/apps/chat-ui/assets/{index-CUkuvI3v.js → index-CmxtUVG1.js} +3 -3
- package/dist/apps/chat-ui/index.html +1 -1
- package/dist/apps/chat-vscode-web/assets/index-B5QK07zO.css +2 -0
- package/dist/apps/chat-vscode-web/assets/index-C3GTPyDo.js +41 -0
- package/dist/apps/chat-vscode-web/index.html +14 -0
- package/dist/apps/vscode-artifacts/latest.vsix +0 -0
- package/dist/apps/vscode-artifacts/pibo-vscode-1.3.0.vsix +0 -0
- package/dist/apps/vscode-artifacts/pibo-vscode-1.3.1.vsix +0 -0
- package/dist/cli.js +32 -1
- package/dist/config/config.js +9 -0
- package/dist/core/context-build.js +36 -2
- package/dist/core/wsl.js +100 -0
- package/dist/gateway/web.js +51 -11
- package/dist/plugins/dev-auth.js +26 -0
- package/dist/setup/cli.js +60 -8
- package/dist/vscode/cli.js +90 -0
- package/dist/vscode/code-cli.js +119 -0
- package/dist/vscode/install.js +177 -0
- package/dist/vscode/status.js +80 -0
- package/dist/vscode/types.js +14 -0
- package/dist/vscode/uninstall.js +49 -0
- package/dist/vscode/vsix-fetcher.js +131 -0
- package/dist/web/channel.js +47 -1
- package/dist/web/http.js +6 -3
- package/docs/ops/install-user-host.md +21 -1
- package/docs/ops/vscode-extension-release.md +160 -0
- package/package.json +6 -4
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detection of the VS Code CLI binary and wrapper around it.
|
|
3
|
+
*
|
|
4
|
+
* The `code` binary is what VS Code installs on `$PATH` when the user enables
|
|
5
|
+
* "Shell Command: Install 'code' command in PATH" from the VS Code command
|
|
6
|
+
* palette. It is the only supported way for `pibo vscode install` to install
|
|
7
|
+
* the extension. If no supported binary is on `$PATH`, install fails with a
|
|
8
|
+
* clear error pointing at https://code.visualstudio.com/.
|
|
9
|
+
*/
|
|
10
|
+
import { spawn as defaultSpawn } from "node:child_process";
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { delimiter, join } from "node:path";
|
|
13
|
+
export const SUPPORTED_CODE_BINARIES = ["code", "code-insiders", "codium"];
|
|
14
|
+
function splitPathEnv(pathValue) {
|
|
15
|
+
if (!pathValue)
|
|
16
|
+
return [];
|
|
17
|
+
return pathValue.split(delimiter).filter((entry) => entry.length > 0);
|
|
18
|
+
}
|
|
19
|
+
function firstExisting(candidates) {
|
|
20
|
+
for (const candidate of candidates) {
|
|
21
|
+
if (existsSync(candidate))
|
|
22
|
+
return candidate;
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
export function detectCodeBinary(options = {}) {
|
|
27
|
+
const pathValue = options.path ?? options.env?.PATH ?? process.env.PATH ?? "";
|
|
28
|
+
const directories = splitPathEnv(pathValue);
|
|
29
|
+
for (const binary of SUPPORTED_CODE_BINARIES) {
|
|
30
|
+
const candidates = directories.map((dir) => join(dir, binary));
|
|
31
|
+
const found = firstExisting(candidates);
|
|
32
|
+
if (found)
|
|
33
|
+
return { binary, path: found };
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
const COLLECT_BUFFER_LIMIT = 1024 * 1024; // 1 MiB
|
|
38
|
+
export function runCodeCommand(options) {
|
|
39
|
+
const spawnImpl = options.spawnImpl ?? defaultSpawn;
|
|
40
|
+
const env = options.env ?? process.env;
|
|
41
|
+
return new Promise((resolveInvocation, rejectInvocation) => {
|
|
42
|
+
let child;
|
|
43
|
+
try {
|
|
44
|
+
child = spawnImpl(options.binary, [...options.args], {
|
|
45
|
+
env,
|
|
46
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
rejectInvocation(error instanceof Error ? error : new Error(String(error)));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const stdoutChunks = [];
|
|
54
|
+
const stderrChunks = [];
|
|
55
|
+
let stdoutLength = 0;
|
|
56
|
+
let stderrLength = 0;
|
|
57
|
+
let timedOut = false;
|
|
58
|
+
const timeoutHandle = typeof options.timeoutMs === "number"
|
|
59
|
+
? setTimeout(() => {
|
|
60
|
+
timedOut = true;
|
|
61
|
+
child.kill("SIGKILL");
|
|
62
|
+
}, options.timeoutMs)
|
|
63
|
+
: null;
|
|
64
|
+
child.stdout?.on("data", (chunk) => {
|
|
65
|
+
stdoutLength += chunk.length;
|
|
66
|
+
if (stdoutLength <= COLLECT_BUFFER_LIMIT)
|
|
67
|
+
stdoutChunks.push(chunk);
|
|
68
|
+
});
|
|
69
|
+
child.stderr?.on("data", (chunk) => {
|
|
70
|
+
stderrLength += chunk.length;
|
|
71
|
+
if (stderrLength <= COLLECT_BUFFER_LIMIT)
|
|
72
|
+
stderrChunks.push(chunk);
|
|
73
|
+
});
|
|
74
|
+
child.on("error", (error) => {
|
|
75
|
+
if (timeoutHandle)
|
|
76
|
+
clearTimeout(timeoutHandle);
|
|
77
|
+
rejectInvocation(error);
|
|
78
|
+
});
|
|
79
|
+
child.on("close", (code) => {
|
|
80
|
+
if (timeoutHandle)
|
|
81
|
+
clearTimeout(timeoutHandle);
|
|
82
|
+
if (timedOut) {
|
|
83
|
+
rejectInvocation(new Error(`code command timed out after ${options.timeoutMs}ms`));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
resolveInvocation({
|
|
87
|
+
stdout: Buffer.concat(stdoutChunks).toString("utf8"),
|
|
88
|
+
stderr: Buffer.concat(stderrChunks).toString("utf8"),
|
|
89
|
+
exitCode: code ?? -1,
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
export async function listInstalledExtensions(options) {
|
|
95
|
+
const result = await runCodeCommand({
|
|
96
|
+
binary: options.binary,
|
|
97
|
+
args: ["--list-extensions", "--show-versions"],
|
|
98
|
+
spawnImpl: options.spawnImpl,
|
|
99
|
+
env: options.env,
|
|
100
|
+
timeoutMs: 30_000,
|
|
101
|
+
});
|
|
102
|
+
if (result.exitCode !== 0) {
|
|
103
|
+
throw new Error(`code --list-extensions failed: ${result.stderr || result.stdout || `exit ${result.exitCode}`}`);
|
|
104
|
+
}
|
|
105
|
+
const entries = [];
|
|
106
|
+
for (const rawLine of result.stdout.split(/\r?\n/)) {
|
|
107
|
+
const line = rawLine.trim();
|
|
108
|
+
if (!line)
|
|
109
|
+
continue;
|
|
110
|
+
const atIndex = line.lastIndexOf("@");
|
|
111
|
+
if (atIndex > 0) {
|
|
112
|
+
entries.push({ id: line.slice(0, atIndex), version: line.slice(atIndex + 1) });
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
entries.push({ id: line });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return entries;
|
|
119
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pibo vscode install` — install the Pibo VS Code extension.
|
|
3
|
+
*
|
|
4
|
+
* Resolution order for the VSIX artifact:
|
|
5
|
+
* 1. `--vsix <path>` → use the local file directly
|
|
6
|
+
* 2. `--from-url <url>` → download from the given URL
|
|
7
|
+
* 3. default → fetch the latest GitHub Release for the repo and
|
|
8
|
+
* use its `.vsix` asset (optionally pinned by
|
|
9
|
+
* `--version <tag>`)
|
|
10
|
+
*
|
|
11
|
+
* The resolved VSIX is cached under `~/.pibo/vscode/cache/<tagName>/` so
|
|
12
|
+
* repeated installs do not re-download.
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { join, resolve } from "node:path";
|
|
17
|
+
import { detectCodeBinary, listInstalledExtensions, runCodeCommand } from "./code-cli.js";
|
|
18
|
+
import { downloadVsixAsset, fetchLatestVsix, isVsixAsset } from "./vsix-fetcher.js";
|
|
19
|
+
import { DEFAULT_GITHUB_OWNER, DEFAULT_GITHUB_REPO, PIBO_VSCODE_CACHE_DIR, PIBO_VSCODE_EXTENSION_ID, } from "./types.js";
|
|
20
|
+
import { findVsixAsset, fetchRelease } from "./vsix-fetcher.js";
|
|
21
|
+
const DEFAULT_LOG = (message) => {
|
|
22
|
+
console.log(message);
|
|
23
|
+
};
|
|
24
|
+
const DEFAULT_ERROR = (message) => {
|
|
25
|
+
process.stderr.write(`${message}\n`);
|
|
26
|
+
};
|
|
27
|
+
function getCacheDir(options) {
|
|
28
|
+
if (options.cacheDir)
|
|
29
|
+
return options.cacheDir;
|
|
30
|
+
const base = options.env?.PIBO_HOME ?? join(homedir(), ".pibo");
|
|
31
|
+
return join(base, PIBO_VSCODE_CACHE_DIR, "cache");
|
|
32
|
+
}
|
|
33
|
+
function cacheKeyForVersion(tagName) {
|
|
34
|
+
return tagName.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
35
|
+
}
|
|
36
|
+
function readCachedVsix(cacheDir, tagName) {
|
|
37
|
+
const dir = join(cacheDir, cacheKeyForVersion(tagName));
|
|
38
|
+
const path = join(dir, "pibo.vsix");
|
|
39
|
+
return existsSync(path) ? path : undefined;
|
|
40
|
+
}
|
|
41
|
+
function writeCachedVsix(cacheDir, tagName, bytes) {
|
|
42
|
+
const dir = join(cacheDir, cacheKeyForVersion(tagName));
|
|
43
|
+
mkdirSync(dir, { recursive: true });
|
|
44
|
+
const path = join(dir, "pibo.vsix");
|
|
45
|
+
writeFileSync(path, bytes);
|
|
46
|
+
return path;
|
|
47
|
+
}
|
|
48
|
+
function cachedTagManifestPath(cacheDir) {
|
|
49
|
+
return join(cacheDir, "last-installed.json");
|
|
50
|
+
}
|
|
51
|
+
function readCachedTagManifest(cacheDir) {
|
|
52
|
+
const path = cachedTagManifestPath(cacheDir);
|
|
53
|
+
if (!existsSync(path))
|
|
54
|
+
return undefined;
|
|
55
|
+
try {
|
|
56
|
+
const json = JSON.parse(readFileSync(path, "utf8"));
|
|
57
|
+
return json;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function writeCachedTagManifest(cacheDir, tagName, vsixPath) {
|
|
64
|
+
const path = cachedTagManifestPath(cacheDir);
|
|
65
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
66
|
+
writeFileSync(path, JSON.stringify({ tagName, vsixPath, installedAt: new Date().toISOString() }, null, 2));
|
|
67
|
+
}
|
|
68
|
+
export async function resolveVsixArtifact(options) {
|
|
69
|
+
if (options.vsixPath) {
|
|
70
|
+
const absolute = resolve(options.vsixPath);
|
|
71
|
+
if (!existsSync(absolute)) {
|
|
72
|
+
throw new Error(`VSIX file not found at ${absolute}`);
|
|
73
|
+
}
|
|
74
|
+
return { tagName: "local", vsixPath: absolute };
|
|
75
|
+
}
|
|
76
|
+
if (!options.skipCache && !options.version) {
|
|
77
|
+
const cachedTag = readCachedTagManifest(options.cacheDir);
|
|
78
|
+
if (cachedTag?.tagName && cachedTag.vsixPath && existsSync(cachedTag.vsixPath)) {
|
|
79
|
+
return { tagName: cachedTag.tagName, vsixPath: cachedTag.vsixPath };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (options.fromUrl) {
|
|
83
|
+
const release = await fetchRelease({ owner: options.owner, repo: options.repo, fetchImpl: options.fetchImpl });
|
|
84
|
+
const asset = findVsixAsset(release) ?? { name: "remote.vsix", browserDownloadUrl: options.fromUrl, size: 0, contentType: "application/octet-stream" };
|
|
85
|
+
const bytes = await downloadVsixAsset({ url: options.fromUrl, fetchImpl: options.fetchImpl });
|
|
86
|
+
const cachedPath = writeCachedVsix(options.cacheDir, release.tagName, bytes);
|
|
87
|
+
return { tagName: release.tagName, vsixPath: cachedPath, bytes };
|
|
88
|
+
}
|
|
89
|
+
const result = await fetchLatestVsix({
|
|
90
|
+
owner: options.owner,
|
|
91
|
+
repo: options.repo,
|
|
92
|
+
tagName: options.version,
|
|
93
|
+
fetchImpl: options.fetchImpl,
|
|
94
|
+
});
|
|
95
|
+
if (!options.skipCache) {
|
|
96
|
+
const cached = readCachedVsix(options.cacheDir, result.tagName);
|
|
97
|
+
if (cached)
|
|
98
|
+
return { tagName: result.tagName, vsixPath: cached };
|
|
99
|
+
}
|
|
100
|
+
const cachedPath = writeCachedVsix(options.cacheDir, result.tagName, result.bytes);
|
|
101
|
+
return { tagName: result.tagName, vsixPath: cachedPath, bytes: result.bytes };
|
|
102
|
+
}
|
|
103
|
+
export async function runInstall(options) {
|
|
104
|
+
const log = options.log ?? DEFAULT_LOG;
|
|
105
|
+
const errorLog = options.error ?? DEFAULT_ERROR;
|
|
106
|
+
const owner = options.owner ?? DEFAULT_GITHUB_OWNER;
|
|
107
|
+
const repo = options.repo ?? DEFAULT_GITHUB_REPO;
|
|
108
|
+
const cacheDir = getCacheDir(options);
|
|
109
|
+
const detected = detectCodeBinary({ env: options.env });
|
|
110
|
+
if (!detected) {
|
|
111
|
+
const message = `No VS Code CLI found on PATH. Install VS Code (https://code.visualstudio.com/) and run 'Shell Command: Install code command in PATH' from the VS Code command palette.`;
|
|
112
|
+
errorLog(message);
|
|
113
|
+
return { status: "failed", reason: "no-code-cli" };
|
|
114
|
+
}
|
|
115
|
+
let artifact;
|
|
116
|
+
try {
|
|
117
|
+
artifact = await resolveVsixArtifact({
|
|
118
|
+
vsixPath: options.vsixPath,
|
|
119
|
+
fromUrl: options.fromUrl,
|
|
120
|
+
version: options.version,
|
|
121
|
+
owner,
|
|
122
|
+
repo,
|
|
123
|
+
fetchImpl: options.fetchImpl,
|
|
124
|
+
cacheDir,
|
|
125
|
+
skipCache: options.skipCache,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
catch (fetchError) {
|
|
129
|
+
const reason = fetchError instanceof Error ? fetchError.message : String(fetchError);
|
|
130
|
+
errorLog(`Failed to obtain VSIX: ${reason}`);
|
|
131
|
+
return { status: "failed", reason };
|
|
132
|
+
}
|
|
133
|
+
log(`Installing pibo VS Code extension ${artifact.tagName} from ${artifact.vsixPath} via ${detected.path}…`);
|
|
134
|
+
let installResult;
|
|
135
|
+
try {
|
|
136
|
+
installResult = await runCodeCommand({
|
|
137
|
+
binary: detected.path,
|
|
138
|
+
args: ["--install-extension", artifact.vsixPath, "--force"],
|
|
139
|
+
spawnImpl: options.spawnImpl,
|
|
140
|
+
env: options.env,
|
|
141
|
+
timeoutMs: 120_000,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
catch (spawnError) {
|
|
145
|
+
const reason = spawnError instanceof Error ? spawnError.message : String(spawnError);
|
|
146
|
+
errorLog(`Failed to invoke ${detected.binary}: ${reason}`);
|
|
147
|
+
return { status: "failed", reason, codeBinary: detected.path, tagName: artifact.tagName };
|
|
148
|
+
}
|
|
149
|
+
if (installResult.exitCode !== 0) {
|
|
150
|
+
const reason = `code --install-extension exited with code ${installResult.exitCode}: ${installResult.stderr || installResult.stdout}`;
|
|
151
|
+
errorLog(reason);
|
|
152
|
+
return { status: "failed", reason, codeBinary: detected.path, tagName: artifact.tagName };
|
|
153
|
+
}
|
|
154
|
+
writeCachedTagManifest(cacheDir, artifact.tagName, artifact.vsixPath);
|
|
155
|
+
try {
|
|
156
|
+
const installed = await listInstalledExtensions({
|
|
157
|
+
binary: detected.path,
|
|
158
|
+
spawnImpl: options.spawnImpl,
|
|
159
|
+
env: options.env,
|
|
160
|
+
});
|
|
161
|
+
const ours = installed.find((entry) => entry.id === PIBO_VSCODE_EXTENSION_ID);
|
|
162
|
+
if (ours) {
|
|
163
|
+
log(`Installed ${ours.id}@${ours.version ?? "?"} via ${detected.path}`);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
log(`code --install-extension reported success but ${PIBO_VSCODE_EXTENSION_ID} is not in the installed list.`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch (listError) {
|
|
170
|
+
// Listing the installed extensions is a best-effort check; do not fail the install on a listing error.
|
|
171
|
+
const reason = listError instanceof Error ? listError.message : String(listError);
|
|
172
|
+
log(`Install completed; failed to verify via --list-extensions: ${reason}`);
|
|
173
|
+
}
|
|
174
|
+
return { status: "installed", tagName: artifact.tagName, vsixPath: artifact.vsixPath, codeBinary: detected.path };
|
|
175
|
+
}
|
|
176
|
+
// Re-export for symmetry with code-cli.ts consumers.
|
|
177
|
+
export { isVsixAsset };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pibo vscode status` — report whether the Pibo VS Code extension is
|
|
3
|
+
* installed, the local VS Code CLI binary, and the cached VSIX artifacts.
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { detectCodeBinary, listInstalledExtensions } from "./code-cli.js";
|
|
9
|
+
import { fetchRelease } from "./vsix-fetcher.js";
|
|
10
|
+
import { DEFAULT_GITHUB_OWNER, DEFAULT_GITHUB_REPO, PIBO_VSCODE_CACHE_DIR, PIBO_VSCODE_EXTENSION_ID, } from "./types.js";
|
|
11
|
+
function getCacheDir(options) {
|
|
12
|
+
if (options.cacheDir)
|
|
13
|
+
return options.cacheDir;
|
|
14
|
+
const base = options.env?.PIBO_HOME ?? join(homedir(), ".pibo");
|
|
15
|
+
return join(base, PIBO_VSCODE_CACHE_DIR, "cache");
|
|
16
|
+
}
|
|
17
|
+
function listCachedReleases(cacheDir, limit) {
|
|
18
|
+
if (!existsSync(cacheDir))
|
|
19
|
+
return [];
|
|
20
|
+
const entries = readdirSync(cacheDir, { withFileTypes: true })
|
|
21
|
+
.filter((entry) => entry.isDirectory() && entry.name !== "node_modules")
|
|
22
|
+
.map((entry) => ({ name: entry.name, mtimeMs: statSync(join(cacheDir, entry.name)).mtimeMs }))
|
|
23
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs)
|
|
24
|
+
.slice(0, limit)
|
|
25
|
+
.map((entry) => entry.name);
|
|
26
|
+
return entries;
|
|
27
|
+
}
|
|
28
|
+
export async function runStatus(options = {}) {
|
|
29
|
+
const owner = options.owner ?? DEFAULT_GITHUB_OWNER;
|
|
30
|
+
const repo = options.repo ?? DEFAULT_GITHUB_REPO;
|
|
31
|
+
const cacheDir = getCacheDir(options);
|
|
32
|
+
const limit = options.limit ?? 5;
|
|
33
|
+
const detected = detectCodeBinary({ env: options.env });
|
|
34
|
+
let installed = false;
|
|
35
|
+
let version;
|
|
36
|
+
if (detected) {
|
|
37
|
+
try {
|
|
38
|
+
const installedExtensions = await listInstalledExtensions({
|
|
39
|
+
binary: detected.path,
|
|
40
|
+
spawnImpl: options.spawnImpl,
|
|
41
|
+
env: options.env,
|
|
42
|
+
});
|
|
43
|
+
const ours = installedExtensions.find((entry) => entry.id === PIBO_VSCODE_EXTENSION_ID);
|
|
44
|
+
if (ours) {
|
|
45
|
+
installed = true;
|
|
46
|
+
version = ours.version;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Listing failed; treat as not-installed for status purposes.
|
|
51
|
+
installed = false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const cachedReleases = listCachedReleases(cacheDir, limit);
|
|
55
|
+
let availableReleases = [];
|
|
56
|
+
try {
|
|
57
|
+
const release = await fetchRelease({ owner, repo, fetchImpl: options.fetchImpl });
|
|
58
|
+
availableReleases = [release.tagName];
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Network failure is non-fatal for status.
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
installed,
|
|
65
|
+
version,
|
|
66
|
+
codeBinary: detected?.path,
|
|
67
|
+
vsixCacheDir: cacheDir,
|
|
68
|
+
availableReleases,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function formatStatusText(status) {
|
|
72
|
+
const lines = [];
|
|
73
|
+
lines.push(`extension: ${status.installed ? `${PIBO_VSCODE_EXTENSION_ID}@${status.version ?? "?"}` : "not installed"}`);
|
|
74
|
+
lines.push(`code binary: ${status.codeBinary ?? "(not on PATH)"}`);
|
|
75
|
+
lines.push(`vsix cache: ${status.vsixCacheDir}`);
|
|
76
|
+
if (status.availableReleases.length > 0) {
|
|
77
|
+
lines.push(`latest release: ${status.availableReleases[0]}`);
|
|
78
|
+
}
|
|
79
|
+
return lines.join("\n");
|
|
80
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types and constants for the `pibo vscode` sub-command.
|
|
3
|
+
*
|
|
4
|
+
* The pibo VS Code extension is a thin VS Code client that opens a WebView
|
|
5
|
+
* pointing at the gateway-served chat-vscode-web app. The extension itself
|
|
6
|
+
* ships as a `.vsix` artifact, published to the VS Code Marketplace and
|
|
7
|
+
* mirrored on GitHub Releases.
|
|
8
|
+
*/
|
|
9
|
+
export const PIBO_VSCODE_EXTENSION_PUBLISHER = "pibo";
|
|
10
|
+
export const PIBO_VSCODE_EXTENSION_NAME = "pibo-vscode";
|
|
11
|
+
export const PIBO_VSCODE_EXTENSION_ID = `${PIBO_VSCODE_EXTENSION_PUBLISHER}.${PIBO_VSCODE_EXTENSION_NAME}`;
|
|
12
|
+
export const DEFAULT_GITHUB_OWNER = "Pascapone";
|
|
13
|
+
export const DEFAULT_GITHUB_REPO = "pibo";
|
|
14
|
+
export const PIBO_VSCODE_CACHE_DIR = "vscode";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pibo vscode uninstall` — remove the installed Pibo VS Code extension.
|
|
3
|
+
*/
|
|
4
|
+
import { detectCodeBinary, runCodeCommand } from "./code-cli.js";
|
|
5
|
+
import { PIBO_VSCODE_EXTENSION_ID } from "./types.js";
|
|
6
|
+
const DEFAULT_LOG = (message) => {
|
|
7
|
+
console.log(message);
|
|
8
|
+
};
|
|
9
|
+
const DEFAULT_ERROR = (message) => {
|
|
10
|
+
process.stderr.write(`${message}\n`);
|
|
11
|
+
};
|
|
12
|
+
export async function runUninstall(options = {}) {
|
|
13
|
+
const log = options.log ?? DEFAULT_LOG;
|
|
14
|
+
const errorLog = options.error ?? DEFAULT_ERROR;
|
|
15
|
+
const detected = detectCodeBinary({ env: options.env });
|
|
16
|
+
if (!detected) {
|
|
17
|
+
errorLog(`No VS Code CLI found on PATH.`);
|
|
18
|
+
return { status: "failed", reason: "no-code-cli" };
|
|
19
|
+
}
|
|
20
|
+
log(`Uninstalling ${PIBO_VSCODE_EXTENSION_ID} via ${detected.path}…`);
|
|
21
|
+
let result;
|
|
22
|
+
try {
|
|
23
|
+
result = await runCodeCommand({
|
|
24
|
+
binary: detected.path,
|
|
25
|
+
args: ["--uninstall-extension", PIBO_VSCODE_EXTENSION_ID],
|
|
26
|
+
spawnImpl: options.spawnImpl,
|
|
27
|
+
env: options.env,
|
|
28
|
+
timeoutMs: 60_000,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
catch (spawnError) {
|
|
32
|
+
const reason = spawnError instanceof Error ? spawnError.message : String(spawnError);
|
|
33
|
+
errorLog(`Failed to invoke ${detected.binary}: ${reason}`);
|
|
34
|
+
return { status: "failed", reason, codeBinary: detected.path };
|
|
35
|
+
}
|
|
36
|
+
if (result.exitCode !== 0) {
|
|
37
|
+
const stderr = result.stderr || result.stdout;
|
|
38
|
+
// VS Code returns a non-zero exit when the extension is not installed; treat that as "not-installed".
|
|
39
|
+
if (/not found|not installed|Cannot find extension/i.test(stderr)) {
|
|
40
|
+
log(`${PIBO_VSCODE_EXTENSION_ID} was not installed.`);
|
|
41
|
+
return { status: "not-installed", codeBinary: detected.path };
|
|
42
|
+
}
|
|
43
|
+
const reason = `code --uninstall-extension exited with code ${result.exitCode}: ${stderr}`;
|
|
44
|
+
errorLog(reason);
|
|
45
|
+
return { status: "failed", reason, codeBinary: detected.path };
|
|
46
|
+
}
|
|
47
|
+
log(`Uninstalled ${PIBO_VSCODE_EXTENSION_ID}`);
|
|
48
|
+
return { status: "uninstalled", codeBinary: detected.path };
|
|
49
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Releases interaction for the Pibo VS Code extension.
|
|
3
|
+
*
|
|
4
|
+
* `pibo vscode install` defaults to fetching the latest VSIX asset from a
|
|
5
|
+
* GitHub Release. The fetch path is pure-functional so the install command
|
|
6
|
+
* can inject a mocked fetch and a mocked spawn in tests.
|
|
7
|
+
*/
|
|
8
|
+
export class VsixFetchError extends Error {
|
|
9
|
+
cause;
|
|
10
|
+
constructor(message, options) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "VsixFetchError";
|
|
13
|
+
if (options?.cause !== undefined)
|
|
14
|
+
this.cause = options.cause;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const VSIX_ASSET_PATTERN = /\.vsix$/i;
|
|
18
|
+
export function isVsixAsset(asset) {
|
|
19
|
+
return VSIX_ASSET_PATTERN.test(asset.name);
|
|
20
|
+
}
|
|
21
|
+
export function findVsixAsset(release) {
|
|
22
|
+
return release.assets.find(isVsixAsset);
|
|
23
|
+
}
|
|
24
|
+
const GITHUB_API_ROOT = "https://api.github.com";
|
|
25
|
+
function buildReleaseUrl(owner, repo, tagName) {
|
|
26
|
+
const base = `${GITHUB_API_ROOT}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases`;
|
|
27
|
+
if (tagName)
|
|
28
|
+
return `${base}/tags/${encodeURIComponent(tagName)}`;
|
|
29
|
+
return `${base}/latest`;
|
|
30
|
+
}
|
|
31
|
+
function parseRelease(payload) {
|
|
32
|
+
if (typeof payload !== "object" || payload === null) {
|
|
33
|
+
throw new VsixFetchError("GitHub Releases payload is not an object");
|
|
34
|
+
}
|
|
35
|
+
const record = payload;
|
|
36
|
+
const tagName = record.tag_name;
|
|
37
|
+
const name = record.name;
|
|
38
|
+
const publishedAt = record.published_at;
|
|
39
|
+
const htmlUrl = record.html_url;
|
|
40
|
+
const rawAssets = record.assets;
|
|
41
|
+
if (typeof tagName !== "string" || typeof name !== "string" || typeof publishedAt !== "string" || typeof htmlUrl !== "string") {
|
|
42
|
+
throw new VsixFetchError("GitHub Releases payload is missing required string fields");
|
|
43
|
+
}
|
|
44
|
+
if (!Array.isArray(rawAssets)) {
|
|
45
|
+
throw new VsixFetchError("GitHub Releases payload is missing assets array");
|
|
46
|
+
}
|
|
47
|
+
const assets = [];
|
|
48
|
+
for (const raw of rawAssets) {
|
|
49
|
+
if (typeof raw !== "object" || raw === null)
|
|
50
|
+
continue;
|
|
51
|
+
const r = raw;
|
|
52
|
+
if (typeof r.name === "string" &&
|
|
53
|
+
typeof r.browser_download_url === "string" &&
|
|
54
|
+
typeof r.size === "number" &&
|
|
55
|
+
typeof r.content_type === "string") {
|
|
56
|
+
assets.push({
|
|
57
|
+
name: r.name,
|
|
58
|
+
browserDownloadUrl: r.browser_download_url,
|
|
59
|
+
size: r.size,
|
|
60
|
+
contentType: r.content_type,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { tagName, name, publishedAt, htmlUrl, assets };
|
|
65
|
+
}
|
|
66
|
+
export async function fetchRelease(options) {
|
|
67
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
68
|
+
const url = buildReleaseUrl(options.owner, options.repo, options.tagName);
|
|
69
|
+
let response;
|
|
70
|
+
try {
|
|
71
|
+
response = await fetchImpl(url, {
|
|
72
|
+
headers: { accept: "application/vnd.github+json", "user-agent": "pibo-cli" },
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
throw new VsixFetchError(`Failed to call GitHub Releases API at ${url}`, { cause: error });
|
|
77
|
+
}
|
|
78
|
+
if (response.status === 404) {
|
|
79
|
+
throw new VsixFetchError(options.tagName
|
|
80
|
+
? `GitHub release ${options.owner}/${options.repo}@${options.tagName} not found`
|
|
81
|
+
: `GitHub repository ${options.owner}/${options.repo} has no published releases`);
|
|
82
|
+
}
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
throw new VsixFetchError(`GitHub Releases API returned HTTP ${response.status}`);
|
|
85
|
+
}
|
|
86
|
+
let payload;
|
|
87
|
+
try {
|
|
88
|
+
payload = await response.json();
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
throw new VsixFetchError("GitHub Releases response was not valid JSON", { cause: error });
|
|
92
|
+
}
|
|
93
|
+
return parseRelease(payload);
|
|
94
|
+
}
|
|
95
|
+
export async function downloadVsixAsset(options) {
|
|
96
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
97
|
+
const maxBytes = options.maxBytes ?? 64 * 1024 * 1024; // 64 MiB
|
|
98
|
+
let response;
|
|
99
|
+
try {
|
|
100
|
+
response = await fetchImpl(options.url, { headers: { "user-agent": "pibo-cli" } });
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
throw new VsixFetchError(`Failed to download VSIX from ${options.url}`, { cause: error });
|
|
104
|
+
}
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
throw new VsixFetchError(`VSIX download returned HTTP ${response.status}`);
|
|
107
|
+
}
|
|
108
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
109
|
+
if (arrayBuffer.byteLength > maxBytes) {
|
|
110
|
+
throw new VsixFetchError(`VSIX download is ${arrayBuffer.byteLength} bytes, exceeding limit of ${maxBytes} bytes`);
|
|
111
|
+
}
|
|
112
|
+
return Buffer.from(arrayBuffer);
|
|
113
|
+
}
|
|
114
|
+
export async function fetchLatestVsix(options) {
|
|
115
|
+
const release = await fetchRelease({
|
|
116
|
+
owner: options.owner,
|
|
117
|
+
repo: options.repo,
|
|
118
|
+
tagName: options.tagName,
|
|
119
|
+
fetchImpl: options.fetchImpl,
|
|
120
|
+
});
|
|
121
|
+
const asset = findVsixAsset(release);
|
|
122
|
+
if (!asset) {
|
|
123
|
+
throw new VsixFetchError(`Release ${release.tagName} has no .vsix asset`);
|
|
124
|
+
}
|
|
125
|
+
const bytes = await downloadVsixAsset({
|
|
126
|
+
url: asset.browserDownloadUrl,
|
|
127
|
+
maxBytes: options.maxBytes,
|
|
128
|
+
fetchImpl: options.fetchImpl,
|
|
129
|
+
});
|
|
130
|
+
return { tagName: release.tagName, asset, bytes };
|
|
131
|
+
}
|
package/dist/web/channel.js
CHANGED
|
@@ -6,6 +6,15 @@ import { PiboWebHttpError, nodeRequestToWebRequest, responseHtml, responseJson,
|
|
|
6
6
|
export const DEFAULT_WEB_CHANNEL_HOST = "127.0.0.1";
|
|
7
7
|
export const DEFAULT_WEB_CHANNEL_PORT = 4788;
|
|
8
8
|
export const WEB_CHANNEL_NAME = "web-host";
|
|
9
|
+
/**
|
|
10
|
+
* Internal header that carries the TCP socket peer address from the web host
|
|
11
|
+
* channel to the auth plugin. This is one of three independent safety layers
|
|
12
|
+
* for the local auth service (see `docs/specs/capabilities/web-auth-and-same-origin-host.md`
|
|
13
|
+
* REQ-010). The header is added on the request side by the channel and MUST
|
|
14
|
+
* be stripped from any response by `sendWebResponse` so it never reaches the
|
|
15
|
+
* browser.
|
|
16
|
+
*/
|
|
17
|
+
export const SOCKET_PEER_HEADER = "x-pibo-socket-peer";
|
|
9
18
|
function redirect(location) {
|
|
10
19
|
return new Response(null, {
|
|
11
20
|
status: 302,
|
|
@@ -33,6 +42,41 @@ function firstHeaderValue(value) {
|
|
|
33
42
|
function isLoopbackAddress(address) {
|
|
34
43
|
return address === "::1" || address === "127.0.0.1" || address?.startsWith("127.") === true || address?.startsWith("::ffff:127.") === true;
|
|
35
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Return a new Request that includes the TCP socket peer address in the
|
|
47
|
+
* `x-pibo-socket-peer` header. The body is preserved via the request body
|
|
48
|
+
* stream consumed into a buffer because the original Request is not cloneable
|
|
49
|
+
* once the body has been read.
|
|
50
|
+
*/
|
|
51
|
+
function withSocketPeerHeader(request, peerAddress) {
|
|
52
|
+
const headers = new Headers(request.headers);
|
|
53
|
+
if (peerAddress)
|
|
54
|
+
headers.set(SOCKET_PEER_HEADER, peerAddress);
|
|
55
|
+
return new Request(request.url, {
|
|
56
|
+
method: request.method,
|
|
57
|
+
headers,
|
|
58
|
+
body: request.body,
|
|
59
|
+
duplex: "half",
|
|
60
|
+
redirect: request.redirect,
|
|
61
|
+
signal: request.signal,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Strip the internal socket peer header from a Response so it never reaches
|
|
66
|
+
* the browser. Auth plugins that accidentally echo the header will not leak
|
|
67
|
+
* the TCP peer information to the client.
|
|
68
|
+
*/
|
|
69
|
+
export function stripSocketPeerHeaderFromResponse(response) {
|
|
70
|
+
if (!response.headers.has(SOCKET_PEER_HEADER))
|
|
71
|
+
return response;
|
|
72
|
+
const headers = new Headers(response.headers);
|
|
73
|
+
headers.delete(SOCKET_PEER_HEADER);
|
|
74
|
+
return new Response(response.body, {
|
|
75
|
+
status: response.status,
|
|
76
|
+
statusText: response.statusText,
|
|
77
|
+
headers,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
36
80
|
function createRequestBaseURL(nodeRequest, host, port) {
|
|
37
81
|
if (isLoopbackAddress(nodeRequest.socket.remoteAddress)) {
|
|
38
82
|
const forwardedHost = firstHeaderValue(nodeRequest.headers["x-forwarded-host"]);
|
|
@@ -163,7 +207,9 @@ export function createWebHostChannel(options = {}) {
|
|
|
163
207
|
return;
|
|
164
208
|
}
|
|
165
209
|
if (url.pathname.startsWith("/api/auth/")) {
|
|
166
|
-
|
|
210
|
+
const authRequest = withSocketPeerHeader(request, nodeRequest.socket.remoteAddress);
|
|
211
|
+
const authResponse = stripSocketPeerHeaderFromResponse(await handleAuthRequest(authRequest));
|
|
212
|
+
await sendWebResponse(nodeResponse, authResponse);
|
|
167
213
|
return;
|
|
168
214
|
}
|
|
169
215
|
const ctx = requireContext();
|
package/dist/web/http.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { gzipSync } from "node:zlib";
|
|
2
2
|
export const MAX_WEB_REQUEST_BODY_BYTES = 4 * 1024 * 1024;
|
|
3
3
|
const MIN_COMPRESS_RESPONSE_BYTES = 1024;
|
|
4
|
+
const INTERNAL_SOCKET_PEER_HEADER = "x-pibo-socket-peer";
|
|
4
5
|
export class PiboWebHttpError extends Error {
|
|
5
6
|
statusCode;
|
|
6
7
|
constructor(message, statusCode) {
|
|
@@ -115,9 +116,11 @@ function responseHeaders(webResponse) {
|
|
|
115
116
|
const headers = {};
|
|
116
117
|
const setCookie = webResponse.headers.getSetCookie?.();
|
|
117
118
|
webResponse.headers.forEach((value, key) => {
|
|
118
|
-
if (key.toLowerCase()
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
if (key.toLowerCase() === "set-cookie")
|
|
120
|
+
return;
|
|
121
|
+
if (key.toLowerCase() === INTERNAL_SOCKET_PEER_HEADER)
|
|
122
|
+
return;
|
|
123
|
+
headers[key] = value;
|
|
121
124
|
});
|
|
122
125
|
if (setCookie?.length) {
|
|
123
126
|
headers["set-cookie"] = setCookie;
|