pilotswarm 0.5.28 → 0.5.30
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/mcp/dist/bin/pilotswarm-mcp.js +4 -0
- package/mcp/dist/bin/pilotswarm-mcp.js.map +1 -1
- package/mcp/dist/src/context.d.ts +43 -10
- package/mcp/dist/src/context.d.ts.map +1 -1
- package/mcp/dist/src/context.js +15 -7
- package/mcp/dist/src/context.js.map +1 -1
- package/mcp/dist/src/resources/artifacts.d.ts.map +1 -1
- package/mcp/dist/src/resources/artifacts.js +4 -3
- package/mcp/dist/src/resources/artifacts.js.map +1 -1
- package/mcp/dist/src/server.d.ts.map +1 -1
- package/mcp/dist/src/server.js +2 -0
- package/mcp/dist/src/server.js.map +1 -1
- package/mcp/dist/src/tools/agent-packages.d.ts +14 -0
- package/mcp/dist/src/tools/agent-packages.d.ts.map +1 -0
- package/mcp/dist/src/tools/agent-packages.js +126 -0
- package/mcp/dist/src/tools/agent-packages.js.map +1 -0
- package/mcp/dist/src/tools/agents.d.ts.map +1 -1
- package/mcp/dist/src/tools/agents.js +18 -0
- package/mcp/dist/src/tools/agents.js.map +1 -1
- package/mcp/dist/src/tools/artifacts.d.ts.map +1 -1
- package/mcp/dist/src/tools/artifacts.js +12 -11
- package/mcp/dist/src/tools/artifacts.js.map +1 -1
- package/mcp/dist/src/tools/capabilities.js +1 -1
- package/mcp/dist/src/tools/capabilities.js.map +1 -1
- package/mcp/dist/src/tools/observability.js +1 -1
- package/mcp/dist/src/tools/observability.js.map +1 -1
- package/mcp/dist/src/tools/sessions.js +2 -2
- package/mcp/dist/src/tools/sessions.js.map +1 -1
- package/mcp/dist/src/tools/system.d.ts.map +1 -1
- package/mcp/dist/src/tools/system.js +56 -5
- package/mcp/dist/src/tools/system.js.map +1 -1
- package/mcp/dist/src/tools/turn-control.js +1 -1
- package/mcp/dist/src/tools/turn-control.js.map +1 -1
- package/mcp/dist/src/util/command.d.ts +3 -3
- package/mcp/dist/src/util/command.d.ts.map +1 -1
- package/mcp/dist/src/util/command.js +1 -1
- package/mcp/dist/src/util/command.js.map +1 -1
- package/package.json +6 -6
- package/tui/bin/tui.js +6 -0
- package/tui/src/agents-cli.js +442 -0
- package/tui/src/app.js +44 -1
- package/tui/src/node-sdk-transport.js +386 -5
- package/tui/src/plugin-config.js +17 -0
- package/ui/core/src/commands.js +7 -0
- package/ui/core/src/controller.js +498 -33
- package/ui/core/src/formatting.js +60 -16
- package/ui/core/src/index.js +2 -0
- package/ui/core/src/reducer.js +421 -34
- package/ui/core/src/repo-import.js +298 -0
- package/ui/core/src/repo-links.js +54 -0
- package/ui/core/src/selectors.js +879 -91
- package/ui/core/src/session-tree.js +109 -8
- package/ui/core/src/state.js +108 -2
- package/ui/core/src/themes/helpers.js +4 -1
- package/ui/core/src/themes/index.js +4 -10
- package/ui/core/src/themes/workspace-dark-rich.js +65 -0
- package/ui/core/src/themes/workspace-dark.js +65 -0
- package/ui/react/src/components.js +181 -4
- package/ui/react/src/web-app.js +1410 -91
- package/web/api/router.js +20 -2
- package/web/authz.js +4 -10
- package/web/dist/assets/{abnfDiagram-VRR7QNED-DLV45wH4.js → abnfDiagram-VRR7QNED-Cimc0f-u.js} +1 -1
- package/web/dist/assets/{arc-CE4xAYdE.js → arc-D8-_Dx5J.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-DfzR8-9F.js → architectureDiagram-ZJ3FMSHR-lzMAuQHJ.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-C2pZdqvH.js → blockDiagram-677ZJIJ3-Dp2CWjVS.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-CKRspTan.js → c4Diagram-LMCZKHZV-DF93UkOy.js} +1 -1
- package/web/dist/assets/channel-OwWeiCox.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-CNjLrfHT.js → chunk-2Q5K7J3B-pbsQRM-F.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-Bv5EwyaJ.js → chunk-32BRIVSS-B_f-gG1O.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-NDBesEbJ.js → chunk-5VM5RSS4-gqlpJDmt.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-DHQht9ty.js → chunk-EX3LRPZG-Cms7hPmM.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-r0OQpJRF.js → chunk-JWPE2WC7-Boq5PtPI.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN-fcwIDgtl.js → chunk-MOJQB5TN-DVn9i8zt.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-x7XQlDO1.js → chunk-RYQCIY6F-a1vKs3UX.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-U8ZNVy1m.js → chunk-V7JOEXUC-i60p5fHd.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-DLEdPLcN.js → chunk-VR4S4FIN-a8p0o-8B.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-TeyAtB-i.js → chunk-XXDRQBXY-BwP1yBKd.js} +1 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-CDaP7nuC.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-CDaP7nuC.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-B3bAl1-Z.js → cose-bilkent-JH36ORCC-HW6kGgze.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-DexP8nG-.js → cynefin-VYW2F7L2-YUFMn-vE.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-Doa2XnSs.js → cynefinDiagram-TSTJHNR4-Brdy17i4.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-DZMBKQcJ.js → dagre-VKFMJZFB-CAiSJJj-.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-BkzibmSq.js → diagram-FQU43EPY-B63Cqe0J.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-CihyHR3a.js → diagram-G47NLZAW-ByktrDJ2.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-OMDUUxwW.js → diagram-NH7WQ7WH-BLSfRMoE.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-Lc0imJba.js → diagram-OA4YK3LP-DNVu-zpk.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-Dlcy8hPe.js → diagram-WEI45ONY-CNf4Sz-I.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-Dh3u17Dg.js → ebnfDiagram-CCIWWBDH-9CT3ergx.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-C0OhlUC2.js → erDiagram-Q63AITRT-BPJHAMgl.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-CADKxT-x.js → flowDiagram-23GEKE2U-BVtpr6Iu.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-iVVdxWB7.js → ganttDiagram-NO4QXBWP-BTHnv9sV.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-BwDUec_a.js → gitGraphDiagram-IHSO6WYX-BfmtqbL5.js} +1 -1
- package/web/dist/assets/index-B37T1c2s.css +1 -0
- package/web/dist/assets/index-Bmb8qK39.js +24 -0
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-CGPUif0s.js → infoDiagram-FWYZ7A6U-kU4i4HRu.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-QYnrl-KA.js → ishikawaDiagram-FXEZZL3T-Ch7i-E8O.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-CeKixxL0.js → journeyDiagram-5HDEW3XC-CPQxp0ps.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-CeYokFBS.js → kanban-definition-HUTT4EX6-BBgMVqoJ.js} +1 -1
- package/web/dist/assets/{linear-BbA6_gnz.js → linear-rwksvzXJ.js} +1 -1
- package/web/dist/assets/{mermaid.core--xGRoQTa.js → mermaid.core-o7PszRUc.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-D53u8h0z.js → mindmap-definition-LN4V7U3C-wgOTdzlq.js} +1 -1
- package/web/dist/assets/{papaparse.min-BU0o7jXn.js → papaparse.min-BRXtjEI6.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-B7QSyG-3.js → pegDiagram-2B236MQR-BkhQqqTm.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-XtSGL7GG.js → pieDiagram-ENE6RG2P-CcMnvxUz.js} +1 -1
- package/web/dist/assets/pilotswarm-DYXtzuUN.js +99 -0
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-LZoYSbTH.js → quadrantDiagram-ABIIQ3AL-_KD1QeEr.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-B5FREx2U.js → railroadDiagram-RFXS5EU6-CqI0olIe.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-1VRFkBQX.js → requirementDiagram-TGXJPOKE-1JxRwusF.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-J456gWvu.js → sankeyDiagram-HTMAVEWB-9IpFoKzi.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-CaCcw6KO.js → sequenceDiagram-DBY2YBRQ-teyObSQ9.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-DClsP3_O.js → sizeCapture-X5ZJPWSS-DVhU2J_6.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-D6VyrKhd.js → stateDiagram-2N3HPSRC-DHPoWpIQ.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-DJjZWYLS.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-q678iAuo.js → swimlanes-5IMT3BWC-BxTtDQoB.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-CV12vDcK.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-WnPK_zhe.js → timeline-definition-FHXFAJF6-D0wXZx-4.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-WpumuLld.js → vennDiagram-L72KCM5P-BSFyI3iV.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-CBrYLUm_.js → wardleyDiagram-EHGQE667-BN-S7hh0.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-DbX1rnDW.js → xychartDiagram-FW5EYKEG-nDNuPs7o.js} +1 -1
- package/web/dist/index.html +3 -3
- package/web/runtime.js +36 -4
- package/ui/core/src/themes/daylight.js +0 -62
- package/ui/core/src/themes/light-high-contrast.js +0 -65
- package/ui/core/src/themes/paper-ink.js +0 -62
- package/ui/core/src/themes/solarized-ops.js +0 -59
- package/ui/core/src/themes/tokyo-night.js +0 -56
- package/web/dist/assets/channel-D7p-E4El.js +0 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-yFZ-1yMb.js +0 -1
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-yFZ-1yMb.js +0 -1
- package/web/dist/assets/index-BGWTu1KK.css +0 -1
- package/web/dist/assets/index-Djfdr7ts.js +0 -24
- package/web/dist/assets/pilotswarm-CtO819QH.js +0 -94
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-D7eKFhHb.js +0 -1
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-DlVHQBQx.js +0 -8
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side agent-package import.
|
|
3
|
+
*
|
|
4
|
+
* The portal reads GitHub / Azure DevOps repos FROM THE BROWSER, using the
|
|
5
|
+
* viewer's own credentials, and hands the collected files to the standard
|
|
6
|
+
* upload op — so every package arrives through one mechanism (files →
|
|
7
|
+
* canonical tar.gz in the artifact store) and the server never makes an
|
|
8
|
+
* outbound call to a code host or stores anyone's token.
|
|
9
|
+
*
|
|
10
|
+
* Host-agnostic on purpose: `fetch` and the auth token are injected, so this
|
|
11
|
+
* is exercised by plain node tests. Base64 encoding works in both runtimes.
|
|
12
|
+
*
|
|
13
|
+
* GitHub: the tree listing comes from api.github.com (1 request); file bytes
|
|
14
|
+
* come from raw.githubusercontent.com when anonymous (CORS-open, no API rate
|
|
15
|
+
* limit) or from the blobs API when a PAT is supplied (private repos).
|
|
16
|
+
* Azure DevOps: items + blobs REST, authorized with an Entra bearer token
|
|
17
|
+
* (the signed-in user) or a PAT via Basic.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { parseAgentSourceLink } from "./repo-links.js";
|
|
21
|
+
|
|
22
|
+
/** The upload envelope the portal accepts (server enforces the same). */
|
|
23
|
+
export const IMPORT_MAX_TOTAL_BYTES = 2 * 1024 * 1024;
|
|
24
|
+
|
|
25
|
+
const SKIP_SEGMENTS = new Set(["node_modules", ".git"]);
|
|
26
|
+
|
|
27
|
+
function bytesToBase64(bytes) {
|
|
28
|
+
const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
29
|
+
if (typeof Buffer !== "undefined") return Buffer.from(view).toString("base64");
|
|
30
|
+
let binary = "";
|
|
31
|
+
const chunk = 0x8000;
|
|
32
|
+
for (let index = 0; index < view.length; index += chunk) {
|
|
33
|
+
binary += String.fromCharCode.apply(null, view.subarray(index, index + chunk));
|
|
34
|
+
}
|
|
35
|
+
return btoa(binary);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function base64ToBytes(content) {
|
|
39
|
+
const compact = String(content || "").replace(/\s+/gu, "");
|
|
40
|
+
if (typeof Buffer !== "undefined") return new Uint8Array(Buffer.from(compact, "base64"));
|
|
41
|
+
const binary = atob(compact);
|
|
42
|
+
const bytes = new Uint8Array(binary.length);
|
|
43
|
+
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
|
|
44
|
+
return bytes;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isSkippedPath(relativePath) {
|
|
48
|
+
return relativePath.split("/").some((segment) => SKIP_SEGMENTS.has(segment));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Normalize a link's path into the package root (dir holding plugin.json). */
|
|
52
|
+
function packageRootFromLinkPath(linkPath) {
|
|
53
|
+
const trimmed = String(linkPath || "").replace(/^\/+/, "").replace(/\/+$/, "");
|
|
54
|
+
if (!trimmed) return "";
|
|
55
|
+
return trimmed.endsWith("/plugin.json")
|
|
56
|
+
? trimmed.slice(0, -"/plugin.json".length)
|
|
57
|
+
: (trimmed === "plugin.json" ? "" : trimmed);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function joinRoot(root, relative) {
|
|
61
|
+
return root ? `${root}/${relative}` : relative;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A network-level failure in the browser is opaque by design (CORS, DNS,
|
|
66
|
+
* blocked). Say what it most likely is instead of "Failed to fetch".
|
|
67
|
+
*/
|
|
68
|
+
function describeNetworkFailure(host, error) {
|
|
69
|
+
const detail = String(error?.message || error || "");
|
|
70
|
+
return new Error(
|
|
71
|
+
`could not reach ${host} from the browser (${detail || "network error"}) — `
|
|
72
|
+
+ "the host may block cross-origin reads from this portal; use Upload folder instead",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function fetchJson(fetchImpl, url, headers, host) {
|
|
77
|
+
let response;
|
|
78
|
+
try {
|
|
79
|
+
response = await fetchImpl(url, { headers: { accept: "application/json", ...headers } });
|
|
80
|
+
} catch (error) {
|
|
81
|
+
throw describeNetworkFailure(host, error);
|
|
82
|
+
}
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
const body = await response.text().catch(() => "");
|
|
85
|
+
throw new Error(`${host} returned HTTP ${response.status}${body ? `: ${body.slice(0, 200)}` : ""}`);
|
|
86
|
+
}
|
|
87
|
+
return response.json();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function fetchBytes(fetchImpl, url, headers, host) {
|
|
91
|
+
let response;
|
|
92
|
+
try {
|
|
93
|
+
response = await fetchImpl(url, { headers });
|
|
94
|
+
} catch (error) {
|
|
95
|
+
throw describeNetworkFailure(host, error);
|
|
96
|
+
}
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
throw new Error(`${host} returned HTTP ${response.status} for ${url}`);
|
|
99
|
+
}
|
|
100
|
+
const buffer = await response.arrayBuffer();
|
|
101
|
+
return new Uint8Array(buffer);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ─── GitHub ──────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
function parseGitHubRepo(repoUrl) {
|
|
107
|
+
const segments = new URL(repoUrl).pathname.split("/").filter(Boolean);
|
|
108
|
+
if (segments.length < 2) throw new Error(`not a GitHub repository URL: ${repoUrl}`);
|
|
109
|
+
return { owner: segments[0], repo: segments[1].replace(/\.git$/u, "") };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function collectGitHubFiles({ repoUrl, ref, path: linkPath }, { fetchImpl, token, onProgress }) {
|
|
113
|
+
const { owner, repo } = parseGitHubRepo(repoUrl);
|
|
114
|
+
const api = "https://api.github.com";
|
|
115
|
+
const headers = {
|
|
116
|
+
"x-github-api-version": "2022-11-28",
|
|
117
|
+
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
let branch = ref;
|
|
121
|
+
if (!branch) {
|
|
122
|
+
onProgress("resolving the default branch…");
|
|
123
|
+
const meta = await fetchJson(fetchImpl, `${api}/repos/${owner}/${repo}`, headers, "api.github.com");
|
|
124
|
+
branch = meta?.default_branch || "main";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
onProgress(`reading the file tree of ${owner}/${repo}@${branch}…`);
|
|
128
|
+
const tree = await fetchJson(
|
|
129
|
+
fetchImpl,
|
|
130
|
+
`${api}/repos/${owner}/${repo}/git/trees/${encodeURIComponent(branch)}?recursive=1`,
|
|
131
|
+
headers,
|
|
132
|
+
"api.github.com",
|
|
133
|
+
);
|
|
134
|
+
if (tree?.truncated) {
|
|
135
|
+
throw new Error("this repository's file tree is too large for the API listing — point the link at the package folder");
|
|
136
|
+
}
|
|
137
|
+
const entries = Array.isArray(tree?.tree) ? tree.tree : [];
|
|
138
|
+
const root = packageRootFromLinkPath(linkPath);
|
|
139
|
+
const manifestPath = joinRoot(root, "plugin.json");
|
|
140
|
+
if (!entries.some((entry) => entry.type === "blob" && entry.path === manifestPath)) {
|
|
141
|
+
throw new Error(`no plugin.json at ${manifestPath || "the repository root"} on ${branch} — link to the folder that contains the manifest`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const prefix = root ? `${root}/` : "";
|
|
145
|
+
const wanted = entries.filter((entry) =>
|
|
146
|
+
entry.type === "blob"
|
|
147
|
+
&& (!prefix || entry.path.startsWith(prefix))
|
|
148
|
+
&& !isSkippedPath(entry.path.slice(prefix.length)));
|
|
149
|
+
|
|
150
|
+
const files = [];
|
|
151
|
+
let total = 0;
|
|
152
|
+
let index = 0;
|
|
153
|
+
for (const entry of wanted) {
|
|
154
|
+
index += 1;
|
|
155
|
+
const relative = entry.path.slice(prefix.length);
|
|
156
|
+
onProgress(`fetching ${index}/${wanted.length}: ${relative}`);
|
|
157
|
+
const bytes = token
|
|
158
|
+
? await fetchGitHubBlobViaApi(fetchImpl, api, owner, repo, entry.sha, headers)
|
|
159
|
+
: await fetchBytes(
|
|
160
|
+
fetchImpl,
|
|
161
|
+
`https://raw.githubusercontent.com/${owner}/${repo}/${encodeURIComponent(branch)}/${entry.path.split("/").map(encodeURIComponent).join("/")}`,
|
|
162
|
+
{},
|
|
163
|
+
"raw.githubusercontent.com",
|
|
164
|
+
);
|
|
165
|
+
total += bytes.length;
|
|
166
|
+
if (total > IMPORT_MAX_TOTAL_BYTES) {
|
|
167
|
+
throw new Error(`package exceeds the ${Math.round(IMPORT_MAX_TOTAL_BYTES / 1024)} KB upload limit at ${relative}`);
|
|
168
|
+
}
|
|
169
|
+
files.push({ path: relative, contentBase64: bytesToBase64(bytes) });
|
|
170
|
+
}
|
|
171
|
+
return files;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function fetchGitHubBlobViaApi(fetchImpl, api, owner, repo, sha, headers) {
|
|
175
|
+
const blob = await fetchJson(fetchImpl, `${api}/repos/${owner}/${repo}/git/blobs/${sha}`, headers, "api.github.com");
|
|
176
|
+
const content = String(blob?.content || "").replace(/\s+/gu, "");
|
|
177
|
+
if (blob?.encoding && blob.encoding !== "base64") {
|
|
178
|
+
throw new Error(`unexpected blob encoding "${blob.encoding}" from api.github.com`);
|
|
179
|
+
}
|
|
180
|
+
return base64ToBytes(content);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ─── Azure DevOps ────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
export function parseAdoRepoUrl(repoUrl) {
|
|
186
|
+
const url = new URL(repoUrl);
|
|
187
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
188
|
+
const gitIndex = segments.indexOf("_git");
|
|
189
|
+
if (gitIndex < 0 || gitIndex === segments.length - 1) {
|
|
190
|
+
throw new Error(`not an Azure DevOps repository URL: ${repoUrl}`);
|
|
191
|
+
}
|
|
192
|
+
const repo = decodeURIComponent(segments[gitIndex + 1]);
|
|
193
|
+
const before = segments.slice(0, gitIndex).map(decodeURIComponent);
|
|
194
|
+
const host = url.hostname.toLowerCase();
|
|
195
|
+
// dev.azure.com/{org}/{project}/_git/{repo} vs {org}.visualstudio.com/{project}/_git/{repo}
|
|
196
|
+
const org = host.endsWith(".visualstudio.com") ? host.split(".")[0] : before[0];
|
|
197
|
+
const project = host.endsWith(".visualstudio.com") ? before[0] : before[1];
|
|
198
|
+
if (!org || !project) throw new Error(`could not read org/project from ${repoUrl}`);
|
|
199
|
+
return { org, project, repo };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function collectAdoFiles({ repoUrl, ref, path: linkPath }, { fetchImpl, token, tokenKind, onProgress }) {
|
|
203
|
+
if (!token) {
|
|
204
|
+
throw new Error("Azure DevOps needs a credential — sign-in did not yield a token; paste a PAT with Code (read)");
|
|
205
|
+
}
|
|
206
|
+
const { org, project, repo } = parseAdoRepoUrl(repoUrl);
|
|
207
|
+
const base = `https://dev.azure.com/${encodeURIComponent(org)}/${encodeURIComponent(project)}/_apis/git/repositories/${encodeURIComponent(repo)}`;
|
|
208
|
+
const authorization = tokenKind === "pat"
|
|
209
|
+
? `Basic ${bytesToBase64(new TextEncoder().encode(`:${token}`))}`
|
|
210
|
+
: `Bearer ${token}`;
|
|
211
|
+
const headers = { authorization };
|
|
212
|
+
const root = packageRootFromLinkPath(linkPath);
|
|
213
|
+
const scopePath = `/${root}`;
|
|
214
|
+
const version = ref ? `&versionDescriptor.version=${encodeURIComponent(ref)}&versionDescriptor.versionType=branch` : "";
|
|
215
|
+
|
|
216
|
+
onProgress(`reading ${repo}${root ? `/${root}` : ""}…`);
|
|
217
|
+
const listing = await fetchJson(
|
|
218
|
+
fetchImpl,
|
|
219
|
+
`${base}/items?scopePath=${encodeURIComponent(scopePath)}&recursionLevel=Full${version}&api-version=7.1`,
|
|
220
|
+
headers,
|
|
221
|
+
"dev.azure.com",
|
|
222
|
+
);
|
|
223
|
+
const items = Array.isArray(listing?.value) ? listing.value : [];
|
|
224
|
+
const blobs = items.filter((item) => item?.gitObjectType === "blob" && !item?.isFolder);
|
|
225
|
+
const prefix = root ? `/${root}/` : "/";
|
|
226
|
+
const manifest = blobs.find((item) => item.path === `${prefix}plugin.json`.replace("//", "/"));
|
|
227
|
+
if (!manifest) {
|
|
228
|
+
throw new Error(`no plugin.json under ${scopePath} — link to the folder that contains the manifest`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const files = [];
|
|
232
|
+
let total = 0;
|
|
233
|
+
let index = 0;
|
|
234
|
+
for (const item of blobs) {
|
|
235
|
+
const relative = String(item.path || "").slice(prefix.length);
|
|
236
|
+
if (!relative || isSkippedPath(relative)) continue;
|
|
237
|
+
index += 1;
|
|
238
|
+
onProgress(`fetching ${index}/${blobs.length}: ${relative}`);
|
|
239
|
+
const bytes = await fetchBytes(
|
|
240
|
+
fetchImpl,
|
|
241
|
+
`${base}/blobs/${encodeURIComponent(item.objectId)}?api-version=7.1&$format=octetStream`,
|
|
242
|
+
{ ...headers, accept: "application/octet-stream" },
|
|
243
|
+
"dev.azure.com",
|
|
244
|
+
);
|
|
245
|
+
total += bytes.length;
|
|
246
|
+
if (total > IMPORT_MAX_TOTAL_BYTES) {
|
|
247
|
+
throw new Error(`package exceeds the ${Math.round(IMPORT_MAX_TOTAL_BYTES / 1024)} KB upload limit at ${relative}`);
|
|
248
|
+
}
|
|
249
|
+
files.push({ path: relative, contentBase64: bytesToBase64(bytes) });
|
|
250
|
+
}
|
|
251
|
+
return files;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ─── entry point ─────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Read a package from a pasted repo link, in the browser, as the viewer.
|
|
258
|
+
* Returns upload-ready files ([{path, contentBase64}]) relative to the
|
|
259
|
+
* package root (the directory holding plugin.json).
|
|
260
|
+
*/
|
|
261
|
+
export async function importPackageFilesFromLink(link, {
|
|
262
|
+
fetchImpl = (typeof fetch === "function" ? fetch : null),
|
|
263
|
+
token = null,
|
|
264
|
+
tokenKind = "bearer",
|
|
265
|
+
onProgress = () => {},
|
|
266
|
+
} = {}) {
|
|
267
|
+
if (typeof fetchImpl !== "function") throw new Error("no fetch implementation available");
|
|
268
|
+
const parsed = parseAgentSourceLink(link);
|
|
269
|
+
if (parsed.error) throw new Error(parsed.error);
|
|
270
|
+
const options = { fetchImpl, token, tokenKind, onProgress };
|
|
271
|
+
const files = parsed.kind === "github"
|
|
272
|
+
? await collectGitHubFiles(parsed, options)
|
|
273
|
+
: await collectAdoFiles(parsed, options);
|
|
274
|
+
if (files.length === 0) throw new Error("the package folder is empty");
|
|
275
|
+
return { kind: parsed.kind, files };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* The package name from an imported file set's manifest, or null when the
|
|
280
|
+
* manifest is missing or unreadable. A package's IDENTITY is its manifest
|
|
281
|
+
* name, so "update this package" has to check it: pointing the update dialog
|
|
282
|
+
* at a folder that builds something else would otherwise publish a second
|
|
283
|
+
* package under a different name and report success.
|
|
284
|
+
*/
|
|
285
|
+
export function readImportedPackageName(files = []) {
|
|
286
|
+
const manifest = (Array.isArray(files) ? files : []).find((file) => file?.path === "plugin.json");
|
|
287
|
+
if (!manifest?.contentBase64) return null;
|
|
288
|
+
try {
|
|
289
|
+
const bytes = base64ToBytes(manifest.contentBase64);
|
|
290
|
+
const parsed = JSON.parse(new TextDecoder().decode(bytes));
|
|
291
|
+
const name = String(parsed?.name || "").trim();
|
|
292
|
+
return name || null;
|
|
293
|
+
} catch {
|
|
294
|
+
// Unreadable here is not the update path's problem to report: the
|
|
295
|
+
// server validates the manifest and says so precisely.
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep-link parsing for agent-package sources: the Add-package dialog takes
|
|
3
|
+
* ONE link — to plugin.json or the folder containing it — and this derives
|
|
4
|
+
* the {kind, repoUrl, ref, path} triple the source registry stores.
|
|
5
|
+
*
|
|
6
|
+
* Recognized shapes:
|
|
7
|
+
* https://github.com/ORG/REPO
|
|
8
|
+
* https://github.com/ORG/REPO/(blob|tree)/REF[/PATH…]
|
|
9
|
+
* https://dev.azure.com/ORG/PROJECT/_git/REPO[?path=/P][&version=GBref]
|
|
10
|
+
* https://ORG.visualstudio.com/PROJECT/_git/REPO[?path=…&version=…]
|
|
11
|
+
*
|
|
12
|
+
* GitHub URLs cannot distinguish a branch containing "/" from the leading
|
|
13
|
+
* path segment; the first segment after blob|tree is taken as the ref.
|
|
14
|
+
*/
|
|
15
|
+
export function parseAgentSourceLink(raw) {
|
|
16
|
+
const text = String(raw || "").trim();
|
|
17
|
+
if (!text) return { error: "Paste a link to plugin.json or the folder that contains it." };
|
|
18
|
+
let url;
|
|
19
|
+
try {
|
|
20
|
+
url = new URL(text);
|
|
21
|
+
} catch {
|
|
22
|
+
return { error: "That does not look like a URL — paste the browser link to plugin.json or its folder." };
|
|
23
|
+
}
|
|
24
|
+
const host = url.hostname.toLowerCase();
|
|
25
|
+
|
|
26
|
+
if (host === "github.com" || host === "www.github.com") {
|
|
27
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
28
|
+
if (segments.length < 2) return { error: "GitHub links need at least github.com/org/repo." };
|
|
29
|
+
const [org, repoRaw, marker, ref, ...rest] = segments;
|
|
30
|
+
const repo = repoRaw.replace(/\.git$/u, "");
|
|
31
|
+
const repoUrl = `https://github.com/${org}/${repo}`;
|
|
32
|
+
if (!marker) return { kind: "github", repoUrl, ref: null, path: null };
|
|
33
|
+
if (marker !== "blob" && marker !== "tree") {
|
|
34
|
+
return { error: `Unrecognized GitHub link shape (…/${marker}/…) — use a /blob/ or /tree/ link, or the bare repo URL.` };
|
|
35
|
+
}
|
|
36
|
+
if (!ref) return { error: "That GitHub link is missing the branch segment." };
|
|
37
|
+
return { kind: "github", repoUrl, ref, path: rest.length ? `/${rest.join("/")}` : null };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const isAdoHost = host === "dev.azure.com" || host.endsWith(".visualstudio.com");
|
|
41
|
+
if (isAdoHost) {
|
|
42
|
+
if (!url.pathname.includes("/_git/")) {
|
|
43
|
+
return { error: "Azure DevOps links must point at a repo (…/_git/<repo>…)." };
|
|
44
|
+
}
|
|
45
|
+
const repoUrl = `${url.origin}${url.pathname.split("?")[0]}`.replace(/\/$/u, "");
|
|
46
|
+
const pathParam = url.searchParams.get("path");
|
|
47
|
+
const versionParam = url.searchParams.get("version");
|
|
48
|
+
// version encodes the ref kind in a two-letter prefix: GB=branch, GT=tag.
|
|
49
|
+
const ref = versionParam && /^G[BT]/u.test(versionParam) ? versionParam.slice(2) : (versionParam || null);
|
|
50
|
+
return { kind: "ado", repoUrl, ref, path: pathParam || null };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { error: "Only GitHub and Azure DevOps links are recognized here — for a tarball use the URL tab." };
|
|
54
|
+
}
|