agent-dev-env 0.1.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 +21 -0
- package/dist/assets/bridge/bridge.js +202 -0
- package/dist/assets/guest/guest-agent-mac.js +586 -0
- package/dist/assets/guest/guest-agent-ubuntu.js +481 -0
- package/dist/assets/guest/guest-agent-windows.js +466 -0
- package/dist/assets/images/mac/CHANGELOG.md +265 -0
- package/dist/assets/images/mac/README.md +63 -0
- package/dist/assets/images/mac/sandbox.pkr.hcl +468 -0
- package/dist/assets/images/mac/vars/sandbox-macos-tahoe.pkrvars.hcl +39 -0
- package/dist/assets/images/ubuntu-arm64-vmware/CHANGELOG.md +239 -0
- package/dist/assets/images/ubuntu-arm64-vmware/README.md +177 -0
- package/dist/assets/images/ubuntu-arm64-vmware/autoinstall/meta-data +2 -0
- package/dist/assets/images/ubuntu-arm64-vmware/autoinstall/user-data +34 -0
- package/dist/assets/images/ubuntu-arm64-vmware/sandbox.pkr.hcl +776 -0
- package/dist/assets/images/ubuntu-arm64-vmware/vars/sandbox-ubuntu-24-04-arm64-vmware.pkrvars.hcl +71 -0
- package/dist/assets/images/windows-arm64-qemu/CHANGELOG.md +209 -0
- package/dist/assets/images/windows-arm64-qemu/README.md +142 -0
- package/dist/assets/images/windows-arm64-qemu/autounattend.xml +325 -0
- package/dist/assets/images/windows-arm64-qemu/qemu-with-tpm.sh +170 -0
- package/dist/assets/images/windows-arm64-qemu/sandbox.pkr.hcl +1012 -0
- package/dist/assets/images/windows-arm64-qemu/vars/sandbox-windows-11-arm64-qemu.pkrvars.hcl +83 -0
- package/dist/assets/images/windows-arm64-vmware/CHANGELOG.md +270 -0
- package/dist/assets/images/windows-arm64-vmware/README.md +171 -0
- package/dist/assets/images/windows-arm64-vmware/autounattend.xml +311 -0
- package/dist/assets/images/windows-arm64-vmware/sandbox.pkr.hcl +1036 -0
- package/dist/assets/images/windows-arm64-vmware/vars/sandbox-windows-11-arm64-vmware.pkrvars.hcl +83 -0
- package/dist/assets/rules/agent-rules-linux.md +68 -0
- package/dist/assets/rules/agent-rules.md +71 -0
- package/dist/assets/watchdog/watch-build-ocr.swift +40 -0
- package/dist/assets/watchdog/watch-build.py +275 -0
- package/dist/cli.js +52 -0
- package/dist/commands/delete.js +179 -0
- package/dist/commands/doctor.js +165 -0
- package/dist/commands/list.js +38 -0
- package/dist/commands/not-yet.js +15 -0
- package/dist/commands/register.js +150 -0
- package/dist/commands/run.js +65 -0
- package/dist/commands/status.js +142 -0
- package/dist/commands/stop.js +186 -0
- package/dist/commands/sync.js +98 -0
- package/dist/lib/exec.js +250 -0
- package/dist/lib/ghcr.js +82 -0
- package/dist/lib/git.js +100 -0
- package/dist/lib/logger.js +87 -0
- package/dist/lib/network.js +63 -0
- package/dist/lib/paths.js +116 -0
- package/dist/lib/platform.js +87 -0
- package/dist/lib/prompt.js +66 -0
- package/dist/lib/qemu.js +373 -0
- package/dist/lib/regex.js +9 -0
- package/dist/lib/ssh.js +230 -0
- package/dist/lib/tart.js +210 -0
- package/dist/lib/template.js +35 -0
- package/dist/lib/vars.js +99 -0
- package/dist/lib/vmrun.js +334 -0
- package/dist/lifecycle/build-macos.js +23 -0
- package/dist/lifecycle/build-qemu.js +204 -0
- package/dist/lifecycle/build-shared.js +355 -0
- package/dist/lifecycle/build-ubuntu.js +184 -0
- package/dist/lifecycle/build-watchdog.js +146 -0
- package/dist/lifecycle/build-windows-vmware.js +112 -0
- package/dist/lifecycle/build.js +55 -0
- package/dist/lifecycle/catalog.js +165 -0
- package/dist/lifecycle/deploy.js +161 -0
- package/dist/lifecycle/tag.js +77 -0
- package/dist/lifecycle/watch-build.js +64 -0
- package/dist/runners/bridges.js +153 -0
- package/dist/runners/framework.js +66 -0
- package/dist/runners/macos-bridges.js +143 -0
- package/dist/runners/macos-guest.js +76 -0
- package/dist/runners/macos-rules.js +77 -0
- package/dist/runners/macos-summary.js +145 -0
- package/dist/runners/macos.js +258 -0
- package/dist/runners/openchamber.js +60 -0
- package/dist/runners/options.js +56 -0
- package/dist/runners/qemu-image.js +183 -0
- package/dist/runners/rules.js +49 -0
- package/dist/runners/ubuntu-bridges.js +154 -0
- package/dist/runners/ubuntu-guest.js +136 -0
- package/dist/runners/ubuntu-image.js +26 -0
- package/dist/runners/ubuntu-rules.js +86 -0
- package/dist/runners/ubuntu-shared.js +65 -0
- package/dist/runners/ubuntu-summary.js +141 -0
- package/dist/runners/ubuntu.js +117 -0
- package/dist/runners/vmware-common.js +158 -0
- package/dist/runners/vmware-image.js +242 -0
- package/dist/runners/windows-autologon.js +119 -0
- package/dist/runners/windows-bridges.js +136 -0
- package/dist/runners/windows-guest.js +213 -0
- package/dist/runners/windows-image.js +23 -0
- package/dist/runners/windows-qemu-summary.js +85 -0
- package/dist/runners/windows-qemu.js +189 -0
- package/dist/runners/windows-shared.js +68 -0
- package/dist/runners/windows-summary.js +104 -0
- package/dist/runners/windows.js +96 -0
- package/dist/settings/common.js +107 -0
- package/dist/settings/macos-copy.js +223 -0
- package/dist/settings/macos.js +41 -0
- package/dist/settings/ubuntu-copy.js +223 -0
- package/dist/settings/ubuntu.js +68 -0
- package/package.json +52 -0
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// packages/guest-agent-mac/src/index.ts
|
|
4
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
5
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join as join2 } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
// packages/bridge-core/src/index.ts
|
|
11
|
+
import { rmSync, writeFileSync } from "node:fs";
|
|
12
|
+
|
|
13
|
+
// packages/bridge-core/src/endpoints.ts
|
|
14
|
+
var PIPE_PREFIX = "\\\\.\\pipe\\";
|
|
15
|
+
function parseEndpoint(spec) {
|
|
16
|
+
const sep = spec.indexOf(":");
|
|
17
|
+
if (sep <= 0) {
|
|
18
|
+
throw new Error(`invalid endpoint '${spec}' (expected unix:|pipe:|tcp:)`);
|
|
19
|
+
}
|
|
20
|
+
const scheme = spec.slice(0, sep);
|
|
21
|
+
const rest = spec.slice(sep + 1);
|
|
22
|
+
switch (scheme) {
|
|
23
|
+
case "unix": {
|
|
24
|
+
if (!rest) {
|
|
25
|
+
throw new Error(`invalid unix endpoint '${spec}': missing path`);
|
|
26
|
+
}
|
|
27
|
+
return { kind: "unix", path: rest };
|
|
28
|
+
}
|
|
29
|
+
case "pipe": {
|
|
30
|
+
if (!rest) {
|
|
31
|
+
throw new Error(`invalid pipe endpoint '${spec}': missing name`);
|
|
32
|
+
}
|
|
33
|
+
const name = rest.startsWith(PIPE_PREFIX) ? rest : `${PIPE_PREFIX}${rest}`;
|
|
34
|
+
return { kind: "pipe", name };
|
|
35
|
+
}
|
|
36
|
+
case "tcp": {
|
|
37
|
+
const m = /^([^:]+):(\d+)$/.exec(rest);
|
|
38
|
+
const port = m ? Number(m[2]) : NaN;
|
|
39
|
+
if (!m || !Number.isInteger(port) || port < 1 || port > 65535) {
|
|
40
|
+
throw new Error(`invalid tcp endpoint '${spec}' (expected tcp:host:port, port 1-65535)`);
|
|
41
|
+
}
|
|
42
|
+
return { kind: "tcp", host: m[1], port };
|
|
43
|
+
}
|
|
44
|
+
default:
|
|
45
|
+
throw new Error(
|
|
46
|
+
`unknown endpoint scheme '${scheme}' in '${spec}' (expected unix:|pipe:|tcp:)`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function formatEndpoint(endpoint) {
|
|
51
|
+
switch (endpoint.kind) {
|
|
52
|
+
case "unix":
|
|
53
|
+
return `unix:${endpoint.path}`;
|
|
54
|
+
case "pipe":
|
|
55
|
+
return `pipe:${endpoint.name}`;
|
|
56
|
+
case "tcp":
|
|
57
|
+
return `tcp:${endpoint.host}:${endpoint.port}`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// packages/bridge-core/src/forwarder.ts
|
|
62
|
+
import net from "node:net";
|
|
63
|
+
import { existsSync, unlinkSync } from "node:fs";
|
|
64
|
+
function startForwarder(options) {
|
|
65
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
66
|
+
const server = net.createServer((client) => {
|
|
67
|
+
sockets.add(client);
|
|
68
|
+
client.on("close", () => sockets.delete(client));
|
|
69
|
+
forwardConnection(client, options.forward, sockets);
|
|
70
|
+
});
|
|
71
|
+
return bind(server, options.listen).then(() => {
|
|
72
|
+
options.log?.(
|
|
73
|
+
`listening on ${formatEndpoint(options.listen)} \u2192 ${formatEndpoint(options.forward)}`
|
|
74
|
+
);
|
|
75
|
+
return {
|
|
76
|
+
close: () => closeForwarder(server, sockets, options.listen, options.log)
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function forwardConnection(client, target, sockets) {
|
|
81
|
+
const upstream = connectTo(target);
|
|
82
|
+
sockets.add(upstream);
|
|
83
|
+
upstream.on("close", () => sockets.delete(upstream));
|
|
84
|
+
upstream.once("connect", () => {
|
|
85
|
+
client.pipe(upstream);
|
|
86
|
+
upstream.pipe(client);
|
|
87
|
+
});
|
|
88
|
+
const tearDown = () => {
|
|
89
|
+
client.destroy();
|
|
90
|
+
upstream.destroy();
|
|
91
|
+
};
|
|
92
|
+
upstream.once("error", tearDown);
|
|
93
|
+
client.once("error", () => {
|
|
94
|
+
upstream.destroy();
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
async function bind(server, endpoint) {
|
|
98
|
+
switch (endpoint.kind) {
|
|
99
|
+
case "unix":
|
|
100
|
+
if (existsSync(endpoint.path)) {
|
|
101
|
+
unlinkSync(endpoint.path);
|
|
102
|
+
}
|
|
103
|
+
await listen(server, endpoint.path);
|
|
104
|
+
break;
|
|
105
|
+
case "pipe":
|
|
106
|
+
await listen(server, endpoint.name);
|
|
107
|
+
break;
|
|
108
|
+
case "tcp":
|
|
109
|
+
await listen(server, { port: endpoint.port, host: endpoint.host });
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function listen(server, spec) {
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
server.once("error", reject);
|
|
116
|
+
server.listen(spec, () => {
|
|
117
|
+
server.removeListener("error", reject);
|
|
118
|
+
resolve();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function connectTo(endpoint) {
|
|
123
|
+
switch (endpoint.kind) {
|
|
124
|
+
case "unix":
|
|
125
|
+
return net.connect({ path: endpoint.path });
|
|
126
|
+
case "pipe":
|
|
127
|
+
return net.connect({ path: endpoint.name });
|
|
128
|
+
case "tcp":
|
|
129
|
+
return net.connect({ port: endpoint.port, host: endpoint.host });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function closeForwarder(server, sockets, listen2, log) {
|
|
133
|
+
return new Promise((resolve) => {
|
|
134
|
+
for (const socket of sockets) {
|
|
135
|
+
socket.destroy();
|
|
136
|
+
}
|
|
137
|
+
server.close(() => {
|
|
138
|
+
if (listen2.kind === "unix" && existsSync(listen2.path)) {
|
|
139
|
+
unlinkSync(listen2.path);
|
|
140
|
+
}
|
|
141
|
+
log?.("closed");
|
|
142
|
+
resolve();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// packages/bridge-core/src/index.ts
|
|
148
|
+
async function runBridge(options) {
|
|
149
|
+
const running = await startForwarder(options);
|
|
150
|
+
if (options.pidfile) {
|
|
151
|
+
writeFileSync(options.pidfile, `${process.pid}
|
|
152
|
+
`);
|
|
153
|
+
}
|
|
154
|
+
await new Promise((resolve) => {
|
|
155
|
+
const stop = () => {
|
|
156
|
+
void running.close().then(() => {
|
|
157
|
+
if (options.pidfile) {
|
|
158
|
+
rmSync(options.pidfile, { force: true });
|
|
159
|
+
}
|
|
160
|
+
resolve();
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
process.on("SIGINT", stop);
|
|
164
|
+
process.on("SIGTERM", stop);
|
|
165
|
+
});
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// packages/bridge-core/src/probe.ts
|
|
170
|
+
import net2 from "node:net";
|
|
171
|
+
function canConnect(endpoint, timeoutMs = 500) {
|
|
172
|
+
return new Promise((resolve) => {
|
|
173
|
+
const socket = endpoint.kind === "tcp" ? net2.connect({ port: endpoint.port, host: endpoint.host }) : net2.connect({ path: endpoint.kind === "unix" ? endpoint.path : endpoint.name });
|
|
174
|
+
const done = (ok) => {
|
|
175
|
+
socket.destroy();
|
|
176
|
+
resolve(ok);
|
|
177
|
+
};
|
|
178
|
+
socket.once("connect", () => done(true));
|
|
179
|
+
socket.once("error", () => done(false));
|
|
180
|
+
setTimeout(() => done(false), timeoutMs);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// packages/guest-rules/src/rules.ts
|
|
185
|
+
import { createHash } from "node:crypto";
|
|
186
|
+
import { mkdirSync, readFileSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
187
|
+
import { join } from "node:path";
|
|
188
|
+
function rulesPaths(home) {
|
|
189
|
+
return {
|
|
190
|
+
marker: join(home, ".config", "agent-dev-env", "agent-rules.sha256"),
|
|
191
|
+
targets: [
|
|
192
|
+
join(home, ".config", "opencode", "AGENTS.md"),
|
|
193
|
+
join(home, ".copilot", "copilot-instructions.md")
|
|
194
|
+
]
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function sha256(text) {
|
|
198
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
199
|
+
}
|
|
200
|
+
function readMaybe(path) {
|
|
201
|
+
try {
|
|
202
|
+
return readFileSync(path, "utf8");
|
|
203
|
+
} catch {
|
|
204
|
+
return void 0;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function rulesAction(home, content) {
|
|
208
|
+
const { marker, targets } = rulesPaths(home);
|
|
209
|
+
const newSha = sha256(content);
|
|
210
|
+
const prevSha = readMaybe(marker)?.trim();
|
|
211
|
+
let conflict = false;
|
|
212
|
+
let install2 = false;
|
|
213
|
+
let update = false;
|
|
214
|
+
for (const target of targets) {
|
|
215
|
+
const current = readMaybe(target);
|
|
216
|
+
if (current === void 0) {
|
|
217
|
+
install2 = true;
|
|
218
|
+
} else if (sha256(current) === newSha) {
|
|
219
|
+
} else if (prevSha !== void 0 && sha256(current) === prevSha) {
|
|
220
|
+
update = true;
|
|
221
|
+
} else {
|
|
222
|
+
conflict = true;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (conflict) return "conflict";
|
|
226
|
+
if (install2) return "install";
|
|
227
|
+
if (update) return "update";
|
|
228
|
+
return "uptodate";
|
|
229
|
+
}
|
|
230
|
+
function applyRules(home, content) {
|
|
231
|
+
const { marker, targets } = rulesPaths(home);
|
|
232
|
+
for (const target of targets) {
|
|
233
|
+
mkdirSync(join(target, ".."), { recursive: true });
|
|
234
|
+
writeFileSync2(target, content);
|
|
235
|
+
}
|
|
236
|
+
mkdirSync(join(marker, ".."), { recursive: true });
|
|
237
|
+
writeFileSync2(marker, `${sha256(content)}
|
|
238
|
+
`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// packages/guest-agent-mac/src/system.ts
|
|
242
|
+
var LAUNCHD_LABELS = {
|
|
243
|
+
"ssh-agent": "dev.agent-sandbox.ssh-agent",
|
|
244
|
+
docker: "dev.agent-sandbox.docker"
|
|
245
|
+
};
|
|
246
|
+
var GUEST_AGENT_SOCKET = "/tmp/ssh-agent.sock";
|
|
247
|
+
var ZPROFILE_MARKERS = {
|
|
248
|
+
agent: "# SSH agent bridge to the host (see docs/ssh-agent.md)",
|
|
249
|
+
env: "# Docker env vars for the host engine (testcontainers support, see docs/macos.md)"
|
|
250
|
+
};
|
|
251
|
+
var SSH_CONFIG_MARKER = "# SSH agent bridge to the host (see docs/ssh-agent.md)";
|
|
252
|
+
function escapeXml(text) {
|
|
253
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
254
|
+
}
|
|
255
|
+
function launchdPlist(bridge, dockerSocketPath) {
|
|
256
|
+
const args = [
|
|
257
|
+
bridge.nodePath,
|
|
258
|
+
bridge.agentPath,
|
|
259
|
+
"bridge",
|
|
260
|
+
bridge.role,
|
|
261
|
+
"--port",
|
|
262
|
+
String(bridge.port),
|
|
263
|
+
"--host-alias",
|
|
264
|
+
bridge.hostAlias,
|
|
265
|
+
"--listen",
|
|
266
|
+
bridge.role === "ssh-agent" ? `unix:${GUEST_AGENT_SOCKET}` : `unix:${dockerSocketPath}`,
|
|
267
|
+
"--forward",
|
|
268
|
+
`tcp:${bridge.hostAlias}:${bridge.port}`
|
|
269
|
+
];
|
|
270
|
+
return [
|
|
271
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
272
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"',
|
|
273
|
+
' "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
274
|
+
'<plist version="1.0">',
|
|
275
|
+
"<dict>",
|
|
276
|
+
` <key>Label</key><string>${escapeXml(bridge.label)}</string>`,
|
|
277
|
+
" <key>ProgramArguments</key>",
|
|
278
|
+
" <array>",
|
|
279
|
+
...args.map((a) => ` <string>${escapeXml(a)}</string>`),
|
|
280
|
+
" </array>",
|
|
281
|
+
" <key>RunAtLoad</key><true/>",
|
|
282
|
+
" <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>",
|
|
283
|
+
" <key>EnvironmentVariables</key>",
|
|
284
|
+
" <dict>",
|
|
285
|
+
" <key>PATH</key><string>/opt/homebrew/bin:/usr/bin:/bin</string>",
|
|
286
|
+
" </dict>",
|
|
287
|
+
"</dict>",
|
|
288
|
+
"</plist>",
|
|
289
|
+
""
|
|
290
|
+
].join("\n");
|
|
291
|
+
}
|
|
292
|
+
function zprofileAgentBlock() {
|
|
293
|
+
return [
|
|
294
|
+
"",
|
|
295
|
+
ZPROFILE_MARKERS.agent,
|
|
296
|
+
'if [ -z "${SSH_AUTH_SOCK:-}" ] || [ "$SSH_AUTH_SOCK" != "/tmp/ssh-agent.sock" ]; then',
|
|
297
|
+
" export SSH_AUTH_SOCK=/tmp/ssh-agent.sock",
|
|
298
|
+
"fi",
|
|
299
|
+
"",
|
|
300
|
+
""
|
|
301
|
+
].join("\n");
|
|
302
|
+
}
|
|
303
|
+
function zprofileDockerBlock() {
|
|
304
|
+
return [
|
|
305
|
+
"",
|
|
306
|
+
ZPROFILE_MARKERS.env,
|
|
307
|
+
'export DOCKER_HOST="unix://$HOME/.docker/run/docker.sock"',
|
|
308
|
+
`export TESTCONTAINERS_HOST_OVERRIDE="$(netstat -nr | awk '/default/{print $2; exit}')"`,
|
|
309
|
+
"",
|
|
310
|
+
""
|
|
311
|
+
].join("\n");
|
|
312
|
+
}
|
|
313
|
+
function sshConfigBlock() {
|
|
314
|
+
return ["", SSH_CONFIG_MARKER, "Host *", " IdentityAgent /tmp/ssh-agent.sock", "", ""].join(
|
|
315
|
+
"\n"
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
function dockerContextArgs(docker, sock) {
|
|
319
|
+
const endpoint = `host=unix://${sock}`;
|
|
320
|
+
return [
|
|
321
|
+
[docker, "context", "create", "host", "--docker", endpoint],
|
|
322
|
+
[docker, "context", "update", "host", "--docker", endpoint],
|
|
323
|
+
[docker, "context", "use", "host"]
|
|
324
|
+
];
|
|
325
|
+
}
|
|
326
|
+
function appendBlockIfMissing(content, block, marker) {
|
|
327
|
+
return content.includes(marker) ? content : `${maybeNewline(content)}${block}`;
|
|
328
|
+
}
|
|
329
|
+
function removeBlock(content, marker) {
|
|
330
|
+
const lines = content.split("\n");
|
|
331
|
+
const markerIndex = lines.findIndex((line) => line === marker);
|
|
332
|
+
if (markerIndex === -1) {
|
|
333
|
+
return content;
|
|
334
|
+
}
|
|
335
|
+
const blockStart = markerIndex > 0 && lines[markerIndex - 1] === "" ? markerIndex - 1 : markerIndex;
|
|
336
|
+
let blockEnd = markerIndex + 1;
|
|
337
|
+
while (blockEnd < lines.length && lines[blockEnd] !== "") {
|
|
338
|
+
blockEnd += 1;
|
|
339
|
+
}
|
|
340
|
+
if (blockEnd < lines.length) {
|
|
341
|
+
blockEnd += 1;
|
|
342
|
+
}
|
|
343
|
+
return [...lines.slice(0, blockStart), ...lines.slice(blockEnd)].join("\n").replace(/\n{3,}/g, "\n\n");
|
|
344
|
+
}
|
|
345
|
+
function maybeNewline(content) {
|
|
346
|
+
return content.length === 0 || content.endsWith("\n") ? "" : "\n";
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// packages/guest-agent-mac/src/index.ts
|
|
350
|
+
var AGENT_PATH = fileURLToPath(import.meta.url);
|
|
351
|
+
var NODE_PATH = process.execPath;
|
|
352
|
+
var HOME = homedir();
|
|
353
|
+
var LAUNCH_AGENTS_DIR = join2(HOME, "Library", "LaunchAgents");
|
|
354
|
+
var DOCKER_SOCKET = join2(HOME, ".docker", "run", "docker.sock");
|
|
355
|
+
var DEFAULTS = {
|
|
356
|
+
agentPort: 4100,
|
|
357
|
+
dockerPort: 4101
|
|
358
|
+
};
|
|
359
|
+
async function main(argv) {
|
|
360
|
+
const [cmd, ...rest] = argv;
|
|
361
|
+
try {
|
|
362
|
+
switch (cmd) {
|
|
363
|
+
case "install":
|
|
364
|
+
return install(rest);
|
|
365
|
+
case "bridge":
|
|
366
|
+
return bridgeCommand(rest);
|
|
367
|
+
case "status":
|
|
368
|
+
return status();
|
|
369
|
+
case "rules":
|
|
370
|
+
return rulesCommand(rest);
|
|
371
|
+
case "uninstall":
|
|
372
|
+
return uninstall();
|
|
373
|
+
default:
|
|
374
|
+
process.stderr.write(usage());
|
|
375
|
+
return 1;
|
|
376
|
+
}
|
|
377
|
+
} catch (err) {
|
|
378
|
+
process.stderr.write(`guest-agent-mac: ${err.message}
|
|
379
|
+
`);
|
|
380
|
+
return 1;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function usage() {
|
|
384
|
+
return [
|
|
385
|
+
"usage: guest-agent-mac <install|bridge|status|rules|uninstall> [options]",
|
|
386
|
+
" install [--agent-port N] [--docker-port N] [--host-alias A]",
|
|
387
|
+
" bridge <ssh-agent|docker> [--listen EP] [--forward EP] [--port N] [--host-alias A]",
|
|
388
|
+
" rules [--force] (content on stdin)",
|
|
389
|
+
""
|
|
390
|
+
].join("\n");
|
|
391
|
+
}
|
|
392
|
+
function argValue(argv, flag) {
|
|
393
|
+
const index = argv.indexOf(flag);
|
|
394
|
+
return index === -1 ? void 0 : argv[index + 1];
|
|
395
|
+
}
|
|
396
|
+
function gatewayFromNetstat(output) {
|
|
397
|
+
for (const line of output.split("\n")) {
|
|
398
|
+
if (!/\sdefault\s/.test(line)) {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
const fields = line.trim().split(/\s+/);
|
|
402
|
+
const gw = fields[1];
|
|
403
|
+
if (gw) {
|
|
404
|
+
return gw;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return void 0;
|
|
408
|
+
}
|
|
409
|
+
function defaultGateway() {
|
|
410
|
+
const res = execFileSync("netstat", ["-nr"], { encoding: "utf8" });
|
|
411
|
+
return gatewayFromNetstat(res);
|
|
412
|
+
}
|
|
413
|
+
function bridgeFor(role, port, hostAlias) {
|
|
414
|
+
return {
|
|
415
|
+
label: LAUNCHD_LABELS[role],
|
|
416
|
+
nodePath: NODE_PATH,
|
|
417
|
+
agentPath: AGENT_PATH,
|
|
418
|
+
role,
|
|
419
|
+
port,
|
|
420
|
+
hostAlias
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
function install(argv) {
|
|
424
|
+
const hostAlias = argValue(argv, "--host-alias") ?? defaultGateway() ?? (() => {
|
|
425
|
+
throw new Error("could not determine the host gateway (--host-alias)");
|
|
426
|
+
})();
|
|
427
|
+
const agentPort = Number(argValue(argv, "--agent-port") ?? DEFAULTS.agentPort);
|
|
428
|
+
const dockerPort = Number(argValue(argv, "--docker-port") ?? DEFAULTS.dockerPort);
|
|
429
|
+
mkdirSync2(LAUNCH_AGENTS_DIR, { recursive: true });
|
|
430
|
+
const uid = execFileSync("id", ["-u"], { encoding: "utf8" }).trim();
|
|
431
|
+
for (const [role, port] of [
|
|
432
|
+
["ssh-agent", agentPort],
|
|
433
|
+
["docker", dockerPort]
|
|
434
|
+
]) {
|
|
435
|
+
const bridge = bridgeFor(role, port, hostAlias);
|
|
436
|
+
const plistPath = join2(LAUNCH_AGENTS_DIR, `${bridge.label}.plist`);
|
|
437
|
+
writeFileSync3(plistPath, launchdPlist(bridge, DOCKER_SOCKET));
|
|
438
|
+
reloadAgent(bridge.label, plistPath, uid);
|
|
439
|
+
process.stdout.write(`installed:${role} (${bridge.label}, port ${port})
|
|
440
|
+
`);
|
|
441
|
+
}
|
|
442
|
+
appendZprofile();
|
|
443
|
+
appendSshConfig();
|
|
444
|
+
setupDockerContext();
|
|
445
|
+
return 0;
|
|
446
|
+
}
|
|
447
|
+
function reloadAgent(label, plistPath, uid) {
|
|
448
|
+
try {
|
|
449
|
+
execFileSync("launchctl", ["bootout", `gui/${uid}/${label}`], { stdio: "ignore" });
|
|
450
|
+
} catch {
|
|
451
|
+
}
|
|
452
|
+
loadAgent(label, plistPath, uid);
|
|
453
|
+
}
|
|
454
|
+
function loadAgent(label, plistPath, uid) {
|
|
455
|
+
try {
|
|
456
|
+
execFileSync("launchctl", ["bootstrap", `gui/${uid}`, plistPath], { stdio: "ignore" });
|
|
457
|
+
} catch {
|
|
458
|
+
try {
|
|
459
|
+
execFileSync("launchctl", ["load", plistPath], { stdio: "ignore" });
|
|
460
|
+
} catch {
|
|
461
|
+
process.stderr.write(`warning: could not load ${label}
|
|
462
|
+
`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function setupDockerContext() {
|
|
467
|
+
const docker = findDockerPath();
|
|
468
|
+
if (!docker) {
|
|
469
|
+
process.stderr.write("warning: docker CLI not found \u2014 skipping the docker context\n");
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const [create, update, use] = dockerContextArgs(docker, DOCKER_SOCKET);
|
|
473
|
+
const created = spawnSync(create[0], create.slice(1), { stdio: "ignore" });
|
|
474
|
+
if (created.status !== 0) {
|
|
475
|
+
spawnSync(update[0], update.slice(1), { stdio: "ignore" });
|
|
476
|
+
}
|
|
477
|
+
const used = spawnSync(use[0], use.slice(1), { stdio: "ignore" });
|
|
478
|
+
if (used.status === 0) {
|
|
479
|
+
process.stdout.write("installed:docker context host\n");
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
function findDockerPath() {
|
|
483
|
+
const res = spawnSync("sh", ["-lc", "command -v docker"], { encoding: "utf8" });
|
|
484
|
+
const docker = res.status === 0 ? res.stdout.trim() : "";
|
|
485
|
+
return docker && docker.startsWith("/") ? docker : void 0;
|
|
486
|
+
}
|
|
487
|
+
function appendZprofile() {
|
|
488
|
+
const zprofile = join2(HOME, ".zprofile");
|
|
489
|
+
let content = readMaybe2(zprofile);
|
|
490
|
+
content = appendBlockIfMissing(content, zprofileAgentBlock(), ZPROFILE_MARKERS.agent);
|
|
491
|
+
content = appendBlockIfMissing(content, zprofileDockerBlock(), ZPROFILE_MARKERS.env);
|
|
492
|
+
writeFileSync3(zprofile, content);
|
|
493
|
+
}
|
|
494
|
+
function appendSshConfig() {
|
|
495
|
+
const dir = join2(HOME, ".ssh");
|
|
496
|
+
mkdirSync2(dir, { recursive: true });
|
|
497
|
+
const config = join2(dir, "config");
|
|
498
|
+
const content = appendBlockIfMissing(
|
|
499
|
+
readMaybe2(config),
|
|
500
|
+
sshConfigBlock(),
|
|
501
|
+
"# SSH agent bridge to the host (see docs/ssh-agent.md)"
|
|
502
|
+
);
|
|
503
|
+
writeFileSync3(config, content);
|
|
504
|
+
execFileSync("chmod", ["700", dir]);
|
|
505
|
+
execFileSync("chmod", ["600", config]);
|
|
506
|
+
}
|
|
507
|
+
function bridgeCommand(argv) {
|
|
508
|
+
const role = argv[0];
|
|
509
|
+
if (role !== "ssh-agent" && role !== "docker") {
|
|
510
|
+
throw new Error(`unknown bridge role '${role}' (ssh-agent|docker)`);
|
|
511
|
+
}
|
|
512
|
+
const port = Number(
|
|
513
|
+
argValue(argv, "--port") ?? DEFAULTS[role === "ssh-agent" ? "agentPort" : "dockerPort"]
|
|
514
|
+
);
|
|
515
|
+
const hostAlias = argValue(argv, "--host-alias") ?? defaultGateway();
|
|
516
|
+
if (!hostAlias) {
|
|
517
|
+
throw new Error("could not determine the host gateway (--host-alias)");
|
|
518
|
+
}
|
|
519
|
+
const listen2 = argValue(argv, "--listen") ?? (role === "ssh-agent" ? `unix:${GUEST_AGENT_SOCKET}` : `unix:${join2(HOME, ".docker", "run", "docker.sock")}`);
|
|
520
|
+
const forward = argValue(argv, "--forward") ?? `tcp:${hostAlias}:${port}`;
|
|
521
|
+
return runBridge({
|
|
522
|
+
listen: parseEndpoint(listen2),
|
|
523
|
+
forward: parseEndpoint(forward),
|
|
524
|
+
log: (message) => process.stderr.write(`${message}
|
|
525
|
+
`)
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
async function status() {
|
|
529
|
+
const agentUp = await canConnect(parseEndpoint(`unix:${GUEST_AGENT_SOCKET}`));
|
|
530
|
+
const dockerUp = await canConnect(
|
|
531
|
+
parseEndpoint(`unix:${join2(HOME, ".docker", "run", "docker.sock")}`)
|
|
532
|
+
);
|
|
533
|
+
process.stdout.write(`bridge-status:ssh-agent=${agentUp ? "up" : "down"}
|
|
534
|
+
`);
|
|
535
|
+
process.stdout.write(`bridge-status:docker=${dockerUp ? "up" : "down"}
|
|
536
|
+
`);
|
|
537
|
+
return 0;
|
|
538
|
+
}
|
|
539
|
+
function rulesCommand(argv) {
|
|
540
|
+
const content = readFileSync2(0, "utf8");
|
|
541
|
+
if (argv.includes("--force")) {
|
|
542
|
+
applyRules(HOME, content);
|
|
543
|
+
process.stdout.write("rules:overwritten\n");
|
|
544
|
+
} else {
|
|
545
|
+
process.stdout.write(`rules:probe=${rulesAction(HOME, content)}
|
|
546
|
+
`);
|
|
547
|
+
}
|
|
548
|
+
return 0;
|
|
549
|
+
}
|
|
550
|
+
function uninstall() {
|
|
551
|
+
const uid = execFileSync("id", ["-u"], { encoding: "utf8" }).trim();
|
|
552
|
+
for (const label of Object.values(LAUNCHD_LABELS)) {
|
|
553
|
+
try {
|
|
554
|
+
execFileSync("launchctl", ["bootout", `gui/${uid}/${label}`], { stdio: "ignore" });
|
|
555
|
+
} catch {
|
|
556
|
+
}
|
|
557
|
+
rmSync2(join2(LAUNCH_AGENTS_DIR, `${label}.plist`), { force: true });
|
|
558
|
+
}
|
|
559
|
+
const zprofile = join2(HOME, ".zprofile");
|
|
560
|
+
const config = join2(HOME, ".ssh", "config");
|
|
561
|
+
writeFileSync3(zprofile, cleanupFile(readMaybe2(zprofile)));
|
|
562
|
+
writeFileSync3(config, cleanupFile(readMaybe2(config)));
|
|
563
|
+
process.stdout.write("uninstalled:ssh-agent,docker\n");
|
|
564
|
+
return 0;
|
|
565
|
+
}
|
|
566
|
+
function cleanupFile(content) {
|
|
567
|
+
let out = removeBlock(content, ZPROFILE_MARKERS.agent);
|
|
568
|
+
out = removeBlock(out, ZPROFILE_MARKERS.env);
|
|
569
|
+
out = removeBlock(out, "# SSH agent bridge to the host (see docs/ssh-agent.md)");
|
|
570
|
+
return out.replace(/\n{3,}/g, "\n\n");
|
|
571
|
+
}
|
|
572
|
+
function readMaybe2(path) {
|
|
573
|
+
try {
|
|
574
|
+
return readFileSync2(path, "utf8");
|
|
575
|
+
} catch {
|
|
576
|
+
return "";
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
580
|
+
void main(process.argv.slice(2)).then((code) => {
|
|
581
|
+
process.exit(code);
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
export {
|
|
585
|
+
gatewayFromNetstat
|
|
586
|
+
};
|