@spencer-kit/coder-studio 0.4.9 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/esm/bin.mjs +29621 -16015
- package/dist/esm/bin.mjs.map +4 -4
- package/dist/esm/server-runner.mjs +27706 -14489
- package/dist/esm/server-runner.mjs.map +4 -4
- package/dist/web/assets/components-CBwEj8F3.css +1 -0
- package/dist/web/assets/components-CGHXUDfF.js +158 -0
- package/dist/web/assets/components-CGHXUDfF.js.map +1 -0
- package/dist/web/assets/main-Na2CPXZJ.js +2 -0
- package/dist/web/assets/main-Na2CPXZJ.js.map +1 -0
- package/dist/web/assets/ui-preview-DHlB71h3.js +23 -0
- package/dist/web/assets/ui-preview-DHlB71h3.js.map +1 -0
- package/dist/web/index.html +3 -3
- package/dist/web/ui-preview.html +3 -3
- package/package.json +1 -1
- package/src/automation-client.ts +36 -0
- package/src/automation-command-client.ts +142 -0
- package/src/bin.test.ts +194 -0
- package/src/cli.ts +121 -0
- package/src/parse-args.ts +221 -2
- package/dist/web/assets/components-6O5yG1fL.css +0 -1
- package/dist/web/assets/components-SjBXPKn9.js +0 -108
- package/dist/web/assets/components-SjBXPKn9.js.map +0 -1
- package/dist/web/assets/main-BjHz157g.js +0 -2
- package/dist/web/assets/main-BjHz157g.js.map +0 -1
- package/dist/web/assets/ui-preview-tdq8QPlu.js +0 -22
- package/dist/web/assets/ui-preview-tdq8QPlu.js.map +0 -1
package/dist/web/index.html
CHANGED
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
<meta name="description" content="Coder Studio - Agent-First Development Environment" />
|
|
7
7
|
<title>Coder Studio</title>
|
|
8
8
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
9
|
-
<script type="module" crossorigin src="/assets/main-
|
|
9
|
+
<script type="module" crossorigin src="/assets/main-Na2CPXZJ.js"></script>
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-CpWojdLp.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/monaco-editor-VTbRDH-J.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/xterm-BVlcrOZ1.js">
|
|
13
|
-
<link rel="modulepreload" crossorigin href="/assets/components-
|
|
13
|
+
<link rel="modulepreload" crossorigin href="/assets/components-CGHXUDfF.js">
|
|
14
14
|
<link rel="stylesheet" crossorigin href="/assets/monaco-editor-Br_kD0ds.css">
|
|
15
15
|
<link rel="stylesheet" crossorigin href="/assets/xterm-BrP-ENHg.css">
|
|
16
|
-
<link rel="stylesheet" crossorigin href="/assets/components-
|
|
16
|
+
<link rel="stylesheet" crossorigin href="/assets/components-CBwEj8F3.css">
|
|
17
17
|
</head>
|
|
18
18
|
<body>
|
|
19
19
|
<div id="root"></div>
|
package/dist/web/ui-preview.html
CHANGED
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Coder Studio UI Preview</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/ui-preview-
|
|
7
|
+
<script type="module" crossorigin src="/assets/ui-preview-DHlB71h3.js"></script>
|
|
8
8
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-CpWojdLp.js">
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/monaco-editor-VTbRDH-J.js">
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/xterm-BVlcrOZ1.js">
|
|
11
|
-
<link rel="modulepreload" crossorigin href="/assets/components-
|
|
11
|
+
<link rel="modulepreload" crossorigin href="/assets/components-CGHXUDfF.js">
|
|
12
12
|
<link rel="stylesheet" crossorigin href="/assets/monaco-editor-Br_kD0ds.css">
|
|
13
13
|
<link rel="stylesheet" crossorigin href="/assets/xterm-BrP-ENHg.css">
|
|
14
|
-
<link rel="stylesheet" crossorigin href="/assets/components-
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/assets/components-CBwEj8F3.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildIdentifyResult,
|
|
3
|
+
DEFAULT_AGENT_AUTOMATION_PERMISSIONS,
|
|
4
|
+
listAutomationCapabilities,
|
|
5
|
+
} from "@coder-studio/core";
|
|
6
|
+
|
|
7
|
+
interface PrintOptions {
|
|
8
|
+
json?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function printIdentify(options: PrintOptions = {}): void {
|
|
12
|
+
const result = buildIdentifyResult();
|
|
13
|
+
|
|
14
|
+
if (options.json) {
|
|
15
|
+
console.log(JSON.stringify(result, null, 2));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.log(result.insideCoderStudio ? "Inside Coder Studio" : "Not running inside Coder Studio");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function printCapabilities(options: PrintOptions = {}): void {
|
|
23
|
+
const result = {
|
|
24
|
+
version: 1,
|
|
25
|
+
commands: listAutomationCapabilities({
|
|
26
|
+
permissions: DEFAULT_AGENT_AUTOMATION_PERMISSIONS,
|
|
27
|
+
}),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
if (options.json) {
|
|
31
|
+
console.log(JSON.stringify(result, null, 2));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
console.log(result.commands.map((command) => `${command.name}: ${command.cli}`).join("\n"));
|
|
36
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { Result } from "@coder-studio/core";
|
|
3
|
+
import WebSocket from "ws";
|
|
4
|
+
import { getServerStatus } from "./server-control.js";
|
|
5
|
+
import { getBrowserUrl } from "./server-url.js";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_COMMAND_TIMEOUT_MS = 30_000;
|
|
8
|
+
|
|
9
|
+
export interface CoderStudioCommandInput {
|
|
10
|
+
apiUrl?: string;
|
|
11
|
+
op: string;
|
|
12
|
+
args: unknown;
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeApiUrl(apiUrl: string): string {
|
|
17
|
+
return apiUrl.endsWith("/") ? apiUrl.slice(0, -1) : apiUrl;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function toWebSocketUrl(apiUrl: string): string {
|
|
21
|
+
const url = new URL(normalizeApiUrl(apiUrl));
|
|
22
|
+
if (url.protocol === "http:") {
|
|
23
|
+
url.protocol = "ws:";
|
|
24
|
+
} else if (url.protocol === "https:") {
|
|
25
|
+
url.protocol = "wss:";
|
|
26
|
+
} else if (url.protocol !== "ws:" && url.protocol !== "wss:") {
|
|
27
|
+
throw new Error(`Unsupported Coder Studio API URL protocol: ${url.protocol}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
url.pathname = `${url.pathname.replace(/\/$/u, "")}/ws`;
|
|
31
|
+
url.search = "";
|
|
32
|
+
url.hash = "";
|
|
33
|
+
return url.toString();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function resolveApiUrl(explicitApiUrl: string | undefined): Promise<string> {
|
|
37
|
+
if (explicitApiUrl) {
|
|
38
|
+
return explicitApiUrl;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (process.env.CODER_STUDIO_API_URL) {
|
|
42
|
+
return process.env.CODER_STUDIO_API_URL;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const status = await getServerStatus();
|
|
46
|
+
const browserUrl = getBrowserUrl(status);
|
|
47
|
+
if (browserUrl) {
|
|
48
|
+
return browserUrl;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
throw new Error(
|
|
52
|
+
"Unable to find a running Coder Studio server. Start it first or pass --api-url."
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseResultMessage(data: WebSocket.RawData): Result | null {
|
|
57
|
+
if (Array.isArray(data)) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const text = Buffer.isBuffer(data)
|
|
62
|
+
? data.toString("utf8")
|
|
63
|
+
: Buffer.from(data as ArrayBuffer).toString("utf8");
|
|
64
|
+
const message = JSON.parse(text) as { kind?: string };
|
|
65
|
+
|
|
66
|
+
if (message.kind !== "result") {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return message as Result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function callCoderStudioCommand<T = unknown>(
|
|
74
|
+
input: CoderStudioCommandInput
|
|
75
|
+
): Promise<T> {
|
|
76
|
+
const apiUrl = await resolveApiUrl(input.apiUrl);
|
|
77
|
+
const wsUrl = toWebSocketUrl(apiUrl);
|
|
78
|
+
const id = randomUUID();
|
|
79
|
+
const timeoutMs = input.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
|
|
80
|
+
|
|
81
|
+
return new Promise<T>((resolve, reject) => {
|
|
82
|
+
const socket = new WebSocket(wsUrl);
|
|
83
|
+
let settled = false;
|
|
84
|
+
const timer = setTimeout(() => {
|
|
85
|
+
finish(() => reject(new Error(`Timed out waiting for ${input.op} result`)));
|
|
86
|
+
}, timeoutMs);
|
|
87
|
+
|
|
88
|
+
function finish(callback: () => void): void {
|
|
89
|
+
if (settled) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
settled = true;
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
socket.close();
|
|
96
|
+
callback();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
socket.on("open", () => {
|
|
100
|
+
socket.send(
|
|
101
|
+
JSON.stringify({
|
|
102
|
+
kind: "command",
|
|
103
|
+
id,
|
|
104
|
+
op: input.op,
|
|
105
|
+
args: input.args,
|
|
106
|
+
})
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
socket.on("message", (data) => {
|
|
111
|
+
let result: Result | null;
|
|
112
|
+
try {
|
|
113
|
+
result = parseResultMessage(data);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
finish(() => reject(error));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!result || result.id !== id) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (result.ok) {
|
|
124
|
+
finish(() => resolve(result.data as T));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const code = result.error?.code ? `${result.error.code}: ` : "";
|
|
129
|
+
finish(() => reject(new Error(`${code}${result.error?.message ?? "Command failed"}`)));
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
socket.on("error", (error) => {
|
|
133
|
+
finish(() => reject(error));
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
socket.on("close", () => {
|
|
137
|
+
if (!settled) {
|
|
138
|
+
finish(() => reject(new Error("Coder Studio command connection closed before a result")));
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
package/src/bin.test.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { join } from "path";
|
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
|
|
6
6
|
const {
|
|
7
|
+
callCoderStudioCommand,
|
|
7
8
|
clearAuthBlockByIp,
|
|
8
9
|
confirmYesNo,
|
|
9
10
|
getServerStatus,
|
|
@@ -17,6 +18,7 @@ const {
|
|
|
17
18
|
stopRunningServer,
|
|
18
19
|
writeCliConfig,
|
|
19
20
|
} = vi.hoisted(() => ({
|
|
21
|
+
callCoderStudioCommand: vi.fn(),
|
|
20
22
|
clearAuthBlockByIp: vi.fn(),
|
|
21
23
|
confirmYesNo: vi.fn(),
|
|
22
24
|
getServerStatus: vi.fn(),
|
|
@@ -64,6 +66,10 @@ vi.mock("./browser.js", () => ({
|
|
|
64
66
|
openBrowser,
|
|
65
67
|
}));
|
|
66
68
|
|
|
69
|
+
vi.mock("./automation-command-client.js", () => ({
|
|
70
|
+
callCoderStudioCommand,
|
|
71
|
+
}));
|
|
72
|
+
|
|
67
73
|
import { main } from "./cli";
|
|
68
74
|
import { parseArgs, RUNTIME_CONFIG_ERROR } from "./parse-args";
|
|
69
75
|
|
|
@@ -79,6 +85,7 @@ beforeEach(() => {
|
|
|
79
85
|
confirmYesNo.mockResolvedValue(false);
|
|
80
86
|
isInteractiveSession.mockReturnValue(true);
|
|
81
87
|
openBrowser.mockResolvedValue(undefined);
|
|
88
|
+
callCoderStudioCommand.mockResolvedValue({ ok: true });
|
|
82
89
|
getServerStatus.mockResolvedValue({
|
|
83
90
|
status: "stopped",
|
|
84
91
|
pid: null,
|
|
@@ -559,6 +566,111 @@ describe("main", () => {
|
|
|
559
566
|
expect(clearAuthBlockByIp).toHaveBeenCalledWith("198.51.100.24");
|
|
560
567
|
expect(logSpy).toHaveBeenCalledWith("Unblocked IP: 198.51.100.24");
|
|
561
568
|
});
|
|
569
|
+
|
|
570
|
+
it("prints identify output", async () => {
|
|
571
|
+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
572
|
+
|
|
573
|
+
await main(["identify", "--json"]);
|
|
574
|
+
|
|
575
|
+
expect(JSON.parse(logSpy.mock.calls[0]?.[0] as string)).toEqual({
|
|
576
|
+
insideCoderStudio: false,
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it("prints capabilities output", async () => {
|
|
581
|
+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
582
|
+
|
|
583
|
+
await main(["capabilities", "--json"]);
|
|
584
|
+
|
|
585
|
+
expect(JSON.parse(logSpy.mock.calls[0]?.[0] as string)).toEqual(
|
|
586
|
+
expect.objectContaining({
|
|
587
|
+
version: 1,
|
|
588
|
+
commands: expect.arrayContaining([expect.objectContaining({ name: "git.status" })]),
|
|
589
|
+
})
|
|
590
|
+
);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it("prints workspace list output through the Coder Studio command API", async () => {
|
|
594
|
+
callCoderStudioCommand.mockResolvedValueOnce([{ id: "ws-1", path: "/repo" }]);
|
|
595
|
+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
596
|
+
|
|
597
|
+
await main(["workspace", "list", "--json"]);
|
|
598
|
+
|
|
599
|
+
expect(callCoderStudioCommand).toHaveBeenCalledWith({
|
|
600
|
+
apiUrl: undefined,
|
|
601
|
+
op: "workspace.list",
|
|
602
|
+
args: {},
|
|
603
|
+
});
|
|
604
|
+
expect(JSON.parse(logSpy.mock.calls[0]?.[0] as string)).toEqual([
|
|
605
|
+
{ id: "ws-1", path: "/repo" },
|
|
606
|
+
]);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it("prints session list output through the Coder Studio command API", async () => {
|
|
610
|
+
callCoderStudioCommand.mockResolvedValueOnce([{ id: "sess-1", workspaceId: "ws-1" }]);
|
|
611
|
+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
612
|
+
|
|
613
|
+
await main(["session", "list", "--workspace", "ws-1", "--json"]);
|
|
614
|
+
|
|
615
|
+
expect(callCoderStudioCommand).toHaveBeenCalledWith({
|
|
616
|
+
apiUrl: undefined,
|
|
617
|
+
op: "session.list",
|
|
618
|
+
args: { workspaceId: "ws-1" },
|
|
619
|
+
});
|
|
620
|
+
expect(JSON.parse(logSpy.mock.calls[0]?.[0] as string)).toEqual([
|
|
621
|
+
{ id: "sess-1", workspaceId: "ws-1" },
|
|
622
|
+
]);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it("prints terminal read output through the Coder Studio command API", async () => {
|
|
626
|
+
callCoderStudioCommand.mockResolvedValueOnce({ terminalId: "term-1", text: "ready\n" });
|
|
627
|
+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
628
|
+
|
|
629
|
+
await main(["terminal", "read", "--terminal", "term-1", "--bytes", "4096", "--json"]);
|
|
630
|
+
|
|
631
|
+
expect(callCoderStudioCommand).toHaveBeenCalledWith({
|
|
632
|
+
apiUrl: undefined,
|
|
633
|
+
op: "terminal.read",
|
|
634
|
+
args: { terminalId: "term-1", bytes: 4096 },
|
|
635
|
+
});
|
|
636
|
+
expect(JSON.parse(logSpy.mock.calls[0]?.[0] as string)).toEqual({
|
|
637
|
+
terminalId: "term-1",
|
|
638
|
+
text: "ready\n",
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it("prints git status output through the Coder Studio command API", async () => {
|
|
643
|
+
callCoderStudioCommand.mockResolvedValueOnce({ branch: "main", entries: [] });
|
|
644
|
+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
645
|
+
|
|
646
|
+
await main(["git", "status", "--workspace", "ws-1", "--json"]);
|
|
647
|
+
|
|
648
|
+
expect(callCoderStudioCommand).toHaveBeenCalledWith({
|
|
649
|
+
apiUrl: undefined,
|
|
650
|
+
op: "git.status",
|
|
651
|
+
args: { workspaceId: "ws-1" },
|
|
652
|
+
});
|
|
653
|
+
expect(JSON.parse(logSpy.mock.calls[0]?.[0] as string)).toEqual({
|
|
654
|
+
branch: "main",
|
|
655
|
+
entries: [],
|
|
656
|
+
});
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
it("prints git diff output through the Coder Studio command API", async () => {
|
|
660
|
+
callCoderStudioCommand.mockResolvedValueOnce({ diff: "diff --git a/a b/a\n" });
|
|
661
|
+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
662
|
+
|
|
663
|
+
await main(["git", "diff", "--workspace", "ws-1", "--path", "src/a.ts", "--json"]);
|
|
664
|
+
|
|
665
|
+
expect(callCoderStudioCommand).toHaveBeenCalledWith({
|
|
666
|
+
apiUrl: undefined,
|
|
667
|
+
op: "git.diff",
|
|
668
|
+
args: { workspaceId: "ws-1", path: "src/a.ts" },
|
|
669
|
+
});
|
|
670
|
+
expect(JSON.parse(logSpy.mock.calls[0]?.[0] as string)).toEqual({
|
|
671
|
+
diff: "diff --git a/a b/a\n",
|
|
672
|
+
});
|
|
673
|
+
});
|
|
562
674
|
});
|
|
563
675
|
|
|
564
676
|
describe("parseArgs", () => {
|
|
@@ -651,6 +763,68 @@ describe("parseArgs", () => {
|
|
|
651
763
|
});
|
|
652
764
|
});
|
|
653
765
|
|
|
766
|
+
it("parses identify command with json output", () => {
|
|
767
|
+
expect(parseArgs(["identify", "--json"])).toEqual({
|
|
768
|
+
command: "identify",
|
|
769
|
+
json: true,
|
|
770
|
+
});
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
it("parses capabilities command with json output", () => {
|
|
774
|
+
expect(parseArgs(["capabilities", "--json"])).toEqual({
|
|
775
|
+
command: "capabilities",
|
|
776
|
+
json: true,
|
|
777
|
+
});
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
it("parses workspace list command with json output", () => {
|
|
781
|
+
expect(parseArgs(["workspace", "list", "--json"])).toEqual({
|
|
782
|
+
command: "workspace",
|
|
783
|
+
workspaceCommand: "list",
|
|
784
|
+
json: true,
|
|
785
|
+
});
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
it("parses session list command with workspace and json output", () => {
|
|
789
|
+
expect(parseArgs(["session", "list", "--workspace", "ws-1", "--json"])).toEqual({
|
|
790
|
+
command: "session",
|
|
791
|
+
sessionCommand: "list",
|
|
792
|
+
workspaceId: "ws-1",
|
|
793
|
+
json: true,
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
it("parses terminal read command with terminal id and byte limit", () => {
|
|
798
|
+
expect(parseArgs(["terminal", "read", "--terminal", "term-1", "--bytes", "4096"])).toEqual({
|
|
799
|
+
command: "terminal",
|
|
800
|
+
terminalCommand: "read",
|
|
801
|
+
terminalId: "term-1",
|
|
802
|
+
bytes: 4096,
|
|
803
|
+
});
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
it("parses git status command with workspace and json output", () => {
|
|
807
|
+
expect(parseArgs(["git", "status", "--workspace", "ws-1", "--json"])).toEqual({
|
|
808
|
+
command: "git",
|
|
809
|
+
gitCommand: "status",
|
|
810
|
+
workspaceId: "ws-1",
|
|
811
|
+
json: true,
|
|
812
|
+
});
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
it("parses git diff command with workspace, path, staged, and json output", () => {
|
|
816
|
+
expect(
|
|
817
|
+
parseArgs(["git", "diff", "--workspace", "ws-1", "--path", "src/a.ts", "--staged", "--json"])
|
|
818
|
+
).toEqual({
|
|
819
|
+
command: "git",
|
|
820
|
+
gitCommand: "diff",
|
|
821
|
+
workspaceId: "ws-1",
|
|
822
|
+
path: "src/a.ts",
|
|
823
|
+
staged: true,
|
|
824
|
+
json: true,
|
|
825
|
+
});
|
|
826
|
+
});
|
|
827
|
+
|
|
654
828
|
it("parses server alias as serve", () => {
|
|
655
829
|
expect(parseArgs(["server"])).toEqual({
|
|
656
830
|
command: "serve",
|
|
@@ -837,6 +1011,26 @@ describe("parseArgs", () => {
|
|
|
837
1011
|
expect(() => parseArgs(["status", "--bogus"])).toThrow("Unknown option: --bogus");
|
|
838
1012
|
});
|
|
839
1013
|
|
|
1014
|
+
it("rejects json output on unsupported commands", () => {
|
|
1015
|
+
expect(() => parseArgs(["status", "--json"])).toThrow("Unknown option: --json");
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
it("requires workspace id for session list", () => {
|
|
1019
|
+
expect(() => parseArgs(["session", "list"])).toThrow("Missing workspace value");
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
it("requires terminal id for terminal read", () => {
|
|
1023
|
+
expect(() => parseArgs(["terminal", "read"])).toThrow("Missing terminal value");
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
it("requires workspace id for git status", () => {
|
|
1027
|
+
expect(() => parseArgs(["git", "status"])).toThrow("Missing workspace value");
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
it("requires path for git diff", () => {
|
|
1031
|
+
expect(() => parseArgs(["git", "diff", "--workspace", "ws-1"])).toThrow("Missing path value");
|
|
1032
|
+
});
|
|
1033
|
+
|
|
840
1034
|
it("allows config-time host-only updates", () => {
|
|
841
1035
|
expect(parseArgs(["config", "--host", "127.0.0.1"])).toEqual({
|
|
842
1036
|
command: "config",
|
package/src/cli.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { existsSync } from "fs";
|
|
|
2
2
|
import { dirname, join } from "path";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
4
|
import { clearAuthBlockByIp, listAuthBlocks } from "./auth-control.js";
|
|
5
|
+
import { printCapabilities, printIdentify } from "./automation-client.js";
|
|
6
|
+
import { callCoderStudioCommand } from "./automation-command-client.js";
|
|
5
7
|
import { openBrowser } from "./browser.js";
|
|
6
8
|
import { type CliConfig, readCliConfig, writeCliConfig } from "./config-store.js";
|
|
7
9
|
import { readLogExcerpt } from "./log-excerpt.js";
|
|
@@ -76,6 +78,12 @@ COMMANDS:
|
|
|
76
78
|
status Show the managed server status
|
|
77
79
|
logs Show the managed server logs
|
|
78
80
|
help Show this help message
|
|
81
|
+
identify Print Coder Studio agent runtime context
|
|
82
|
+
capabilities Print agent-facing automation capabilities
|
|
83
|
+
workspace Read workspace automation data
|
|
84
|
+
session Read session automation data
|
|
85
|
+
terminal Read terminal automation data
|
|
86
|
+
git Read git automation data
|
|
79
87
|
version Show version
|
|
80
88
|
|
|
81
89
|
OPTIONS:
|
|
@@ -99,6 +107,13 @@ EXAMPLES:
|
|
|
99
107
|
coder-studio open --restart
|
|
100
108
|
coder-studio status
|
|
101
109
|
coder-studio logs
|
|
110
|
+
coder-studio identify --json
|
|
111
|
+
coder-studio capabilities --json
|
|
112
|
+
coder-studio workspace list --json
|
|
113
|
+
coder-studio session list --workspace ws_123 --json
|
|
114
|
+
coder-studio terminal read --terminal term_123 --bytes 4096 --json
|
|
115
|
+
coder-studio git status --workspace ws_123 --json
|
|
116
|
+
coder-studio git diff --workspace ws_123 --path src/a.ts --json
|
|
102
117
|
coder-studio stop
|
|
103
118
|
coder-studio config --host 0.0.0.0 --port 8080
|
|
104
119
|
`);
|
|
@@ -137,6 +152,35 @@ function showVersion(): void {
|
|
|
137
152
|
console.log(`@spencer-kit/coder-studio v${getCliVersion(import.meta.url)}`);
|
|
138
153
|
}
|
|
139
154
|
|
|
155
|
+
function printCommandResult(result: unknown, options: { json?: boolean } = {}): void {
|
|
156
|
+
if (options.json) {
|
|
157
|
+
console.log(JSON.stringify(result, null, 2));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (
|
|
162
|
+
typeof result === "object" &&
|
|
163
|
+
result !== null &&
|
|
164
|
+
"text" in result &&
|
|
165
|
+
typeof (result as { text?: unknown }).text === "string"
|
|
166
|
+
) {
|
|
167
|
+
console.log((result as { text: string }).text);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (
|
|
172
|
+
typeof result === "object" &&
|
|
173
|
+
result !== null &&
|
|
174
|
+
"diff" in result &&
|
|
175
|
+
typeof (result as { diff?: unknown }).diff === "string"
|
|
176
|
+
) {
|
|
177
|
+
console.log((result as { diff: string }).diff);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
console.log(JSON.stringify(result, null, 2));
|
|
182
|
+
}
|
|
183
|
+
|
|
140
184
|
function formatAuthBlocks(blocks: Awaited<ReturnType<typeof listAuthBlocks>>): string {
|
|
141
185
|
if (blocks.length === 0) {
|
|
142
186
|
return "No blocked IPs.";
|
|
@@ -297,6 +341,83 @@ export async function main(argv = process.argv.slice(2)): Promise<void> {
|
|
|
297
341
|
return;
|
|
298
342
|
}
|
|
299
343
|
|
|
344
|
+
if (args.command === "identify") {
|
|
345
|
+
printIdentify({ json: args.json });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (args.command === "capabilities") {
|
|
350
|
+
printCapabilities({ json: args.json });
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (args.command === "workspace" && args.workspaceCommand === "list") {
|
|
355
|
+
printCommandResult(
|
|
356
|
+
await callCoderStudioCommand({
|
|
357
|
+
apiUrl: args.apiUrl,
|
|
358
|
+
op: "workspace.list",
|
|
359
|
+
args: {},
|
|
360
|
+
}),
|
|
361
|
+
{ json: args.json }
|
|
362
|
+
);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (args.command === "session" && args.sessionCommand === "list") {
|
|
367
|
+
printCommandResult(
|
|
368
|
+
await callCoderStudioCommand({
|
|
369
|
+
apiUrl: args.apiUrl,
|
|
370
|
+
op: "session.list",
|
|
371
|
+
args: { workspaceId: args.workspaceId! },
|
|
372
|
+
}),
|
|
373
|
+
{ json: args.json }
|
|
374
|
+
);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (args.command === "terminal" && args.terminalCommand === "read") {
|
|
379
|
+
printCommandResult(
|
|
380
|
+
await callCoderStudioCommand({
|
|
381
|
+
apiUrl: args.apiUrl,
|
|
382
|
+
op: "terminal.read",
|
|
383
|
+
args: {
|
|
384
|
+
terminalId: args.terminalId!,
|
|
385
|
+
...(args.bytes !== undefined ? { bytes: args.bytes } : {}),
|
|
386
|
+
},
|
|
387
|
+
}),
|
|
388
|
+
{ json: args.json }
|
|
389
|
+
);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (args.command === "git" && args.gitCommand === "status") {
|
|
394
|
+
printCommandResult(
|
|
395
|
+
await callCoderStudioCommand({
|
|
396
|
+
apiUrl: args.apiUrl,
|
|
397
|
+
op: "git.status",
|
|
398
|
+
args: { workspaceId: args.workspaceId! },
|
|
399
|
+
}),
|
|
400
|
+
{ json: args.json }
|
|
401
|
+
);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (args.command === "git" && args.gitCommand === "diff") {
|
|
406
|
+
printCommandResult(
|
|
407
|
+
await callCoderStudioCommand({
|
|
408
|
+
apiUrl: args.apiUrl,
|
|
409
|
+
op: "git.diff",
|
|
410
|
+
args: {
|
|
411
|
+
workspaceId: args.workspaceId!,
|
|
412
|
+
...(args.path !== undefined ? { path: args.path } : {}),
|
|
413
|
+
...(args.staged === true ? { staged: true } : {}),
|
|
414
|
+
},
|
|
415
|
+
}),
|
|
416
|
+
{ json: args.json }
|
|
417
|
+
);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
300
421
|
if (args.command === "auth") {
|
|
301
422
|
if (args.authCommand === "ban-list") {
|
|
302
423
|
console.log(formatAuthBlocks(await listAuthBlocks()));
|