@lamplitisles/dsh-nanocodex 0.1.0-beta.0
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 +65 -0
- package/NOTICE +2 -0
- package/README.md +182 -0
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/cordis.patch.yml +52 -0
- package/dist/index.d.ts +435 -0
- package/dist/index.js +3405 -0
- package/dist/standard-preset.d.ts +7 -0
- package/dist/standard-preset.js +25 -0
- package/engine-release.json +14 -0
- package/package.json +119 -0
- package/presets/standard/agent.cordis.yml +6 -0
- package/presets/standard/preset.yml +2 -0
- package/vendor/nanocodex/README.md +1344 -0
- package/vendor/nanocodex/actions/events.d.mts +10 -0
- package/vendor/nanocodex/actions/events.mjs +163 -0
- package/vendor/nanocodex/actions/index.d.mts +13 -0
- package/vendor/nanocodex/actions/index.mjs +35 -0
- package/vendor/nanocodex/actions/session.d.mts +66 -0
- package/vendor/nanocodex/actions/session.mjs +59 -0
- package/vendor/nanocodex/actions/turn.d.mts +59 -0
- package/vendor/nanocodex/actions/turn.mjs +37 -0
- package/vendor/nanocodex/actions/voice.d.mts +22 -0
- package/vendor/nanocodex/actions/voice.mjs +50 -0
- package/vendor/nanocodex/browser/Agent.d.mts +55 -0
- package/vendor/nanocodex/browser/Agent.mjs +11 -0
- package/vendor/nanocodex/browser/ChatGptSubscription.d.mts +7 -0
- package/vendor/nanocodex/browser/ChatGptSubscription.mjs +19 -0
- package/vendor/nanocodex/browser/InlineAgent.mjs +266 -0
- package/vendor/nanocodex/browser/Transport.d.mts +74 -0
- package/vendor/nanocodex/browser/Transport.mjs +61 -0
- package/vendor/nanocodex/browser/Voice.d.mts +71 -0
- package/vendor/nanocodex/browser/Voice.mjs +291 -0
- package/vendor/nanocodex/browser/VoiceSession.mjs +688 -0
- package/vendor/nanocodex/browser/WorkerAgent.d.mts +65 -0
- package/vendor/nanocodex/browser/WorkerAgent.mjs +1472 -0
- package/vendor/nanocodex/browser/agent.worker.mjs +3 -0
- package/vendor/nanocodex/browser/config.d.mts +36 -0
- package/vendor/nanocodex/browser/config.mjs +439 -0
- package/vendor/nanocodex/browser/engine.mjs +13 -0
- package/vendor/nanocodex/browser/harness.mjs +55 -0
- package/vendor/nanocodex/browser/host.d.mts +83 -0
- package/vendor/nanocodex/browser/host.mjs +555 -0
- package/vendor/nanocodex/browser/hostManagedWebSocket.d.mts +14 -0
- package/vendor/nanocodex/browser/hostManagedWebSocket.mjs +110 -0
- package/vendor/nanocodex/browser/index.d.mts +92 -0
- package/vendor/nanocodex/browser/index.mjs +23 -0
- package/vendor/nanocodex/browser/indexeddb-durability-store.mjs +197 -0
- package/vendor/nanocodex/browser/workspace.d.mts +16 -0
- package/vendor/nanocodex/browser/workspace.mjs +107 -0
- package/vendor/nanocodex/cloud/Client.d.mts +83 -0
- package/vendor/nanocodex/cloud/Client.mjs +231 -0
- package/vendor/nanocodex/cloud/Decorator.d.mts +31 -0
- package/vendor/nanocodex/cloud/Decorator.mjs +37 -0
- package/vendor/nanocodex/cloud/Dialog.d.mts +113 -0
- package/vendor/nanocodex/cloud/Dialog.mjs +366 -0
- package/vendor/nanocodex/cloud/Errors.d.mts +22 -0
- package/vendor/nanocodex/cloud/Errors.mjs +39 -0
- package/vendor/nanocodex/cloud/Principal.d.mts +26 -0
- package/vendor/nanocodex/cloud/Principal.mjs +88 -0
- package/vendor/nanocodex/cloud/RemoteProvider.mjs +88 -0
- package/vendor/nanocodex/cloud/Transport.d.mts +47 -0
- package/vendor/nanocodex/cloud/Transport.mjs +454 -0
- package/vendor/nanocodex/cloud/actions/account.d.mts +9 -0
- package/vendor/nanocodex/cloud/actions/account.mjs +19 -0
- package/vendor/nanocodex/cloud/actions/agent.d.mts +18 -0
- package/vendor/nanocodex/cloud/actions/agent.mjs +428 -0
- package/vendor/nanocodex/cloud/actions/connection.d.mts +133 -0
- package/vendor/nanocodex/cloud/actions/connection.mjs +580 -0
- package/vendor/nanocodex/cloud/actions/grant.d.mts +10 -0
- package/vendor/nanocodex/cloud/actions/grant.mjs +12 -0
- package/vendor/nanocodex/cloud/actions/index.d.mts +7 -0
- package/vendor/nanocodex/cloud/actions/index.mjs +7 -0
- package/vendor/nanocodex/cloud/actions/machineUsd.d.mts +23 -0
- package/vendor/nanocodex/cloud/actions/machineUsd.mjs +37 -0
- package/vendor/nanocodex/cloud/actions/model.d.mts +12 -0
- package/vendor/nanocodex/cloud/actions/model.mjs +50 -0
- package/vendor/nanocodex/cloud/actions/mpp.d.mts +28 -0
- package/vendor/nanocodex/cloud/actions/mpp.mjs +39 -0
- package/vendor/nanocodex/cloud/index.d.mts +30 -0
- package/vendor/nanocodex/cloud/index.mjs +9 -0
- package/vendor/nanocodex/cloud/internal.mjs +533 -0
- package/vendor/nanocodex/cloud/mercator.mjs +108 -0
- package/vendor/nanocodex/cloud/server/HostPrincipal.d.mts +34 -0
- package/vendor/nanocodex/cloud/server/HostPrincipal.mjs +202 -0
- package/vendor/nanocodex/cloud/server/index.d.mts +1 -0
- package/vendor/nanocodex/cloud/server/index.mjs +1 -0
- package/vendor/nanocodex/cloud/types.d.mts +211 -0
- package/vendor/nanocodex/cloudflare/Agent.d.mts +133 -0
- package/vendor/nanocodex/cloudflare/Agent.mjs +811 -0
- package/vendor/nanocodex/cloudflare/egress-subject.mjs +19 -0
- package/vendor/nanocodex/cloudflare/egress.d.mts +41 -0
- package/vendor/nanocodex/cloudflare/egress.mjs +119 -0
- package/vendor/nanocodex/cloudflare/event-socket.mjs +316 -0
- package/vendor/nanocodex/cloudflare/index.d.mts +7 -0
- package/vendor/nanocodex/cloudflare/index.mjs +6 -0
- package/vendor/nanocodex/host/Agent.d.mts +55 -0
- package/vendor/nanocodex/host/Agent.mjs +1 -0
- package/vendor/nanocodex/host/index.d.mts +82 -0
- package/vendor/nanocodex/host/index.mjs +10 -0
- package/vendor/nanocodex/index.d.mts +81 -0
- package/vendor/nanocodex/index.mjs +12 -0
- package/vendor/nanocodex/internal.mjs +1275 -0
- package/vendor/nanocodex/managed/Agent.d.mts +380 -0
- package/vendor/nanocodex/managed/Agent.mjs +1694 -0
- package/vendor/nanocodex/managed/ManagedError.d.mts +9 -0
- package/vendor/nanocodex/managed/ManagedError.mjs +8 -0
- package/vendor/nanocodex/managed/README.md +241 -0
- package/vendor/nanocodex/managed/Voice.mjs +202 -0
- package/vendor/nanocodex/managed/index.d.mts +20 -0
- package/vendor/nanocodex/managed/index.mjs +2 -0
- package/vendor/nanocodex/managed/internal.mjs +119 -0
- package/vendor/nanocodex/node/Agent.d.mts +50 -0
- package/vendor/nanocodex/node/Agent.mjs +203 -0
- package/vendor/nanocodex/node/ChatGptSubscription.d.mts +9 -0
- package/vendor/nanocodex/node/ChatGptSubscription.mjs +17 -0
- package/vendor/nanocodex/node/Transport.d.mts +44 -0
- package/vendor/nanocodex/node/Transport.mjs +46 -0
- package/vendor/nanocodex/node/host.mjs +731 -0
- package/vendor/nanocodex/node/index.d.mts +73 -0
- package/vendor/nanocodex/node/index.mjs +17 -0
- package/vendor/nanocodex/node/workspace.d.mts +10 -0
- package/vendor/nanocodex/node/workspace.mjs +198 -0
- package/vendor/nanocodex/package.json +200 -0
- package/vendor/nanocodex/pkg-node/nanocodex.d.ts +675 -0
- package/vendor/nanocodex/pkg-node/nanocodex.js +2628 -0
- package/vendor/nanocodex/pkg-node/nanocodex_bg.wasm.d.ts +105 -0
- package/vendor/nanocodex/pkg-node/package.json +1 -0
- package/vendor/nanocodex/pkg-web/.nanocodex-bindgen-stamp +6 -0
- package/vendor/nanocodex/pkg-web/nanocodex-build.json +1 -0
- package/vendor/nanocodex/pkg-web/nanocodex.d.ts +805 -0
- package/vendor/nanocodex/pkg-web/nanocodex.js +2714 -0
- package/vendor/nanocodex/pkg-web/nanocodex_bg.js +2615 -0
- package/vendor/nanocodex/pkg-web/nanocodex_bg.wasm +0 -0
- package/vendor/nanocodex/pkg-web/nanocodex_bg.wasm.d.ts +105 -0
- package/vendor/nanocodex/pkg-web/nanocodex_worker.js +9 -0
- package/vendor/nanocodex/pkg-web/package.json +1 -0
- package/vendor/nanocodex/runtime/chatgpt-subscription.mjs +212 -0
- package/vendor/nanocodex/runtime/cloudflare-durability-store.d.mts +26 -0
- package/vendor/nanocodex/runtime/cloudflare-durability-store.mjs +168 -0
- package/vendor/nanocodex/runtime/code-evaluator.worker.mjs +137 -0
- package/vendor/nanocodex/runtime/code-runtime.mjs +1 -0
- package/vendor/nanocodex/runtime/durability-store.d.mts +85 -0
- package/vendor/nanocodex/runtime/durability-store.mjs +631 -0
- package/vendor/nanocodex/runtime/durability.mjs +153 -0
- package/vendor/nanocodex/runtime/managed-transport.mjs +460 -0
- package/vendor/nanocodex/runtime/mcp-runtime.mjs +534 -0
- package/vendor/nanocodex/runtime/postgres-durability-store.d.mts +64 -0
- package/vendor/nanocodex/runtime/postgres-durability-store.mjs +532 -0
- package/vendor/nanocodex/runtime/quickjs-evaluator.d.mts +17 -0
- package/vendor/nanocodex/runtime/quickjs-evaluator.mjs +236 -0
- package/vendor/nanocodex/runtime/responses-transport.mjs +22 -0
- package/vendor/nanocodex/runtime/subagents.d.mts +107 -0
- package/vendor/nanocodex/runtime/subagents.mjs +54 -0
- package/vendor/nanocodex/runtime/subscription-store.d.mts +8 -0
- package/vendor/nanocodex/runtime/subscription-store.mjs +42 -0
- package/vendor/nanocodex/runtime/tempo-provider.d.mts +125 -0
- package/vendor/nanocodex/runtime/tempo-provider.mjs +264 -0
- package/vendor/nanocodex/runtime/tool-configuration.mjs +1 -0
- package/vendor/nanocodex/runtime/tool-router.mjs +1 -0
- package/vendor/nanocodex/runtime/utf8.mjs +1 -0
- package/vendor/nanocodex/runtime/worker-evaluator.mjs +162 -0
- package/vendor/nanocodex/runtime/workspace.d.mts +1 -0
- package/vendor/nanocodex/runtime/workspace.mjs +1 -0
- package/vendor/nanocodex/tools/Tools.d.mts +56 -0
- package/vendor/nanocodex/tools/Tools.mjs +136 -0
- package/vendor/nanocodex/tools/artifact.d.mts +1 -0
- package/vendor/nanocodex/tools/artifact.mjs +1 -0
- package/vendor/nanocodex/tools/attachment.mjs +1 -0
- package/vendor/nanocodex/tools/bash.d.mts +1 -0
- package/vendor/nanocodex/tools/bash.mjs +1 -0
- package/vendor/nanocodex/tools/browser/accountInfo.mjs +795 -0
- package/vendor/nanocodex/tools/browser/browserBuffer.mjs +18 -0
- package/vendor/nanocodex/tools/browser/browserCompiler.mjs +155 -0
- package/vendor/nanocodex/tools/browser/browserEgress.mjs +161 -0
- package/vendor/nanocodex/tools/browser/browserPython.mjs +133 -0
- package/vendor/nanocodex/tools/browser/browserShell.mjs +1075 -0
- package/vendor/nanocodex/tools/browser/browserSprintf.mjs +4 -0
- package/vendor/nanocodex/tools/browser/browserSsh.mjs +201 -0
- package/vendor/nanocodex/tools/browser/browserZlib.mjs +136 -0
- package/vendor/nanocodex/tools/browser/compiler.worker.mjs +160 -0
- package/vendor/nanocodex/tools/browser/devTunnelsSshBrowser.mjs +13 -0
- package/vendor/nanocodex/tools/browser/index.d.mts +209 -0
- package/vendor/nanocodex/tools/browser/index.mjs +174 -0
- package/vendor/nanocodex/tools/browser/opfsGit.mjs +248 -0
- package/vendor/nanocodex/tools/browser/python.worker.mjs +114 -0
- package/vendor/nanocodex/tools/browser/threadGit.mjs +197 -0
- package/vendor/nanocodex/tools/browser/unsupportedNodeRsa.mjs +5 -0
- package/vendor/nanocodex/tools/browser/workspace.mjs +83 -0
- package/vendor/nanocodex/tools/dataset.d.mts +1 -0
- package/vendor/nanocodex/tools/dataset.mjs +1 -0
- package/vendor/nanocodex/tools/datasetContract.mjs +1 -0
- package/vendor/nanocodex/tools/datasetEngine.mjs +1 -0
- package/vendor/nanocodex/tools/hostedCatalog.d.mts +1 -0
- package/vendor/nanocodex/tools/hostedCatalog.mjs +1 -0
- package/vendor/nanocodex/tools/index.d.mts +49 -0
- package/vendor/nanocodex/tools/index.mjs +19 -0
- package/vendor/nanocodex/tools/namedTool.mjs +1 -0
- package/vendor/nanocodex/tools/repository-workspace.d.mts +1 -0
- package/vendor/nanocodex/tools/repository-workspace.mjs +1 -0
- package/vendor/nanocodex/tools/ssh.d.mts +1 -0
- package/vendor/nanocodex/tools/ssh.mjs +1 -0
- package/vendor/nanocodex/tools/standard.mjs +1 -0
- package/vendor/nanocodex/tools/standardDescriptions.mjs +1 -0
- package/vendor/nanocodex/types.d.mts +663 -0
- package/vendor/nanocodex/wasm.d.mts +4 -0
- package/vendor/nanocodex/worker/ChatGptSubscription.d.mts +9 -0
- package/vendor/nanocodex/worker/ChatGptSubscription.mjs +29 -0
- package/vendor/nanocodex/worker/index.d.mts +18 -0
- package/vendor/nanocodex/worker/index.mjs +5 -0
- package/vendor/nanocodex-tools/README.md +65 -0
- package/vendor/nanocodex-tools/dist/hosted/app-tool-catalog.d.ts +7 -0
- package/vendor/nanocodex-tools/dist/hosted/app-tool-catalog.js +14 -0
- package/vendor/nanocodex-tools/dist/hosted/broker-core.d.ts +172 -0
- package/vendor/nanocodex-tools/dist/hosted/broker-core.js +1278 -0
- package/vendor/nanocodex-tools/dist/hosted/index.d.ts +3 -0
- package/vendor/nanocodex-tools/dist/hosted/index.js +3 -0
- package/vendor/nanocodex-tools/dist/hosted/protocol.d.ts +120 -0
- package/vendor/nanocodex-tools/dist/hosted/protocol.js +415 -0
- package/vendor/nanocodex-tools/dist/index.d.ts +19 -0
- package/vendor/nanocodex-tools/dist/index.js +14 -0
- package/vendor/nanocodex-tools/dist/memory.d.ts +204 -0
- package/vendor/nanocodex-tools/dist/memory.js +411 -0
- package/vendor/nanocodex-tools/dist/namespace.d.ts +87 -0
- package/vendor/nanocodex-tools/dist/namespace.js +338 -0
- package/vendor/nanocodex-tools/dist/repository-archive.d.ts +4 -0
- package/vendor/nanocodex-tools/dist/repository-archive.js +104 -0
- package/vendor/nanocodex-tools/dist/runtime.d.ts +30 -0
- package/vendor/nanocodex-tools/dist/runtime.js +37 -0
- package/vendor/nanocodex-tools/dist/session.d.ts +167 -0
- package/vendor/nanocodex-tools/dist/session.js +286 -0
- package/vendor/nanocodex-tools/dist/shell.d.ts +51 -0
- package/vendor/nanocodex-tools/dist/shell.js +553 -0
- package/vendor/nanocodex-tools/dist/ssh.d.ts +29 -0
- package/vendor/nanocodex-tools/dist/ssh.js +66 -0
- package/vendor/nanocodex-tools/dist/workspace.d.ts +45 -0
- package/vendor/nanocodex-tools/dist/workspace.js +237 -0
- package/vendor/nanocodex-tools/package.json +122 -0
- package/vendor/nanocodex-tools/runtime/code-runtime.mjs +817 -0
- package/vendor/nanocodex-tools/runtime/tool-configuration.mjs +50 -0
- package/vendor/nanocodex-tools/runtime/tool-router.mjs +730 -0
- package/vendor/nanocodex-tools/runtime/utf8.d.mts +2 -0
- package/vendor/nanocodex-tools/runtime/utf8.mjs +25 -0
- package/vendor/nanocodex-tools/tools/artifact.d.mts +69 -0
- package/vendor/nanocodex-tools/tools/artifact.mjs +270 -0
- package/vendor/nanocodex-tools/tools/attachment.mjs +603 -0
- package/vendor/nanocodex-tools/tools/bash.d.mts +90 -0
- package/vendor/nanocodex-tools/tools/bash.mjs +742 -0
- package/vendor/nanocodex-tools/tools/dataset.d.mts +9 -0
- package/vendor/nanocodex-tools/tools/dataset.mjs +47 -0
- package/vendor/nanocodex-tools/tools/datasetContract.mjs +54 -0
- package/vendor/nanocodex-tools/tools/datasetEngine.mjs +1167 -0
- package/vendor/nanocodex-tools/tools/execution-contract.d.mts +5 -0
- package/vendor/nanocodex-tools/tools/execution-contract.mjs +63 -0
- package/vendor/nanocodex-tools/tools/hostedCatalog.d.mts +20 -0
- package/vendor/nanocodex-tools/tools/hostedCatalog.mjs +67 -0
- package/vendor/nanocodex-tools/tools/hostedMachine.d.mts +10 -0
- package/vendor/nanocodex-tools/tools/hostedMachine.mjs +56 -0
- package/vendor/nanocodex-tools/tools/just-bash-browser.d.mts +1 -0
- package/vendor/nanocodex-tools/tools/just-bash-browser.mjs +1 -0
- package/vendor/nanocodex-tools/tools/namedTool.d.mts +3 -0
- package/vendor/nanocodex-tools/tools/namedTool.mjs +3 -0
- package/vendor/nanocodex-tools/tools/nodeProcess.d.mts +10 -0
- package/vendor/nanocodex-tools/tools/nodeProcess.mjs +314 -0
- package/vendor/nanocodex-tools/tools/processOutput.mjs +56 -0
- package/vendor/nanocodex-tools/tools/repository-workspace.d.mts +55 -0
- package/vendor/nanocodex-tools/tools/repository-workspace.mjs +397 -0
- package/vendor/nanocodex-tools/tools/ssh.d.mts +56 -0
- package/vendor/nanocodex-tools/tools/ssh.mjs +503 -0
- package/vendor/nanocodex-tools/tools/standard.mjs +458 -0
- package/vendor/nanocodex-tools/tools/standardDescriptions.mjs +3 -0
- package/vendor/nanocodex-tools/tools/types.d.mts +83 -0
- package/vendor/nanocodex-tools/tools/workspace.d.mts +25 -0
- package/vendor/nanocodex-tools/tools/workspace.mjs +282 -0
- package/vendor/nanocodex-tools/tools/x.d.mts +30 -0
- package/vendor/nanocodex-tools/tools/x.mjs +91 -0
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
import git, {} from "isomorphic-git";
|
|
2
|
+
import { downloadRepositoryArchive } from "./repository-archive.js";
|
|
3
|
+
const REPOSITORY = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
4
|
+
const GITHUB_REPOSITORY = /^https:\/\/github\.com\/([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+?)(?:\.git)?\/?$/;
|
|
5
|
+
/** gh compatibility command backed by the connected GitHub account. */
|
|
6
|
+
export function createGhCommand(fetch, clone) {
|
|
7
|
+
return {
|
|
8
|
+
name: "gh",
|
|
9
|
+
trusted: true,
|
|
10
|
+
async execute(args, context = {}) {
|
|
11
|
+
const request = (url, options) => {
|
|
12
|
+
context.signal?.throwIfAborted();
|
|
13
|
+
return fetch(url, { ...options, signal: context.signal });
|
|
14
|
+
};
|
|
15
|
+
try {
|
|
16
|
+
context.signal?.throwIfAborted();
|
|
17
|
+
if (args[0] === "auth" && args[1] === "status") {
|
|
18
|
+
const user = await github(request, "/user");
|
|
19
|
+
return ok(`Logged in to github.com as ${text(user, "login")} through the connected account.\n`);
|
|
20
|
+
}
|
|
21
|
+
if (args[0] === "api") {
|
|
22
|
+
const fields = apiFields(args);
|
|
23
|
+
const explicitMethod = option(args, "--method", "-X");
|
|
24
|
+
const method = (explicitMethod ?? (Object.keys(fields).length ? "POST" : "GET")).toUpperCase();
|
|
25
|
+
const endpoint = positional(args.slice(1), [
|
|
26
|
+
"--method", "-X", "-f", "-F", "--field", "--raw-field",
|
|
27
|
+
]);
|
|
28
|
+
if (!endpoint)
|
|
29
|
+
throw new Error("gh api requires an endpoint");
|
|
30
|
+
let path = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
|
|
31
|
+
const hasFields = Object.keys(fields).length > 0;
|
|
32
|
+
if (hasFields && (method === "GET" || method === "HEAD")) {
|
|
33
|
+
const target = new URL(path, "https://api.github.com");
|
|
34
|
+
for (const [name, value] of Object.entries(fields))
|
|
35
|
+
target.searchParams.set(name, value);
|
|
36
|
+
path = `${target.pathname}${target.search}`;
|
|
37
|
+
}
|
|
38
|
+
return ok(`${JSON.stringify(await github(request, path, {
|
|
39
|
+
method,
|
|
40
|
+
...(hasFields && method !== "GET" && method !== "HEAD"
|
|
41
|
+
? { body: JSON.stringify(fields) }
|
|
42
|
+
: {}),
|
|
43
|
+
}), null, 2)}\n`);
|
|
44
|
+
}
|
|
45
|
+
if (args[0] === "repo" && args[1] === "view") {
|
|
46
|
+
const repository = option(args.slice(2), "--repo", "-R")
|
|
47
|
+
?? args.slice(2).find((value) => !value.startsWith("-"));
|
|
48
|
+
requireRepository(repository, "gh repo view requires OWNER/REPO");
|
|
49
|
+
const repo = requireRecord(await github(request, `/repos/${repository}`), "repository");
|
|
50
|
+
return ok([
|
|
51
|
+
`name:\t${optionalText(repo, "full_name") ?? repository}`,
|
|
52
|
+
`description:\t${optionalText(repo, "description") ?? ""}`,
|
|
53
|
+
`url:\t${optionalText(repo, "html_url") ?? `https://github.com/${repository}`}`,
|
|
54
|
+
"",
|
|
55
|
+
].join("\n"));
|
|
56
|
+
}
|
|
57
|
+
if (args[0] === "repo" && args[1] === "clone") {
|
|
58
|
+
if (!clone)
|
|
59
|
+
throw new Error("repository cloning is unavailable in this runtime");
|
|
60
|
+
return clone(ghRepoCloneArguments(args.slice(2)), context);
|
|
61
|
+
}
|
|
62
|
+
if (args[0] === "repo" && args[1] === "list") {
|
|
63
|
+
const owner = positional(args.slice(2), ["--limit", "-L"]);
|
|
64
|
+
const perPage = limit(option(args.slice(2), "--limit", "-L"));
|
|
65
|
+
const repositories = await github(request, `/user/repos?${new URLSearchParams({
|
|
66
|
+
affiliation: "owner,collaborator,organization_member",
|
|
67
|
+
per_page: "100",
|
|
68
|
+
sort: "updated",
|
|
69
|
+
})}`);
|
|
70
|
+
if (!Array.isArray(repositories))
|
|
71
|
+
throw new Error("GitHub returned an invalid repository list");
|
|
72
|
+
const selected = repositories.filter((repository) => {
|
|
73
|
+
if (!owner)
|
|
74
|
+
return true;
|
|
75
|
+
const record = requireRecord(repository, "repository");
|
|
76
|
+
const repositoryOwner = requireRecord(record.owner, "repository owner");
|
|
77
|
+
return optionalText(repositoryOwner, "login")?.toLowerCase() === owner.toLowerCase();
|
|
78
|
+
}).slice(0, perPage);
|
|
79
|
+
return ok(selected.map((repository) => {
|
|
80
|
+
const record = requireRecord(repository, "repository");
|
|
81
|
+
return [
|
|
82
|
+
optionalText(record, "full_name") ?? "unknown",
|
|
83
|
+
optionalText(record, "description") ?? "",
|
|
84
|
+
record.private === true ? "private" : "public",
|
|
85
|
+
].join("\t");
|
|
86
|
+
}).join("\n") + (selected.length ? "\n" : ""));
|
|
87
|
+
}
|
|
88
|
+
if (args[0] === "pr" && args[1] === "list") {
|
|
89
|
+
const repository = option(args.slice(2), "--repo", "-R");
|
|
90
|
+
requireRepository(repository, "gh pr list requires --repo OWNER/REPO");
|
|
91
|
+
const pulls = await github(request, `/repos/${repository}/pulls?${new URLSearchParams({
|
|
92
|
+
state: "open",
|
|
93
|
+
per_page: String(limit(option(args.slice(2), "--limit", "-L"))),
|
|
94
|
+
})}`);
|
|
95
|
+
if (!Array.isArray(pulls))
|
|
96
|
+
throw new Error("GitHub returned an invalid pull request list");
|
|
97
|
+
return ok(pulls.map((pull) => {
|
|
98
|
+
const row = requireRecord(pull, "pull request");
|
|
99
|
+
const head = requireRecord(row.head, "pull request head");
|
|
100
|
+
return [row.number, text(row, "title"), text(head, "ref")].join("\t");
|
|
101
|
+
}).join("\n") + (pulls.length ? "\n" : ""));
|
|
102
|
+
}
|
|
103
|
+
return fail([
|
|
104
|
+
"gh (Nanocodex Just Bash compatibility command)",
|
|
105
|
+
"",
|
|
106
|
+
"Supported commands:",
|
|
107
|
+
" gh auth status",
|
|
108
|
+
" gh api [--method METHOD] [-f key=value] ENDPOINT",
|
|
109
|
+
" gh repo list [OWNER] [--limit N]",
|
|
110
|
+
" gh repo clone OWNER/REPO [DIRECTORY] [-- GITFLAGS...]",
|
|
111
|
+
" gh repo view OWNER/REPO",
|
|
112
|
+
" gh pr list --repo OWNER/REPO [--limit N]",
|
|
113
|
+
"",
|
|
114
|
+
"Connected GitHub calls use the permissions granted in Profile.",
|
|
115
|
+
"",
|
|
116
|
+
].join("\n"));
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
return fail(`gh: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function ghRepoCloneArguments(args) {
|
|
125
|
+
const separator = args.indexOf("--");
|
|
126
|
+
const commandArgs = separator === -1 ? args : args.slice(0, separator);
|
|
127
|
+
const gitArgs = separator === -1 ? [] : args.slice(separator + 1);
|
|
128
|
+
if (commandArgs.some((value) => value.startsWith("-"))) {
|
|
129
|
+
throw new Error("gh repo clone supports OWNER/REPO [DIRECTORY] [-- GITFLAGS...]");
|
|
130
|
+
}
|
|
131
|
+
if (commandArgs.length < 1 || commandArgs.length > 2) {
|
|
132
|
+
throw new Error("gh repo clone requires OWNER/REPO and an optional destination");
|
|
133
|
+
}
|
|
134
|
+
const repository = commandArgs[0];
|
|
135
|
+
requireRepository(repository, "gh repo clone requires OWNER/REPO");
|
|
136
|
+
return [
|
|
137
|
+
"clone",
|
|
138
|
+
...gitArgs,
|
|
139
|
+
`https://github.com/${repository}.git`,
|
|
140
|
+
...(commandArgs[1] === undefined ? [] : [commandArgs[1]]),
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
/** Git compatibility command backed by durable storage and host-authorized GitHub downloads. */
|
|
144
|
+
export function createGitCommand(fetch, workspace) {
|
|
145
|
+
return {
|
|
146
|
+
name: "git",
|
|
147
|
+
trusted: true,
|
|
148
|
+
async execute(args, context = {}) {
|
|
149
|
+
try {
|
|
150
|
+
const mounted = commandWorkspace(workspace(), context.signal);
|
|
151
|
+
const command = args[0];
|
|
152
|
+
if (command === "clone") {
|
|
153
|
+
return ok(await cloneRepository(fetch, workspace(), args.slice(1), context));
|
|
154
|
+
}
|
|
155
|
+
const dir = await gitDirectory(mounted, context.cwd);
|
|
156
|
+
const fs = workspaceFs(mounted);
|
|
157
|
+
if (command === "status") {
|
|
158
|
+
const matrix = await git.statusMatrix({ fs, dir });
|
|
159
|
+
const changed = matrix.filter(([, head, workdir, stage]) => head !== workdir || head !== stage);
|
|
160
|
+
if (args.includes("--short") || args.includes("-s") || args.includes("--porcelain")) {
|
|
161
|
+
const lines = changed.flatMap(([path, head, workdir, stage]) => shortStatusCodes(head, workdir, stage).map((code) => `${code} ${path}`));
|
|
162
|
+
return ok(lines.join("\n") + (lines.length ? "\n" : ""));
|
|
163
|
+
}
|
|
164
|
+
const branch = await git.currentBranch({ fs, dir, fullname: false });
|
|
165
|
+
return ok(`On branch ${branch ?? "HEAD"}\n${changed.length ? `${changed.length} changed path(s)\n` : "nothing to commit, working tree clean\n"}`);
|
|
166
|
+
}
|
|
167
|
+
if (command === "log") {
|
|
168
|
+
const depth = logDepth(args.slice(1));
|
|
169
|
+
const commits = await git.log({ fs, dir, depth });
|
|
170
|
+
if (args.includes("--oneline")) {
|
|
171
|
+
return ok(commits.map(({ oid, commit }) => `${oid.slice(0, 7)} ${commit.message.split("\n")[0]}`).join("\n") + (commits.length ? "\n" : ""));
|
|
172
|
+
}
|
|
173
|
+
return ok(commits.map(({ oid, commit }) => [
|
|
174
|
+
`commit ${oid}`,
|
|
175
|
+
`Author: ${commit.author.name} <${commit.author.email}>`,
|
|
176
|
+
`Date: ${new Date(commit.author.timestamp * 1_000).toISOString()}`,
|
|
177
|
+
"",
|
|
178
|
+
` ${commit.message.trim().replaceAll("\n", "\n ")}`,
|
|
179
|
+
"",
|
|
180
|
+
].join("\n")).join("\n"));
|
|
181
|
+
}
|
|
182
|
+
if (command === "rev-parse" && args[1] === "HEAD") {
|
|
183
|
+
return ok(`${await git.resolveRef({ fs, dir, ref: "HEAD" })}\n`);
|
|
184
|
+
}
|
|
185
|
+
if (command === "branch" && (args.length === 1 || args.includes("--show-current"))) {
|
|
186
|
+
const branch = await git.currentBranch({ fs, dir, fullname: false });
|
|
187
|
+
return ok(args.includes("--show-current") ? `${branch ?? ""}\n` : `${branch ? `* ${branch}` : "* (detached HEAD)"}\n`);
|
|
188
|
+
}
|
|
189
|
+
if (command === "remote" && (args.length === 1 || args.includes("-v"))) {
|
|
190
|
+
const remotes = await git.listRemotes({ fs, dir });
|
|
191
|
+
return ok(remotes.flatMap(({ remote, url }) => args.includes("-v")
|
|
192
|
+
? [`${remote}\t${url} (fetch)`]
|
|
193
|
+
: [remote]).join("\n") + (remotes.length ? "\n" : ""));
|
|
194
|
+
}
|
|
195
|
+
if (command === "ls-files") {
|
|
196
|
+
const files = await git.listFiles({ fs, dir });
|
|
197
|
+
return ok(`${files.join("\n")}${files.length ? "\n" : ""}`);
|
|
198
|
+
}
|
|
199
|
+
return fail([
|
|
200
|
+
`git: '${command ?? ""}' is not implemented by managed git`,
|
|
201
|
+
"Nanocodex git supports clone, status, log, rev-parse HEAD, branch, remote, and ls-files.",
|
|
202
|
+
"",
|
|
203
|
+
].join("\n"));
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
return fail(`git: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function shortStatusCodes(head, workdir, stage) {
|
|
212
|
+
if (head === 0 && stage === 0)
|
|
213
|
+
return workdir === 0 ? [] : ["??"];
|
|
214
|
+
if (head === 1 && stage === 0)
|
|
215
|
+
return workdir === 0
|
|
216
|
+
? ["D "]
|
|
217
|
+
: ["D ", "??"];
|
|
218
|
+
const index = head === 0 ? "A" : stage === head ? " " : "M";
|
|
219
|
+
const workingTree = workdir === stage ? " " : workdir === 0 ? "D" : "M";
|
|
220
|
+
return index === " " && workingTree === " " ? [] : [`${index}${workingTree}`];
|
|
221
|
+
}
|
|
222
|
+
async function gitDirectory(workspace, cwd) {
|
|
223
|
+
let directory = typeof cwd === "string" ? gitWorkspacePath(workspace, cwd) : workspace.root;
|
|
224
|
+
for (;;) {
|
|
225
|
+
if (await workspaceEntry(workspace, `${directory}/.git`))
|
|
226
|
+
return directory;
|
|
227
|
+
if (directory === workspace.root)
|
|
228
|
+
throw new Error("not a git repository");
|
|
229
|
+
directory = directory.slice(0, directory.lastIndexOf("/")) || workspace.root;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function logDepth(args) {
|
|
233
|
+
const compact = args.find((value) => /^-\d+$/.test(value));
|
|
234
|
+
const explicit = option(args, "--max-count", "-n") ?? joinedOption(args, "--max-count");
|
|
235
|
+
const value = explicit ?? compact?.slice(1);
|
|
236
|
+
if (value === undefined)
|
|
237
|
+
return 20;
|
|
238
|
+
const depth = positiveInteger(value, "log depth");
|
|
239
|
+
return depth;
|
|
240
|
+
}
|
|
241
|
+
async function cloneRepository(fetch, workspace, args, context) {
|
|
242
|
+
const { cwd, signal } = context;
|
|
243
|
+
const depthValue = option(args, "--depth", "-") ?? joinedOption(args, "--depth");
|
|
244
|
+
const depth = depthValue === undefined ? undefined : positiveInteger(depthValue, "--depth");
|
|
245
|
+
const branch = option(args, "--branch", "-b") ?? joinedOption(args, "--branch");
|
|
246
|
+
const positionals = gitPositionals(args);
|
|
247
|
+
const remote = positionals[0];
|
|
248
|
+
const match = remote?.match(GITHUB_REPOSITORY);
|
|
249
|
+
if (!match)
|
|
250
|
+
throw new Error("clone requires an https://github.com/OWNER/REPO URL");
|
|
251
|
+
if (branch !== undefined && (!branch || branch.startsWith("-") || branch.includes(".."))) {
|
|
252
|
+
throw new Error("--branch must name one branch or tag");
|
|
253
|
+
}
|
|
254
|
+
const repository = match[1];
|
|
255
|
+
const requestedDestination = positionals[1] ?? repository.slice(repository.indexOf("/") + 1);
|
|
256
|
+
const absoluteWorkspaceDestination = requestedDestination.startsWith(`${workspace.root}/`);
|
|
257
|
+
const destination = absoluteWorkspaceDestination
|
|
258
|
+
? requestedDestination.slice(workspace.root.length + 1)
|
|
259
|
+
: requestedDestination;
|
|
260
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(destination) || destination === "." || destination === "..") {
|
|
261
|
+
throw new Error("clone destination must be one workspace directory name");
|
|
262
|
+
}
|
|
263
|
+
const root = !absoluteWorkspaceDestination
|
|
264
|
+
&& typeof cwd === "string" && cwd.startsWith(`${workspace.root}/`)
|
|
265
|
+
? cwd
|
|
266
|
+
: workspace.root;
|
|
267
|
+
const dir = `${root}/${destination}`;
|
|
268
|
+
if (await workspaceEntry(workspace, dir))
|
|
269
|
+
throw new Error(`destination path '${destination}' already exists`);
|
|
270
|
+
try {
|
|
271
|
+
if (depth === undefined) {
|
|
272
|
+
await downloadRepositoryArchive(fetch, commandWorkspace(workspace, signal), repository, branch ?? "HEAD", dir, signal);
|
|
273
|
+
return `Downloaded source files into '${destination}' (no .git or history).\n`;
|
|
274
|
+
}
|
|
275
|
+
await git.clone({
|
|
276
|
+
fs: workspaceFs(commandWorkspace(workspace, signal)),
|
|
277
|
+
http: managedGitHttp(fetch, signal),
|
|
278
|
+
dir,
|
|
279
|
+
url: `https://github.com/${repository}.git`,
|
|
280
|
+
depth,
|
|
281
|
+
singleBranch: true,
|
|
282
|
+
...(branch === undefined ? {} : { ref: branch }),
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
if (await workspaceEntry(workspace, dir)) {
|
|
287
|
+
await workspace.remove(dir, { recursive: true });
|
|
288
|
+
}
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
return `Cloning into '${destination}'...\n`;
|
|
292
|
+
}
|
|
293
|
+
function managedGitHttp(fetch, signal) {
|
|
294
|
+
return {
|
|
295
|
+
async request(request) {
|
|
296
|
+
signal?.throwIfAborted();
|
|
297
|
+
const body = request.body === undefined
|
|
298
|
+
? undefined
|
|
299
|
+
: await collectGitBody(request.body);
|
|
300
|
+
const response = await (fetch.stream ?? fetch)(request.url, {
|
|
301
|
+
method: request.method,
|
|
302
|
+
headers: request.headers,
|
|
303
|
+
body,
|
|
304
|
+
signal: signal ?? (request.signal instanceof AbortSignal ? request.signal : undefined),
|
|
305
|
+
});
|
|
306
|
+
return {
|
|
307
|
+
url: response.url,
|
|
308
|
+
statusCode: response.status,
|
|
309
|
+
statusMessage: response.statusText,
|
|
310
|
+
headers: response.headers,
|
|
311
|
+
body: (async function* () {
|
|
312
|
+
if (response.body instanceof Uint8Array)
|
|
313
|
+
yield response.body;
|
|
314
|
+
else
|
|
315
|
+
yield* response.body;
|
|
316
|
+
})(),
|
|
317
|
+
};
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
async function collectGitBody(body) {
|
|
322
|
+
const chunks = [];
|
|
323
|
+
let size = 0;
|
|
324
|
+
for await (const chunk of body) {
|
|
325
|
+
size += chunk.byteLength;
|
|
326
|
+
chunks.push(chunk);
|
|
327
|
+
}
|
|
328
|
+
const joined = new Uint8Array(size);
|
|
329
|
+
let offset = 0;
|
|
330
|
+
for (const chunk of chunks) {
|
|
331
|
+
joined.set(chunk, offset);
|
|
332
|
+
offset += chunk.byteLength;
|
|
333
|
+
}
|
|
334
|
+
return joined;
|
|
335
|
+
}
|
|
336
|
+
function workspaceFs(workspace) {
|
|
337
|
+
const resolve = (path) => gitWorkspacePath(workspace, path);
|
|
338
|
+
const promises = {
|
|
339
|
+
readFile: async (path, options) => {
|
|
340
|
+
const contents = await workspace.readFile(resolve(path));
|
|
341
|
+
const encoding = typeof options === "string" ? options : options?.encoding;
|
|
342
|
+
return encoding ? new TextDecoder(encoding).decode(contents) : contents;
|
|
343
|
+
},
|
|
344
|
+
writeFile: async (path, contents) => workspace.writeFile(resolve(path), contents),
|
|
345
|
+
unlink: async (path) => workspace.remove(resolve(path)),
|
|
346
|
+
readdir: async (path) => (await workspace.list(resolve(path)))
|
|
347
|
+
.map(({ path: child }) => child.slice(child.lastIndexOf("/") + 1)),
|
|
348
|
+
mkdir: async (path, options) => {
|
|
349
|
+
path = resolve(path);
|
|
350
|
+
if (options?.recursive) {
|
|
351
|
+
const relative = path.startsWith(workspace.root) ? path.slice(workspace.root.length) : path;
|
|
352
|
+
let current = workspace.root;
|
|
353
|
+
for (const segment of relative.split("/").filter(Boolean)) {
|
|
354
|
+
current += `/${segment}`;
|
|
355
|
+
if (!(await workspaceEntry(workspace, current)))
|
|
356
|
+
await workspace.mkdir(current);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
await workspace.mkdir(path);
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
rmdir: async (path) => workspace.remove(resolve(path)),
|
|
364
|
+
stat: async (path) => gitStat(await requiredWorkspaceEntry(workspace, resolve(path))),
|
|
365
|
+
lstat: async (path) => gitStat(await requiredWorkspaceEntry(workspace, resolve(path))),
|
|
366
|
+
readlink: async () => { throw Object.assign(new Error("symbolic links are unavailable"), { code: "EINVAL" }); },
|
|
367
|
+
symlink: async () => { throw Object.assign(new Error("symbolic links are unavailable"), { code: "ENOSYS" }); },
|
|
368
|
+
chmod: async (path) => { await requiredWorkspaceEntry(workspace, resolve(path)); },
|
|
369
|
+
};
|
|
370
|
+
return { promises };
|
|
371
|
+
}
|
|
372
|
+
function commandWorkspace(workspace, signal) {
|
|
373
|
+
return {
|
|
374
|
+
root: workspace.root,
|
|
375
|
+
list: (...args) => { signal?.throwIfAborted(); return workspace.list(...args); },
|
|
376
|
+
readFile: (...args) => { signal?.throwIfAborted(); return workspace.readFile(...args); },
|
|
377
|
+
writeFile: (...args) => { signal?.throwIfAborted(); return workspace.writeFile(...args); },
|
|
378
|
+
mkdir: (...args) => { signal?.throwIfAborted(); return workspace.mkdir(...args); },
|
|
379
|
+
remove: (...args) => { signal?.throwIfAborted(); return workspace.remove(...args); },
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
function gitWorkspacePath(workspace, path) {
|
|
383
|
+
const source = path.startsWith("/") ? path : `${workspace.root}/${path}`;
|
|
384
|
+
const segments = [];
|
|
385
|
+
for (const segment of source.split("/")) {
|
|
386
|
+
if (!segment || segment === ".")
|
|
387
|
+
continue;
|
|
388
|
+
if (segment === "..")
|
|
389
|
+
segments.pop();
|
|
390
|
+
else
|
|
391
|
+
segments.push(segment);
|
|
392
|
+
}
|
|
393
|
+
const resolved = `/${segments.join("/")}`;
|
|
394
|
+
if (resolved !== workspace.root && !resolved.startsWith(`${workspace.root}/`)) {
|
|
395
|
+
throw Object.assign(new Error(`path escapes ${workspace.root}`), { code: "EPERM" });
|
|
396
|
+
}
|
|
397
|
+
return resolved;
|
|
398
|
+
}
|
|
399
|
+
async function workspaceEntry(workspace, path) {
|
|
400
|
+
if (path === workspace.root)
|
|
401
|
+
return { kind: "directory", path };
|
|
402
|
+
const separator = path.lastIndexOf("/");
|
|
403
|
+
const parent = path.slice(0, separator) || workspace.root;
|
|
404
|
+
const target = path.slice(separator + 1);
|
|
405
|
+
try {
|
|
406
|
+
return (await workspace.list(parent))
|
|
407
|
+
.find((entry) => entry.path === target || entry.path.endsWith(`/${target}`));
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
if (error?.code === "ENOENT")
|
|
411
|
+
return undefined;
|
|
412
|
+
throw error;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
async function requiredWorkspaceEntry(workspace, path) {
|
|
416
|
+
const entry = await workspaceEntry(workspace, path);
|
|
417
|
+
if (entry)
|
|
418
|
+
return entry;
|
|
419
|
+
throw Object.assign(new Error(`${path} does not exist`), { code: "ENOENT" });
|
|
420
|
+
}
|
|
421
|
+
function gitStat(entry) {
|
|
422
|
+
const modified = new Date(entry.modifiedAt ?? 0);
|
|
423
|
+
return {
|
|
424
|
+
isFile: () => entry.kind === "file",
|
|
425
|
+
isDirectory: () => entry.kind === "directory",
|
|
426
|
+
isSymbolicLink: () => false,
|
|
427
|
+
mode: entry.kind === "directory" ? 0o755 : 0o644,
|
|
428
|
+
size: entry.size ?? 0,
|
|
429
|
+
mtime: modified,
|
|
430
|
+
ctime: modified,
|
|
431
|
+
birthtime: modified,
|
|
432
|
+
dev: 0,
|
|
433
|
+
ino: 0,
|
|
434
|
+
uid: 0,
|
|
435
|
+
gid: 0,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
function gitPositionals(args) {
|
|
439
|
+
const positionals = [];
|
|
440
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
441
|
+
const value = args[index];
|
|
442
|
+
if (value === "--depth" || value === "--branch" || value === "-b") {
|
|
443
|
+
index += 1;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
if (value.startsWith("--depth=") || value.startsWith("--branch="))
|
|
447
|
+
continue;
|
|
448
|
+
if (value.startsWith("-"))
|
|
449
|
+
throw new Error(`unsupported clone option '${value}'`);
|
|
450
|
+
positionals.push(value);
|
|
451
|
+
}
|
|
452
|
+
if (positionals.length < 1 || positionals.length > 2)
|
|
453
|
+
throw new Error("clone requires a repository URL and optional destination");
|
|
454
|
+
return positionals;
|
|
455
|
+
}
|
|
456
|
+
function joinedOption(args, name) {
|
|
457
|
+
return args.find((value) => value.startsWith(`${name}=`))?.slice(name.length + 1);
|
|
458
|
+
}
|
|
459
|
+
function positiveInteger(value, name) {
|
|
460
|
+
const parsed = Number(value);
|
|
461
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1)
|
|
462
|
+
throw new Error(`${name} must be a positive integer`);
|
|
463
|
+
return parsed;
|
|
464
|
+
}
|
|
465
|
+
async function github(fetch, path, options = {}) {
|
|
466
|
+
const url = new URL(path, "https://api.github.com");
|
|
467
|
+
if (url.origin !== "https://api.github.com")
|
|
468
|
+
throw new Error("endpoint is outside api.github.com");
|
|
469
|
+
const response = await fetch(url.href, {
|
|
470
|
+
method: options.method,
|
|
471
|
+
headers: {
|
|
472
|
+
accept: "application/vnd.github+json",
|
|
473
|
+
...(options.body === undefined ? {} : { "content-type": "application/json" }),
|
|
474
|
+
},
|
|
475
|
+
body: options.body,
|
|
476
|
+
});
|
|
477
|
+
const raw = new TextDecoder().decode(response.body);
|
|
478
|
+
let value;
|
|
479
|
+
try {
|
|
480
|
+
value = raw ? JSON.parse(raw) : null;
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
throw new Error(`GitHub returned invalid JSON (HTTP ${response.status})`);
|
|
484
|
+
}
|
|
485
|
+
if (response.status < 200 || response.status >= 300) {
|
|
486
|
+
const detail = value && typeof value === "object"
|
|
487
|
+
? optionalText(value, "message")
|
|
488
|
+
?? optionalText(value, "error")
|
|
489
|
+
: undefined;
|
|
490
|
+
throw new Error(`GitHub request failed (HTTP ${response.status}${detail ? `: ${detail}` : ""})`);
|
|
491
|
+
}
|
|
492
|
+
return value;
|
|
493
|
+
}
|
|
494
|
+
function apiFields(args) {
|
|
495
|
+
const fields = Object.create(null);
|
|
496
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
497
|
+
if (!["-f", "-F", "--field", "--raw-field"].includes(args[index]))
|
|
498
|
+
continue;
|
|
499
|
+
const field = args[index + 1];
|
|
500
|
+
const separator = field?.indexOf("=") ?? -1;
|
|
501
|
+
if (!field || separator <= 0)
|
|
502
|
+
throw new Error(`${args[index]} requires key=value`);
|
|
503
|
+
fields[field.slice(0, separator)] = field.slice(separator + 1);
|
|
504
|
+
index += 1;
|
|
505
|
+
}
|
|
506
|
+
return fields;
|
|
507
|
+
}
|
|
508
|
+
function option(args, long, short) {
|
|
509
|
+
const index = args.findIndex((value) => value === long || value === short);
|
|
510
|
+
return index === -1 ? undefined : args[index + 1];
|
|
511
|
+
}
|
|
512
|
+
function positional(args, optionsWithValues) {
|
|
513
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
514
|
+
const value = args[index];
|
|
515
|
+
if (optionsWithValues.includes(value)) {
|
|
516
|
+
index += 1;
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
if (!value.startsWith("-"))
|
|
520
|
+
return value;
|
|
521
|
+
}
|
|
522
|
+
return undefined;
|
|
523
|
+
}
|
|
524
|
+
function limit(value) {
|
|
525
|
+
if (value === undefined)
|
|
526
|
+
return 30;
|
|
527
|
+
const parsed = Number(value);
|
|
528
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > 100) {
|
|
529
|
+
throw new Error("--limit must be an integer from 1 to 100");
|
|
530
|
+
}
|
|
531
|
+
return parsed;
|
|
532
|
+
}
|
|
533
|
+
function requireRepository(value, message) {
|
|
534
|
+
if (!value || !REPOSITORY.test(value))
|
|
535
|
+
throw new Error(message);
|
|
536
|
+
}
|
|
537
|
+
function requireRecord(value, name) {
|
|
538
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
539
|
+
throw new Error(`GitHub returned an invalid ${name}`);
|
|
540
|
+
}
|
|
541
|
+
return value;
|
|
542
|
+
}
|
|
543
|
+
function text(value, key) {
|
|
544
|
+
const field = requireRecord(value, "response")[key];
|
|
545
|
+
if (typeof field !== "string")
|
|
546
|
+
throw new Error(`GitHub response is missing ${key}`);
|
|
547
|
+
return field;
|
|
548
|
+
}
|
|
549
|
+
function optionalText(value, key) {
|
|
550
|
+
return typeof value[key] === "string" ? value[key] : undefined;
|
|
551
|
+
}
|
|
552
|
+
function ok(stdout) { return { stdout, stderr: "", exitCode: 0 }; }
|
|
553
|
+
function fail(stderr) { return { stdout: "", stderr, exitCode: 1 }; }
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type SshCommandResult, type SshIdentityReferenceRequest } from "../tools/ssh.mjs";
|
|
2
|
+
import type { Workspace } from "../tools/types.mjs";
|
|
3
|
+
export type SshSocket = Readonly<{
|
|
4
|
+
readable: ReadableStream<Uint8Array>;
|
|
5
|
+
writable: WritableStream<Uint8Array>;
|
|
6
|
+
opened: Promise<unknown>;
|
|
7
|
+
closed: Promise<void>;
|
|
8
|
+
close(): Promise<void>;
|
|
9
|
+
}>;
|
|
10
|
+
export type SshConnect = (address: Readonly<{
|
|
11
|
+
hostname: string;
|
|
12
|
+
port: number;
|
|
13
|
+
}>, options: Readonly<{
|
|
14
|
+
allowHalfOpen: boolean;
|
|
15
|
+
secureTransport: "off";
|
|
16
|
+
}>) => SshSocket;
|
|
17
|
+
/** Creates a workspace-backed SSH command over an injected Web Streams socket transport. */
|
|
18
|
+
export declare function createWorkspaceSshCommand(options: Readonly<{
|
|
19
|
+
connect: SshConnect;
|
|
20
|
+
filesystem(): Workspace;
|
|
21
|
+
resolvePassword?(reference: string): Promise<string>;
|
|
22
|
+
executeWithIdentityReference?(request: SshIdentityReferenceRequest, context: Readonly<{
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
}>): Promise<SshCommandResult>;
|
|
25
|
+
}>): Readonly<{
|
|
26
|
+
name: "ssh";
|
|
27
|
+
trusted?: boolean;
|
|
28
|
+
execute(args: string[], context: unknown): Promise<SshCommandResult>;
|
|
29
|
+
}>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createSshCommand, createWebStreamSshStream, } from "../tools/ssh.mjs";
|
|
2
|
+
/** Creates a workspace-backed SSH command over an injected Web Streams socket transport. */
|
|
3
|
+
export function createWorkspaceSshCommand(options) {
|
|
4
|
+
return createSshCommand({
|
|
5
|
+
transport: "tcp",
|
|
6
|
+
maxOutputBytes: 4 * 1024 * 1024,
|
|
7
|
+
readIdentity: async (path, context) => new TextDecoder().decode(await options.filesystem().readFile(resolveWorkspacePath(options.filesystem().root, context.cwd, path))),
|
|
8
|
+
...(options.resolvePassword === undefined
|
|
9
|
+
? {}
|
|
10
|
+
: { resolvePassword: options.resolvePassword }),
|
|
11
|
+
...(options.executeWithIdentityReference === undefined
|
|
12
|
+
? {}
|
|
13
|
+
: { executeWithIdentityReference: options.executeWithIdentityReference }),
|
|
14
|
+
async openStream(endpoint, signal) {
|
|
15
|
+
if (endpoint instanceof URL)
|
|
16
|
+
throw new Error("TCP SSH requires a host and port");
|
|
17
|
+
const socket = options.connect(endpoint, { allowHalfOpen: true, secureTransport: "off" });
|
|
18
|
+
try {
|
|
19
|
+
await abortable(socket.opened, signal);
|
|
20
|
+
return createWebStreamSshStream(socket, signal);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
await socket.close();
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function resolveWorkspacePath(root, cwd, path) {
|
|
30
|
+
const absolute = path.startsWith("/");
|
|
31
|
+
if (absolute && path !== root && !path.startsWith(`${root}/`)) {
|
|
32
|
+
throw new Error(`SSH identity path must stay within ${root}`);
|
|
33
|
+
}
|
|
34
|
+
const parts = absolute ? [] : relativeParts(root, cwd);
|
|
35
|
+
const input = absolute ? path.slice(root.length) : path;
|
|
36
|
+
for (const part of input.split("/")) {
|
|
37
|
+
if (!part || part === ".")
|
|
38
|
+
continue;
|
|
39
|
+
if (part === "..") {
|
|
40
|
+
if (parts.length === 0)
|
|
41
|
+
throw new Error(`SSH identity path escapes ${root}`);
|
|
42
|
+
parts.pop();
|
|
43
|
+
}
|
|
44
|
+
else
|
|
45
|
+
parts.push(part);
|
|
46
|
+
}
|
|
47
|
+
return parts.length === 0 ? root : `${root}/${parts.join("/")}`;
|
|
48
|
+
}
|
|
49
|
+
function relativeParts(root, path) {
|
|
50
|
+
if (path === root)
|
|
51
|
+
return [];
|
|
52
|
+
if (!path.startsWith(`${root}/`))
|
|
53
|
+
throw new Error(`SSH cwd must stay within ${root}`);
|
|
54
|
+
return path.slice(root.length + 1).split("/").filter(Boolean);
|
|
55
|
+
}
|
|
56
|
+
function abortable(promise, signal) {
|
|
57
|
+
if (signal === undefined)
|
|
58
|
+
return promise;
|
|
59
|
+
if (signal.aborted)
|
|
60
|
+
return Promise.reject(signal.reason ?? new Error("SSH command cancelled"));
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const abort = () => reject(signal.reason ?? new Error("SSH command cancelled"));
|
|
63
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
64
|
+
promise.then((value) => { signal.removeEventListener("abort", abort); resolve(value); }, (error) => { signal.removeEventListener("abort", abort); reject(error); });
|
|
65
|
+
});
|
|
66
|
+
}
|