devcon-cli 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/README.md +15 -0
- package/dist/agent-bun.js +9 -0
- package/dist/agent-node.js +8 -0
- package/dist/index.js +790 -0
- package/package.json +24 -0
package/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# devcon-cli
|
|
2
|
+
|
|
3
|
+
To install dependencies:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
bun install
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
To run:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun run index.ts
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This project was created using `bun init` in bun v1.3.8. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
import*as O from"fs";import*as Y from"net";function _(B){let w=[],G=new Set,J=B.split(`
|
|
4
|
+
`);for(let z=1;z<J.length;z++){let j=J[z].trim();if(!j)continue;let F=j.split(/\s+/),N=F[1],K=F[3];if(!N||!K)continue;if(K!=="0A")continue;let U=N.split(":")[1];if(!U)continue;let Q=parseInt(U,16);if(Q<=0||G.has(Q))continue;let R=F[9];if(!R)continue;let V=parseInt(R,10);if(isNaN(V))continue;G.add(Q),w.push({port:Q,inode:V})}return w.sort((z,j)=>z.port-j.port)}function $(){let B=new Map,w;try{w=O.readdirSync("/proc").filter((G)=>/^\d+$/.test(G))}catch{return B}for(let G of w){let J=parseInt(G,10),z=`/proc/${J}/fd`,j;try{j=O.readdirSync(z)}catch{continue}for(let F of j)try{let K=O.readlinkSync(`${z}/${F}`).match(/^socket:\[(\d+)\]$/);if(K)B.set(parseInt(K[1],10),J)}catch{continue}}return B}function C(){let B=new Map,w;try{w=O.readdirSync("/proc").filter((G)=>/^\d+$/.test(G))}catch{return B}for(let G of w){let J=parseInt(G,10);try{let z=O.readFileSync(`/proc/${J}/stat`,"utf-8"),j=z.lastIndexOf(")");if(j===-1)continue;let N=z.slice(j+2).split(" "),K=parseInt(N[1],10);if(!isNaN(K))B.set(J,K)}catch{continue}}return B}function I(B){let w=new Map;for(let[z,j]of B){let F=w.get(j);if(!F)F=[],w.set(j,F);F.push(z)}let G=new Set,J=[];for(let[z,j]of B)if(j===0&&z!==1)G.add(z),J.push(z);while(J.length>0){let z=J.shift(),j=w.get(z);if(j){for(let F of j)if(!G.has(F))G.add(F),J.push(F)}}return G}function L(){function B(j){process.stdout.write(JSON.stringify(j)+`
|
|
5
|
+
`)}B({type:"ready",pid:process.pid});let w="";function G(){let j="";try{j+=O.readFileSync("/proc/net/tcp","utf-8")}catch{}try{j+=O.readFileSync("/proc/net/tcp6","utf-8")}catch{}let F=_(j);if(F.length===0){if(w!=="")w="",B({type:"ports",ports:[]});return}let N=$(),K=C(),U=I(K),Q=[];for(let{port:V,inode:Z}of F){let W=N.get(Z);if(W!==void 0&&U.has(W))Q.push(V)}let R=Q.join(",");if(R!==w)w=R,B({type:"ports",ports:Q})}G();let J=setInterval(G,1000),z="";process.stdin.setEncoding("utf-8"),process.stdin.on("data",(j)=>{z+=j;let F=z.split(`
|
|
6
|
+
`);z=F.pop();for(let N of F){if(!N.trim())continue;try{let K=JSON.parse(N);if(K.type==="stop")clearInterval(J),process.exit(0);if(K.type==="ping")B({type:"ready",pid:process.pid})}catch{}}}),process.stdin.on("end",()=>{clearInterval(J),process.exit(0)})}function v(B){let w=Y.createConnection({host:"127.0.0.1",port:B},()=>{process.stdin.pipe(w),w.pipe(process.stdout)});w.on("error",(G)=>{process.stderr.write(`bridge error: ${G.message}
|
|
7
|
+
`),process.exit(1)}),w.on("close",()=>{process.exit(0)}),process.stdin.on("end",()=>{w.end()})}var X=process.argv[2];if(X==="monitor")L();else if(X==="bridge"){let B=parseInt(process.argv[3],10);if(isNaN(B))process.stderr.write(`Usage: agent bridge <port>
|
|
8
|
+
`),process.exit(1);v(B)}else process.stderr.write(`Usage: agent <monitor|bridge> [port]
|
|
9
|
+
`),process.exit(1);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import*as O from"node:fs";import*as Y from"node:net";function _(B){let w=[],G=new Set,J=B.split(`
|
|
3
|
+
`);for(let z=1;z<J.length;z++){let j=J[z].trim();if(!j)continue;let F=j.split(/\s+/),N=F[1],K=F[3];if(!N||!K)continue;if(K!=="0A")continue;let U=N.split(":")[1];if(!U)continue;let Q=parseInt(U,16);if(Q<=0||G.has(Q))continue;let R=F[9];if(!R)continue;let V=parseInt(R,10);if(isNaN(V))continue;G.add(Q),w.push({port:Q,inode:V})}return w.sort((z,j)=>z.port-j.port)}function $(){let B=new Map,w;try{w=O.readdirSync("/proc").filter((G)=>/^\d+$/.test(G))}catch{return B}for(let G of w){let J=parseInt(G,10),z=`/proc/${J}/fd`,j;try{j=O.readdirSync(z)}catch{continue}for(let F of j)try{let K=O.readlinkSync(`${z}/${F}`).match(/^socket:\[(\d+)\]$/);if(K)B.set(parseInt(K[1],10),J)}catch{continue}}return B}function C(){let B=new Map,w;try{w=O.readdirSync("/proc").filter((G)=>/^\d+$/.test(G))}catch{return B}for(let G of w){let J=parseInt(G,10);try{let z=O.readFileSync(`/proc/${J}/stat`,"utf-8"),j=z.lastIndexOf(")");if(j===-1)continue;let N=z.slice(j+2).split(" "),K=parseInt(N[1],10);if(!isNaN(K))B.set(J,K)}catch{continue}}return B}function I(B){let w=new Map;for(let[z,j]of B){let F=w.get(j);if(!F)F=[],w.set(j,F);F.push(z)}let G=new Set,J=[];for(let[z,j]of B)if(j===0&&z!==1)G.add(z),J.push(z);while(J.length>0){let z=J.shift(),j=w.get(z);if(j){for(let F of j)if(!G.has(F))G.add(F),J.push(F)}}return G}function L(){function B(j){process.stdout.write(JSON.stringify(j)+`
|
|
4
|
+
`)}B({type:"ready",pid:process.pid});let w="";function G(){let j="";try{j+=O.readFileSync("/proc/net/tcp","utf-8")}catch{}try{j+=O.readFileSync("/proc/net/tcp6","utf-8")}catch{}let F=_(j);if(F.length===0){if(w!=="")w="",B({type:"ports",ports:[]});return}let N=$(),K=C(),U=I(K),Q=[];for(let{port:V,inode:Z}of F){let W=N.get(Z);if(W!==void 0&&U.has(W))Q.push(V)}let R=Q.join(",");if(R!==w)w=R,B({type:"ports",ports:Q})}G();let J=setInterval(G,1000),z="";process.stdin.setEncoding("utf-8"),process.stdin.on("data",(j)=>{z+=j;let F=z.split(`
|
|
5
|
+
`);z=F.pop();for(let N of F){if(!N.trim())continue;try{let K=JSON.parse(N);if(K.type==="stop")clearInterval(J),process.exit(0);if(K.type==="ping")B({type:"ready",pid:process.pid})}catch{}}}),process.stdin.on("end",()=>{clearInterval(J),process.exit(0)})}function v(B){let w=Y.createConnection({host:"127.0.0.1",port:B},()=>{process.stdin.pipe(w),w.pipe(process.stdout)});w.on("error",(G)=>{process.stderr.write(`bridge error: ${G.message}
|
|
6
|
+
`),process.exit(1)}),w.on("close",()=>{process.exit(0)}),process.stdin.on("end",()=>{w.end()})}var X=process.argv[2];if(X==="monitor")L();else if(X==="bridge"){let B=parseInt(process.argv[3],10);if(isNaN(B))process.stderr.write(`Usage: agent bridge <port>
|
|
7
|
+
`),process.exit(1);v(B)}else process.stderr.write(`Usage: agent <monitor|bridge> [port]
|
|
8
|
+
`),process.exit(1);
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
var __require = import.meta.require;
|
|
4
|
+
|
|
5
|
+
// src/cli/parser.ts
|
|
6
|
+
import { resolve } from "path";
|
|
7
|
+
var HELP = `devcon - devcontainer CLI with port forwarding
|
|
8
|
+
|
|
9
|
+
Usage: devcon-cli [options] <command> [args...]
|
|
10
|
+
|
|
11
|
+
Options:
|
|
12
|
+
--workspace-folder <path> Path to workspace (default: current directory)
|
|
13
|
+
--verbose, -v Enable verbose logging
|
|
14
|
+
--help, -h Show this help
|
|
15
|
+
|
|
16
|
+
Commands:
|
|
17
|
+
up Start container and port forwarding
|
|
18
|
+
down Stop container and port forwarding
|
|
19
|
+
exec -- <cmd> Execute command in container
|
|
20
|
+
shell Open interactive shell in container
|
|
21
|
+
forward [host:]container Forward a port manually
|
|
22
|
+
ports List forwarded ports
|
|
23
|
+
<other> Passed through to devcontainer CLI`;
|
|
24
|
+
function parseArgs(argv) {
|
|
25
|
+
let workspaceFolder = process.cwd();
|
|
26
|
+
let verbose = false;
|
|
27
|
+
let command = "";
|
|
28
|
+
const args = [];
|
|
29
|
+
let i = 0;
|
|
30
|
+
while (i < argv.length) {
|
|
31
|
+
const arg = argv[i];
|
|
32
|
+
if (arg === "--help" || arg === "-h") {
|
|
33
|
+
console.log(HELP);
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
if (arg === "--verbose" || arg === "-v") {
|
|
37
|
+
verbose = true;
|
|
38
|
+
i++;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (arg === "--workspace-folder") {
|
|
42
|
+
workspaceFolder = resolve(argv[++i] ?? workspaceFolder);
|
|
43
|
+
i++;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (!command) {
|
|
47
|
+
command = arg;
|
|
48
|
+
} else {
|
|
49
|
+
args.push(arg);
|
|
50
|
+
}
|
|
51
|
+
i++;
|
|
52
|
+
}
|
|
53
|
+
return { workspaceFolder, verbose, command, args };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/cli/commands.ts
|
|
57
|
+
var commands = new Map;
|
|
58
|
+
function registerCommand(name, handler) {
|
|
59
|
+
commands.set(name, handler);
|
|
60
|
+
}
|
|
61
|
+
function getCommand(name) {
|
|
62
|
+
return commands.get(name);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/util/log.ts
|
|
66
|
+
var RESET = "\x1B[0m";
|
|
67
|
+
var DIM = "\x1B[2m";
|
|
68
|
+
var RED = "\x1B[31m";
|
|
69
|
+
var YELLOW = "\x1B[33m";
|
|
70
|
+
var GREEN = "\x1B[32m";
|
|
71
|
+
var CYAN = "\x1B[36m";
|
|
72
|
+
var BOLD = "\x1B[1m";
|
|
73
|
+
var verbose = false;
|
|
74
|
+
function setVerbose(v) {
|
|
75
|
+
verbose = v;
|
|
76
|
+
}
|
|
77
|
+
function debug(...args) {
|
|
78
|
+
if (verbose) {
|
|
79
|
+
console.error(`${DIM}[debug]${RESET}`, ...args);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function info(...args) {
|
|
83
|
+
console.error(`${CYAN}[devcon]${RESET}`, ...args);
|
|
84
|
+
}
|
|
85
|
+
function success(...args) {
|
|
86
|
+
console.error(`${GREEN}[devcon]${RESET}`, ...args);
|
|
87
|
+
}
|
|
88
|
+
function warn(...args) {
|
|
89
|
+
console.error(`${YELLOW}[warn]${RESET}`, ...args);
|
|
90
|
+
}
|
|
91
|
+
function error(...args) {
|
|
92
|
+
console.error(`${RED}${BOLD}[error]${RESET}`, ...args);
|
|
93
|
+
}
|
|
94
|
+
function portLog(hostPort, containerPort, action) {
|
|
95
|
+
info(`${GREEN}${hostPort}${RESET} -> ${CYAN}${containerPort}${RESET} ${DIM}(${action})${RESET}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/core/devcontainer.ts
|
|
99
|
+
async function devcontainerUp(workspaceFolder) {
|
|
100
|
+
const args = [
|
|
101
|
+
"up",
|
|
102
|
+
"--workspace-folder",
|
|
103
|
+
workspaceFolder
|
|
104
|
+
];
|
|
105
|
+
debug("devcontainer", ...args);
|
|
106
|
+
const proc = Bun.spawn(["devcontainer", ...args], {
|
|
107
|
+
stdout: "pipe",
|
|
108
|
+
stderr: "pipe"
|
|
109
|
+
});
|
|
110
|
+
const stdout = await new Response(proc.stdout).text();
|
|
111
|
+
const stderr = await new Response(proc.stderr).text();
|
|
112
|
+
const exitCode = await proc.exited;
|
|
113
|
+
if (stderr.trim()) {
|
|
114
|
+
for (const line of stderr.trim().split(`
|
|
115
|
+
`)) {
|
|
116
|
+
debug("[devcontainer]", line);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (exitCode !== 0) {
|
|
120
|
+
throw new Error(`devcontainer up failed (exit ${exitCode}):
|
|
121
|
+
${stderr}`);
|
|
122
|
+
}
|
|
123
|
+
const lines = stdout.trim().split(`
|
|
124
|
+
`);
|
|
125
|
+
const jsonLine = lines[lines.length - 1];
|
|
126
|
+
try {
|
|
127
|
+
const result = JSON.parse(jsonLine);
|
|
128
|
+
if (result.outcome === "error") {
|
|
129
|
+
throw new Error(`devcontainer up error: ${result.message ?? result.description ?? "unknown"}`);
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
outcome: result.outcome,
|
|
133
|
+
containerId: result.containerId,
|
|
134
|
+
remoteUser: result.remoteUser,
|
|
135
|
+
remoteWorkspaceFolder: result.remoteWorkspaceFolder
|
|
136
|
+
};
|
|
137
|
+
} catch (e) {
|
|
138
|
+
if (e instanceof SyntaxError) {
|
|
139
|
+
error("Failed to parse devcontainer output:", jsonLine);
|
|
140
|
+
throw new Error("devcontainer up did not return valid JSON");
|
|
141
|
+
}
|
|
142
|
+
throw e;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function devcontainerExec(workspaceFolder, cmd) {
|
|
146
|
+
const args = [
|
|
147
|
+
"exec",
|
|
148
|
+
"--workspace-folder",
|
|
149
|
+
workspaceFolder,
|
|
150
|
+
...cmd
|
|
151
|
+
];
|
|
152
|
+
debug("devcontainer", ...args);
|
|
153
|
+
const proc = Bun.spawn(["devcontainer", ...args], {
|
|
154
|
+
stdin: "inherit",
|
|
155
|
+
stdout: "inherit",
|
|
156
|
+
stderr: "inherit"
|
|
157
|
+
});
|
|
158
|
+
return await proc.exited;
|
|
159
|
+
}
|
|
160
|
+
async function devcontainerPassthrough(args) {
|
|
161
|
+
debug("devcontainer", ...args);
|
|
162
|
+
const proc = Bun.spawn(["devcontainer", ...args], {
|
|
163
|
+
stdin: "inherit",
|
|
164
|
+
stdout: "inherit",
|
|
165
|
+
stderr: "inherit"
|
|
166
|
+
});
|
|
167
|
+
return await proc.exited;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/core/docker.ts
|
|
171
|
+
function dockerExec(containerId, cmd, opts = {}) {
|
|
172
|
+
const args = ["exec"];
|
|
173
|
+
if (opts.interactive)
|
|
174
|
+
args.push("-i");
|
|
175
|
+
if (opts.tty)
|
|
176
|
+
args.push("-t");
|
|
177
|
+
args.push(containerId, ...cmd);
|
|
178
|
+
debug("docker", ...args);
|
|
179
|
+
return Bun.spawn(["docker", ...args], {
|
|
180
|
+
stdin: opts.stdin ?? "pipe",
|
|
181
|
+
stdout: opts.stdout ?? "pipe",
|
|
182
|
+
stderr: opts.stderr ?? "pipe"
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
async function dockerWriteFile(containerId, destPath, content) {
|
|
186
|
+
debug("docker write", destPath, `(${content.length} bytes)`);
|
|
187
|
+
const proc = Bun.spawn(["docker", "exec", "-i", containerId, "tee", destPath], {
|
|
188
|
+
stdin: "pipe",
|
|
189
|
+
stdout: "pipe",
|
|
190
|
+
stderr: "pipe"
|
|
191
|
+
});
|
|
192
|
+
proc.stdin.write(content);
|
|
193
|
+
proc.stdin.end();
|
|
194
|
+
const exitCode = await proc.exited;
|
|
195
|
+
if (exitCode !== 0) {
|
|
196
|
+
const stderr = await new Response(proc.stderr).text();
|
|
197
|
+
throw new Error(`docker write failed (exit ${exitCode}): ${stderr}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async function dockerWhich(containerId, command) {
|
|
201
|
+
const proc = Bun.spawn(["docker", "exec", containerId, "which", command], { stdout: "pipe", stderr: "pipe" });
|
|
202
|
+
return await proc.exited === 0;
|
|
203
|
+
}
|
|
204
|
+
async function dockerChmod(containerId, path, mode = "+x") {
|
|
205
|
+
const proc = Bun.spawn(["docker", "exec", containerId, "chmod", mode, path], { stdout: "pipe", stderr: "pipe" });
|
|
206
|
+
const exitCode = await proc.exited;
|
|
207
|
+
if (exitCode !== 0) {
|
|
208
|
+
throw new Error(`docker chmod failed (exit ${exitCode})`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/core/config.ts
|
|
213
|
+
var CONFIG_FILES = [
|
|
214
|
+
".devcontainer/devcontainer.json",
|
|
215
|
+
".devcontainer.json"
|
|
216
|
+
];
|
|
217
|
+
async function readDevcontainerConfig(workspaceFolder) {
|
|
218
|
+
for (const rel of CONFIG_FILES) {
|
|
219
|
+
const path = `${workspaceFolder}/${rel}`;
|
|
220
|
+
const file = Bun.file(path);
|
|
221
|
+
if (await file.exists()) {
|
|
222
|
+
debug("Reading config from", path);
|
|
223
|
+
try {
|
|
224
|
+
const text = await file.text();
|
|
225
|
+
const cleaned = text.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/,(\s*[}\]])/g, "$1");
|
|
226
|
+
return JSON.parse(cleaned);
|
|
227
|
+
} catch (e) {
|
|
228
|
+
warn(`Failed to parse ${path}: ${e}`);
|
|
229
|
+
return {};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
debug("No devcontainer.json found");
|
|
234
|
+
return {};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/core/tcp-proxy.ts
|
|
238
|
+
var AGENT_PATH = "/tmp/devcon-agent.js";
|
|
239
|
+
function bridgeConnection(containerId, containerPort, hostSocket, runtime) {
|
|
240
|
+
const proc = dockerExec(containerId, [runtime, AGENT_PATH, "bridge", String(containerPort)], {
|
|
241
|
+
interactive: true,
|
|
242
|
+
stdin: "pipe",
|
|
243
|
+
stdout: "pipe",
|
|
244
|
+
stderr: "pipe"
|
|
245
|
+
});
|
|
246
|
+
let closing = false;
|
|
247
|
+
function cleanup() {
|
|
248
|
+
if (closing)
|
|
249
|
+
return;
|
|
250
|
+
closing = true;
|
|
251
|
+
try {
|
|
252
|
+
hostSocket.end();
|
|
253
|
+
} catch {}
|
|
254
|
+
try {
|
|
255
|
+
proc.kill();
|
|
256
|
+
} catch {}
|
|
257
|
+
}
|
|
258
|
+
const stdout = proc.stdout;
|
|
259
|
+
if (stdout && typeof stdout !== "number") {
|
|
260
|
+
(async () => {
|
|
261
|
+
try {
|
|
262
|
+
const reader = stdout.getReader();
|
|
263
|
+
while (true) {
|
|
264
|
+
const { done, value } = await reader.read();
|
|
265
|
+
if (done)
|
|
266
|
+
break;
|
|
267
|
+
hostSocket.write(value);
|
|
268
|
+
}
|
|
269
|
+
} catch (e) {
|
|
270
|
+
debug("bridge stdout pipe ended:", e);
|
|
271
|
+
}
|
|
272
|
+
cleanup();
|
|
273
|
+
})();
|
|
274
|
+
}
|
|
275
|
+
const stderr = proc.stderr;
|
|
276
|
+
if (stderr && typeof stderr !== "number") {
|
|
277
|
+
(async () => {
|
|
278
|
+
try {
|
|
279
|
+
const text = await new Response(stderr).text();
|
|
280
|
+
if (text.trim()) {
|
|
281
|
+
debug(`bridge stderr (port ${containerPort}):`, text.trim());
|
|
282
|
+
}
|
|
283
|
+
} catch {}
|
|
284
|
+
})();
|
|
285
|
+
}
|
|
286
|
+
hostSocket.__bridgeProc = proc;
|
|
287
|
+
hostSocket.__bridgeCleanup = cleanup;
|
|
288
|
+
}
|
|
289
|
+
function bridgeWrite(hostSocket, data) {
|
|
290
|
+
const proc = hostSocket.__bridgeProc;
|
|
291
|
+
if (proc?.stdin) {
|
|
292
|
+
proc.stdin.write(data);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function bridgeClose(hostSocket) {
|
|
296
|
+
const cleanup = hostSocket.__bridgeCleanup;
|
|
297
|
+
if (cleanup)
|
|
298
|
+
cleanup();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/core/port-manager.ts
|
|
302
|
+
class PortManager {
|
|
303
|
+
ports = new Map;
|
|
304
|
+
containerId;
|
|
305
|
+
runtime;
|
|
306
|
+
constructor(containerId, runtime) {
|
|
307
|
+
this.containerId = containerId;
|
|
308
|
+
this.runtime = runtime;
|
|
309
|
+
}
|
|
310
|
+
getForwardedPorts() {
|
|
311
|
+
return [...this.ports.values()].map((p) => p.info);
|
|
312
|
+
}
|
|
313
|
+
isForwarded(containerPort) {
|
|
314
|
+
return this.ports.has(containerPort);
|
|
315
|
+
}
|
|
316
|
+
async forward(containerPort, hostPort = containerPort, source = "auto") {
|
|
317
|
+
if (this.ports.has(containerPort)) {
|
|
318
|
+
debug(`Port ${containerPort} already forwarded`);
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
const portInfo = {
|
|
322
|
+
containerPort,
|
|
323
|
+
hostPort,
|
|
324
|
+
source,
|
|
325
|
+
status: "starting"
|
|
326
|
+
};
|
|
327
|
+
try {
|
|
328
|
+
const containerId = this.containerId;
|
|
329
|
+
const runtime = this.runtime;
|
|
330
|
+
const listener = Bun.listen({
|
|
331
|
+
hostname: "127.0.0.1",
|
|
332
|
+
port: hostPort,
|
|
333
|
+
socket: {
|
|
334
|
+
open(socket) {
|
|
335
|
+
debug(`New connection on host port ${hostPort} -> container port ${containerPort}`);
|
|
336
|
+
bridgeConnection(containerId, containerPort, socket, runtime);
|
|
337
|
+
},
|
|
338
|
+
data(socket, data) {
|
|
339
|
+
bridgeWrite(socket, Buffer.from(data));
|
|
340
|
+
},
|
|
341
|
+
close(socket) {
|
|
342
|
+
bridgeClose(socket);
|
|
343
|
+
},
|
|
344
|
+
error(socket, err) {
|
|
345
|
+
debug(`Socket error on port ${hostPort}:`, err.message);
|
|
346
|
+
bridgeClose(socket);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
portInfo.status = "forwarding";
|
|
351
|
+
this.ports.set(containerPort, { info: portInfo, listener });
|
|
352
|
+
portLog(hostPort, containerPort, source);
|
|
353
|
+
return true;
|
|
354
|
+
} catch (e) {
|
|
355
|
+
if (e.code === "EADDRINUSE") {
|
|
356
|
+
if (source === "manual") {
|
|
357
|
+
throw new Error(`Host port ${hostPort} is already in use`);
|
|
358
|
+
}
|
|
359
|
+
warn(`Port ${hostPort} already in use, skipping auto-forward for container port ${containerPort}`);
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
portInfo.status = "error";
|
|
363
|
+
error(`Failed to forward port ${containerPort}:`, e.message);
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
remove(containerPort) {
|
|
368
|
+
const managed = this.ports.get(containerPort);
|
|
369
|
+
if (!managed)
|
|
370
|
+
return false;
|
|
371
|
+
try {
|
|
372
|
+
managed.listener.stop();
|
|
373
|
+
} catch {}
|
|
374
|
+
this.ports.delete(containerPort);
|
|
375
|
+
info(`Stopped forwarding port ${containerPort}`);
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
async syncAutoDetected(detectedPorts) {
|
|
379
|
+
const detected = new Set(detectedPorts);
|
|
380
|
+
for (const [containerPort, managed] of this.ports) {
|
|
381
|
+
if (managed.info.source === "auto" && !detected.has(containerPort)) {
|
|
382
|
+
this.remove(containerPort);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
for (const port of detectedPorts) {
|
|
386
|
+
if (!this.ports.has(port)) {
|
|
387
|
+
await this.forward(port, port, "auto");
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
shutdown() {
|
|
392
|
+
for (const [, managed] of this.ports) {
|
|
393
|
+
try {
|
|
394
|
+
managed.listener.stop();
|
|
395
|
+
} catch {}
|
|
396
|
+
debug(`Stopped listener on port ${managed.info.hostPort}`);
|
|
397
|
+
}
|
|
398
|
+
this.ports.clear();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// src/core/port-monitor.ts
|
|
403
|
+
var AGENT_PATH2 = "/tmp/devcon-agent.js";
|
|
404
|
+
var MAX_RETRIES = 3;
|
|
405
|
+
var RETRY_DELAY_MS = 2000;
|
|
406
|
+
async function startPortMonitor(containerId, runtime, onPortChange) {
|
|
407
|
+
let retries = 0;
|
|
408
|
+
let stopped = false;
|
|
409
|
+
let currentProc = null;
|
|
410
|
+
async function start() {
|
|
411
|
+
const proc2 = dockerExec(containerId, [runtime, AGENT_PATH2, "monitor"], {
|
|
412
|
+
interactive: true,
|
|
413
|
+
stdin: "pipe",
|
|
414
|
+
stdout: "pipe",
|
|
415
|
+
stderr: "pipe"
|
|
416
|
+
});
|
|
417
|
+
currentProc = proc2;
|
|
418
|
+
const stderr = proc2.stderr;
|
|
419
|
+
if (stderr && typeof stderr !== "number") {
|
|
420
|
+
(async () => {
|
|
421
|
+
try {
|
|
422
|
+
const text = await new Response(stderr).text();
|
|
423
|
+
if (text.trim()) {
|
|
424
|
+
debug("[agent stderr]", text.trim());
|
|
425
|
+
}
|
|
426
|
+
} catch {}
|
|
427
|
+
})();
|
|
428
|
+
}
|
|
429
|
+
const stdout = proc2.stdout;
|
|
430
|
+
if (stdout && typeof stdout !== "number") {
|
|
431
|
+
(async () => {
|
|
432
|
+
try {
|
|
433
|
+
const decoder = new TextDecoder;
|
|
434
|
+
let buf = "";
|
|
435
|
+
const reader = stdout.getReader();
|
|
436
|
+
while (true) {
|
|
437
|
+
const { done, value } = await reader.read();
|
|
438
|
+
if (done)
|
|
439
|
+
break;
|
|
440
|
+
buf += decoder.decode(value, { stream: true });
|
|
441
|
+
const lines = buf.split(`
|
|
442
|
+
`);
|
|
443
|
+
buf = lines.pop();
|
|
444
|
+
for (const line of lines) {
|
|
445
|
+
if (!line.trim())
|
|
446
|
+
continue;
|
|
447
|
+
try {
|
|
448
|
+
const msg = JSON.parse(line);
|
|
449
|
+
debug("[agent]", msg);
|
|
450
|
+
if (msg.type === "ready") {
|
|
451
|
+
info("Agent ready (pid:", msg.pid + ")");
|
|
452
|
+
retries = 0;
|
|
453
|
+
} else if (msg.type === "ports") {
|
|
454
|
+
onPortChange(msg.ports);
|
|
455
|
+
}
|
|
456
|
+
} catch {
|
|
457
|
+
debug("[agent] unparseable line:", line);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
} catch (e) {
|
|
462
|
+
debug("Agent stdout reader ended:", e);
|
|
463
|
+
}
|
|
464
|
+
if (!stopped && retries < MAX_RETRIES) {
|
|
465
|
+
retries++;
|
|
466
|
+
warn(`Agent exited, retrying (${retries}/${MAX_RETRIES}) in ${RETRY_DELAY_MS}ms...`);
|
|
467
|
+
await Bun.sleep(RETRY_DELAY_MS);
|
|
468
|
+
if (!stopped) {
|
|
469
|
+
await start();
|
|
470
|
+
}
|
|
471
|
+
} else if (!stopped) {
|
|
472
|
+
error("Agent exited and max retries reached");
|
|
473
|
+
}
|
|
474
|
+
})();
|
|
475
|
+
}
|
|
476
|
+
return proc2;
|
|
477
|
+
}
|
|
478
|
+
const proc = await start();
|
|
479
|
+
return {
|
|
480
|
+
proc,
|
|
481
|
+
stop() {
|
|
482
|
+
stopped = true;
|
|
483
|
+
if (currentProc) {
|
|
484
|
+
try {
|
|
485
|
+
const stdin = currentProc.stdin;
|
|
486
|
+
stdin?.write?.(JSON.stringify({ type: "stop" }) + `
|
|
487
|
+
`);
|
|
488
|
+
} catch {}
|
|
489
|
+
setTimeout(() => {
|
|
490
|
+
try {
|
|
491
|
+
currentProc?.kill();
|
|
492
|
+
} catch {}
|
|
493
|
+
}, 1000);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// src/core/state.ts
|
|
500
|
+
import { createHash } from "crypto";
|
|
501
|
+
function stateFilePath(workspaceFolder) {
|
|
502
|
+
const hash = createHash("sha256").update(workspaceFolder).digest("hex").slice(0, 12);
|
|
503
|
+
return `/tmp/devcon-${hash}.json`;
|
|
504
|
+
}
|
|
505
|
+
async function writeState(workspaceFolder, containerId, ports) {
|
|
506
|
+
const path = stateFilePath(workspaceFolder);
|
|
507
|
+
const state = {
|
|
508
|
+
containerId,
|
|
509
|
+
workspaceFolder,
|
|
510
|
+
ports,
|
|
511
|
+
pid: process.pid
|
|
512
|
+
};
|
|
513
|
+
debug("Writing state to", path);
|
|
514
|
+
await Bun.write(path, JSON.stringify(state, null, 2));
|
|
515
|
+
}
|
|
516
|
+
async function readState(workspaceFolder) {
|
|
517
|
+
const path = stateFilePath(workspaceFolder);
|
|
518
|
+
const file = Bun.file(path);
|
|
519
|
+
if (!await file.exists()) {
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
try {
|
|
523
|
+
return await file.json();
|
|
524
|
+
} catch {
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
async function removeState(workspaceFolder) {
|
|
529
|
+
const path = stateFilePath(workspaceFolder);
|
|
530
|
+
try {
|
|
531
|
+
const { unlink } = await import("fs/promises");
|
|
532
|
+
await unlink(path);
|
|
533
|
+
debug("Removed state file", path);
|
|
534
|
+
} catch {}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// src/commands/up.ts
|
|
538
|
+
var AGENT_PATH3 = "/tmp/devcon-agent.js";
|
|
539
|
+
async function commandUp(opts) {
|
|
540
|
+
const { workspaceFolder } = opts;
|
|
541
|
+
info("Starting devcontainer...");
|
|
542
|
+
const result = await devcontainerUp(workspaceFolder);
|
|
543
|
+
success(`Container started (${result.containerId.slice(0, 12)})`);
|
|
544
|
+
debug("Remote user:", result.remoteUser);
|
|
545
|
+
debug("Workspace folder:", result.remoteWorkspaceFolder);
|
|
546
|
+
const containerId = result.containerId;
|
|
547
|
+
let runtime = "node";
|
|
548
|
+
if (await dockerWhich(containerId, "node")) {
|
|
549
|
+
runtime = "node";
|
|
550
|
+
} else if (await dockerWhich(containerId, "bun")) {
|
|
551
|
+
runtime = "bun";
|
|
552
|
+
} else {
|
|
553
|
+
error("No Node.js or Bun runtime found in container.");
|
|
554
|
+
error("The devcon agent requires Node.js or Bun to run.");
|
|
555
|
+
process.exit(1);
|
|
556
|
+
}
|
|
557
|
+
debug("Using runtime:", runtime);
|
|
558
|
+
info("Deploying agent...");
|
|
559
|
+
const agentFile = runtime === "bun" ? "agent-bun.js" : "agent-node.js";
|
|
560
|
+
const agentPath = new URL(`../../dist/${agentFile}`, import.meta.url).pathname;
|
|
561
|
+
const agentCode = await Bun.file(agentPath).text();
|
|
562
|
+
await dockerWriteFile(containerId, AGENT_PATH3, agentCode);
|
|
563
|
+
await dockerChmod(containerId, AGENT_PATH3);
|
|
564
|
+
success("Agent deployed");
|
|
565
|
+
const config = await readDevcontainerConfig(workspaceFolder);
|
|
566
|
+
const configPorts = config.forwardPorts ?? [];
|
|
567
|
+
if (configPorts.length > 0) {
|
|
568
|
+
debug("Config forwardPorts:", configPorts);
|
|
569
|
+
}
|
|
570
|
+
const portManager = new PortManager(containerId, runtime);
|
|
571
|
+
for (const port of configPorts) {
|
|
572
|
+
const onAutoForward = config.portsAttributes?.[String(port)]?.onAutoForward;
|
|
573
|
+
if (onAutoForward === "ignore") {
|
|
574
|
+
debug(`Skipping port ${port} (onAutoForward: ignore)`);
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
await portManager.forward(port, port, "config");
|
|
578
|
+
}
|
|
579
|
+
const ignoreSet = new Set;
|
|
580
|
+
if (config.portsAttributes) {
|
|
581
|
+
for (const [portStr, attrs] of Object.entries(config.portsAttributes)) {
|
|
582
|
+
if (attrs.onAutoForward === "ignore") {
|
|
583
|
+
ignoreSet.add(parseInt(portStr, 10));
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const monitor = await startPortMonitor(containerId, runtime, async (ports) => {
|
|
588
|
+
const filtered = ports.filter((p) => !ignoreSet.has(p));
|
|
589
|
+
await portManager.syncAutoDetected(filtered);
|
|
590
|
+
await writeState(workspaceFolder, containerId, portManager.getForwardedPorts());
|
|
591
|
+
});
|
|
592
|
+
await writeState(workspaceFolder, containerId, portManager.getForwardedPorts());
|
|
593
|
+
let shuttingDown = false;
|
|
594
|
+
async function shutdown() {
|
|
595
|
+
if (shuttingDown)
|
|
596
|
+
return;
|
|
597
|
+
shuttingDown = true;
|
|
598
|
+
info("Shutting down...");
|
|
599
|
+
monitor.stop();
|
|
600
|
+
portManager.shutdown();
|
|
601
|
+
info("Stopping container...");
|
|
602
|
+
const stopProc = Bun.spawn(["docker", "stop", containerId], {
|
|
603
|
+
stdout: "pipe",
|
|
604
|
+
stderr: "pipe"
|
|
605
|
+
});
|
|
606
|
+
const stopExit = await stopProc.exited;
|
|
607
|
+
if (stopExit === 0) {
|
|
608
|
+
success("Container stopped");
|
|
609
|
+
} else {
|
|
610
|
+
error("Failed to stop container", containerId.slice(0, 12));
|
|
611
|
+
}
|
|
612
|
+
await removeState(workspaceFolder);
|
|
613
|
+
process.exit(0);
|
|
614
|
+
}
|
|
615
|
+
process.on("SIGINT", shutdown);
|
|
616
|
+
process.on("SIGTERM", shutdown);
|
|
617
|
+
info("Port forwarding active. Press Ctrl+C to stop.");
|
|
618
|
+
await new Promise(() => {});
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// src/commands/down.ts
|
|
622
|
+
async function commandDown(opts) {
|
|
623
|
+
const { workspaceFolder } = opts;
|
|
624
|
+
const state = await readState(workspaceFolder);
|
|
625
|
+
if (state?.pid) {
|
|
626
|
+
try {
|
|
627
|
+
process.kill(state.pid, "SIGTERM");
|
|
628
|
+
info("Sent SIGTERM to devcon process (pid:", state.pid + ")");
|
|
629
|
+
} catch {
|
|
630
|
+
debug("devcon process already exited");
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
info("Stopping devcontainer...");
|
|
634
|
+
const proc = Bun.spawn(["devcontainer", "down", "--workspace-folder", workspaceFolder], { stdout: "inherit", stderr: "inherit" });
|
|
635
|
+
const exitCode = await proc.exited;
|
|
636
|
+
if (exitCode !== 0 && state?.containerId) {
|
|
637
|
+
debug("devcontainer down failed, trying docker stop");
|
|
638
|
+
const dockerProc = Bun.spawn(["docker", "stop", state.containerId], { stdout: "pipe", stderr: "pipe" });
|
|
639
|
+
const dockerExit = await dockerProc.exited;
|
|
640
|
+
if (dockerExit === 0) {
|
|
641
|
+
success("Container stopped");
|
|
642
|
+
} else {
|
|
643
|
+
warn("Could not stop container", state.containerId);
|
|
644
|
+
}
|
|
645
|
+
} else if (exitCode === 0) {
|
|
646
|
+
success("Container stopped");
|
|
647
|
+
}
|
|
648
|
+
await removeState(workspaceFolder);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// src/commands/exec.ts
|
|
652
|
+
async function commandExec(opts) {
|
|
653
|
+
const { workspaceFolder, args } = opts;
|
|
654
|
+
const cmd = args[0] === "--" ? args.slice(1) : args;
|
|
655
|
+
if (cmd.length === 0) {
|
|
656
|
+
error("Usage: devcon exec -- <command> [args...]");
|
|
657
|
+
process.exit(1);
|
|
658
|
+
}
|
|
659
|
+
const exitCode = await devcontainerExec(workspaceFolder, cmd);
|
|
660
|
+
process.exit(exitCode);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// src/commands/shell.ts
|
|
664
|
+
async function commandShell(opts) {
|
|
665
|
+
const { workspaceFolder } = opts;
|
|
666
|
+
const state = await readState(workspaceFolder);
|
|
667
|
+
if (!state?.containerId) {
|
|
668
|
+
error("No running container found. Run `devcon up` first.");
|
|
669
|
+
process.exit(1);
|
|
670
|
+
}
|
|
671
|
+
const shell = opts.args[0] ?? "/bin/sh";
|
|
672
|
+
debug("Opening shell:", shell, "in container:", state.containerId.slice(0, 12));
|
|
673
|
+
const proc = Bun.spawn(["docker", "exec", "-it", state.containerId, shell], { stdin: "inherit", stdout: "inherit", stderr: "inherit" });
|
|
674
|
+
const exitCode = await proc.exited;
|
|
675
|
+
process.exit(exitCode);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// src/commands/forward.ts
|
|
679
|
+
function parsePortSpec(spec) {
|
|
680
|
+
const parts = spec.split(":");
|
|
681
|
+
if (parts.length === 2) {
|
|
682
|
+
return {
|
|
683
|
+
hostPort: parseInt(parts[0], 10),
|
|
684
|
+
containerPort: parseInt(parts[1], 10)
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
const port = parseInt(parts[0], 10);
|
|
688
|
+
return { hostPort: port, containerPort: port };
|
|
689
|
+
}
|
|
690
|
+
async function commandForward(opts) {
|
|
691
|
+
const { workspaceFolder, args } = opts;
|
|
692
|
+
if (args.length === 0) {
|
|
693
|
+
error("Usage: devcon forward [host_port:]container_port");
|
|
694
|
+
process.exit(1);
|
|
695
|
+
}
|
|
696
|
+
const state = await readState(workspaceFolder);
|
|
697
|
+
if (!state?.containerId) {
|
|
698
|
+
error("No running container found. Run `devcon up` first.");
|
|
699
|
+
process.exit(1);
|
|
700
|
+
}
|
|
701
|
+
const { hostPort, containerPort } = parsePortSpec(args[0]);
|
|
702
|
+
if (isNaN(hostPort) || isNaN(containerPort)) {
|
|
703
|
+
error("Invalid port specification:", args[0]);
|
|
704
|
+
process.exit(1);
|
|
705
|
+
}
|
|
706
|
+
let runtime = "node";
|
|
707
|
+
if (await dockerWhich(state.containerId, "node")) {
|
|
708
|
+
runtime = "node";
|
|
709
|
+
} else if (await dockerWhich(state.containerId, "bun")) {
|
|
710
|
+
runtime = "bun";
|
|
711
|
+
} else {
|
|
712
|
+
error("No runtime found in container");
|
|
713
|
+
process.exit(1);
|
|
714
|
+
}
|
|
715
|
+
const portManager = new PortManager(state.containerId, runtime);
|
|
716
|
+
try {
|
|
717
|
+
await portManager.forward(containerPort, hostPort, "manual");
|
|
718
|
+
success(`Forwarding localhost:${hostPort} -> container:${containerPort}`);
|
|
719
|
+
} catch (e) {
|
|
720
|
+
error(e.message);
|
|
721
|
+
process.exit(1);
|
|
722
|
+
}
|
|
723
|
+
const currentState = await readState(workspaceFolder);
|
|
724
|
+
if (currentState) {
|
|
725
|
+
const ports = [...currentState.ports, ...portManager.getForwardedPorts()];
|
|
726
|
+
await writeState(workspaceFolder, state.containerId, ports);
|
|
727
|
+
}
|
|
728
|
+
info("Press Ctrl+C to stop forwarding.");
|
|
729
|
+
process.on("SIGINT", () => {
|
|
730
|
+
portManager.shutdown();
|
|
731
|
+
process.exit(0);
|
|
732
|
+
});
|
|
733
|
+
process.on("SIGTERM", () => {
|
|
734
|
+
portManager.shutdown();
|
|
735
|
+
process.exit(0);
|
|
736
|
+
});
|
|
737
|
+
await new Promise(() => {});
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// src/commands/ports.ts
|
|
741
|
+
async function commandPorts(opts) {
|
|
742
|
+
const state = await readState(opts.workspaceFolder);
|
|
743
|
+
if (!state) {
|
|
744
|
+
warn("No running devcon instance found.");
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
if (state.ports.length === 0) {
|
|
748
|
+
info("No ports currently forwarded.");
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
console.log("Forwarded ports:");
|
|
752
|
+
console.log("");
|
|
753
|
+
console.log(" HOST CONTAINER SOURCE STATUS");
|
|
754
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
755
|
+
for (const port of state.ports) {
|
|
756
|
+
const host = String(port.hostPort).padEnd(9);
|
|
757
|
+
const container = String(port.containerPort).padEnd(9);
|
|
758
|
+
const source = port.source.padEnd(6);
|
|
759
|
+
console.log(` ${host} ${container} ${source} ${port.status}`);
|
|
760
|
+
}
|
|
761
|
+
console.log("");
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// src/commands/passthrough.ts
|
|
765
|
+
async function commandPassthrough(opts) {
|
|
766
|
+
const allArgs = [opts.command, ...opts.args];
|
|
767
|
+
debug("Passing through to devcontainer:", allArgs);
|
|
768
|
+
const exitCode = await devcontainerPassthrough(allArgs);
|
|
769
|
+
process.exit(exitCode);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// index.ts
|
|
773
|
+
registerCommand("up", commandUp);
|
|
774
|
+
registerCommand("down", commandDown);
|
|
775
|
+
registerCommand("exec", commandExec);
|
|
776
|
+
registerCommand("shell", commandShell);
|
|
777
|
+
registerCommand("forward", commandForward);
|
|
778
|
+
registerCommand("ports", commandPorts);
|
|
779
|
+
var opts = parseArgs(process.argv.slice(2));
|
|
780
|
+
setVerbose(opts.verbose);
|
|
781
|
+
if (!opts.command) {
|
|
782
|
+
parseArgs(["--help"]);
|
|
783
|
+
process.exit(0);
|
|
784
|
+
}
|
|
785
|
+
var handler = getCommand(opts.command);
|
|
786
|
+
if (handler) {
|
|
787
|
+
await handler(opts);
|
|
788
|
+
} else {
|
|
789
|
+
await commandPassthrough(opts);
|
|
790
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "devcon-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Devcontainer CLI with automatic port forwarding",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "bun scripts/build.ts"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"devcon": "dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist/"
|
|
14
|
+
],
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@devcontainers/cli": "^0.83.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/bun": "latest"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"typescript": "^5.9.3"
|
|
23
|
+
}
|
|
24
|
+
}
|