Skip to content

Commit ac27aec

Browse files
feat: #750
1 parent 7671617 commit ac27aec

File tree

10 files changed

+54
-60
lines changed

10 files changed

+54
-60
lines changed

eslint.config.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export default defineConfig(
6363
"error",
6464
{ considerDefaultExhaustiveForUnions: true },
6565
],
66+
"@typescript-eslint/no-non-null-assertion": "error",
6667
"@typescript-eslint/no-unused-vars": [
6768
"error",
6869
{ varsIgnorePattern: "^_", argsIgnorePattern: "^_" },
@@ -137,6 +138,8 @@ export default defineConfig(
137138
"@typescript-eslint/unbound-method": "off",
138139
// Empty callbacks are common in test stubs
139140
"@typescript-eslint/no-empty-function": "off",
141+
// Test assertions often use non-null assertions for brevity
142+
"@typescript-eslint/no-non-null-assertion": "off",
140143
// Test mocks often have loose typing - relax unsafe rules
141144
"@typescript-eslint/no-unsafe-assignment": "off",
142145
"@typescript-eslint/no-unsafe-call": "off",

src/api/authInterceptor.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,12 @@ export class AuthInterceptor implements vscode.Disposable {
123123
return this.authRequiredPromise;
124124
}
125125

126+
if (!this.onAuthRequired) {
127+
return false;
128+
}
129+
126130
this.logger.debug("Triggering re-authentication");
127-
this.authRequiredPromise = this.onAuthRequired!(hostname);
131+
this.authRequiredPromise = this.onAuthRequired(hostname);
128132

129133
try {
130134
return await this.authRequiredPromise;

src/api/workspace.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
import { spawn } from "node:child_process";
88
import * as vscode from "vscode";
99

10-
import { type CliAuth, getGlobalFlags } from "../cliConfig";
10+
import { type CliAuth, getGlobalShellFlags } from "../cliConfig";
1111
import { type FeatureSet } from "../featureSet";
1212
import { escapeCommandArg } from "../util";
1313
import { type UnidirectionalStream } from "../websocket/eventStreamConnection";
@@ -65,7 +65,7 @@ export async function startWorkspaceIfStoppedOrFailed(
6565

6666
return new Promise((resolve, reject) => {
6767
const startArgs = [
68-
...getGlobalFlags(vscode.workspace.getConfiguration(), auth),
68+
...getGlobalShellFlags(vscode.workspace.getConfiguration(), auth),
6969
"start",
7070
"--yes",
7171
createWorkspaceIdentifier(workspace),

src/cliConfig.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,30 +23,29 @@ export function getGlobalFlagsRaw(
2323
* Returns global configuration flags for Coder CLI commands with auth values
2424
* escaped for shell use (e.g., `terminal.sendText`, `spawn({ shell: true })`).
2525
*/
26-
export function getGlobalFlags(
26+
export function getGlobalShellFlags(
2727
configs: Pick<WorkspaceConfiguration, "get">,
2828
auth: CliAuth,
2929
): string[] {
30-
return buildGlobalFlags(configs, auth, true);
30+
return buildGlobalFlags(configs, auth, escapeCommandArg);
3131
}
3232

3333
/**
3434
* Returns global configuration flags for Coder CLI commands with raw auth
3535
* values suitable for `execFile` (no shell escaping).
3636
*/
37-
export function getGlobalFlagsForExec(
37+
export function getGlobalFlags(
3838
configs: Pick<WorkspaceConfiguration, "get">,
3939
auth: CliAuth,
4040
): string[] {
41-
return buildGlobalFlags(configs, auth, false);
41+
return buildGlobalFlags(configs, auth, (s) => s);
4242
}
4343

4444
function buildGlobalFlags(
4545
configs: Pick<WorkspaceConfiguration, "get">,
4646
auth: CliAuth,
47-
escapeAuth: boolean,
47+
esc: (s: string) => string,
4848
): string[] {
49-
const esc = escapeAuth ? escapeCommandArg : (s: string) => s;
5049
const authFlags =
5150
auth.mode === "url"
5251
? ["--url", esc(auth.url)]

src/commands.ts

Lines changed: 11 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
type WorkspaceAgent,
44
} from "coder/site/src/api/typesGenerated";
55
import * as fs from "node:fs/promises";
6-
import * as os from "node:os";
76
import * as path from "node:path";
87
import * as semver from "semver";
98
import * as vscode from "vscode";
@@ -12,7 +11,7 @@ import { createWorkspaceIdentifier, extractAgents } from "./api/api-helper";
1211
import { type CoderApi } from "./api/coderApi";
1312
import {
1413
getGlobalFlags,
15-
getGlobalFlagsForExec,
14+
getGlobalShellFlags,
1615
resolveCliAuth,
1716
} from "./cliConfig";
1817
import { type CliManager } from "./core/cliManager";
@@ -152,8 +151,8 @@ export class Commands {
152151
}
153152

154153
/**
155-
* Run a speed test against the currently connected workspace and save the
156-
* results to a file chosen by the user.
154+
* Run a speed test against the currently connected workspace and display the
155+
* results in a new editor document.
157156
*/
158157
public async speedTest(): Promise<void> {
159158
const workspace = this.workspace;
@@ -167,7 +166,7 @@ export class Commands {
167166
prompt: "Duration for the speed test (e.g., 5s, 10s, 1m)",
168167
value: "5s",
169168
validateInput: (v) => {
170-
return /^\d+[sm]$/.test(v.trim())
169+
return /^\d+[smh]$/.test(v.trim())
171170
? null
172171
: "Enter a duration like 5s, 10s, or 1m";
173172
},
@@ -186,7 +185,7 @@ export class Commands {
186185
const configDir = this.pathResolver.getGlobalConfigDir(safeHost);
187186
const configs = vscode.workspace.getConfiguration();
188187
const auth = resolveCliAuth(configs, featureSet, baseUrl, configDir);
189-
const globalFlags = getGlobalFlagsForExec(configs, auth);
188+
const globalFlags = getGlobalFlags(configs, auth);
190189
const workspaceName = createWorkspaceIdentifier(workspace);
191190

192191
return cliUtils.speedtest(binary, globalFlags, workspaceName, {
@@ -196,7 +195,7 @@ export class Commands {
196195
},
197196
{
198197
location: vscode.ProgressLocation.Notification,
199-
title: `Running speed test (${duration.trim()})...`,
198+
title: `Running ${duration.trim()} speed test...`,
200199
cancellable: true,
201200
},
202201
);
@@ -211,29 +210,11 @@ export class Commands {
211210
return;
212211
}
213212

214-
const defaultName = `speedtest-${workspace.name}-${new Date().toISOString().slice(0, 10)}.json`;
215-
const defaultUri = vscode.Uri.joinPath(
216-
vscode.workspace.workspaceFolders?.[0]?.uri ??
217-
vscode.Uri.file(os.homedir()),
218-
defaultName,
219-
);
220-
const uri = await vscode.window.showSaveDialog({
221-
defaultUri,
222-
filters: { JSON: ["json"] },
213+
const doc = await vscode.workspace.openTextDocument({
214+
content: result.value,
215+
language: "json",
223216
});
224-
if (!uri) {
225-
return;
226-
}
227-
228-
await vscode.workspace.fs.writeFile(uri, Buffer.from(result.value));
229-
const action = await vscode.window.showInformationMessage(
230-
"Speed test results saved.",
231-
"Open File",
232-
);
233-
if (action === "Open File") {
234-
const doc = await vscode.workspace.openTextDocument(uri);
235-
await vscode.window.showTextDocument(doc);
236-
}
217+
await vscode.window.showTextDocument(doc);
237218
}
238219

239220
/**
@@ -587,7 +568,7 @@ export class Commands {
587568
const configDir = this.pathResolver.getGlobalConfigDir(safeHost);
588569
const configs = vscode.workspace.getConfiguration();
589570
const auth = resolveCliAuth(configs, featureSet, baseUrl, configDir);
590-
const globalFlags = getGlobalFlags(configs, auth);
571+
const globalFlags = getGlobalShellFlags(configs, auth);
591572
terminal.sendText(
592573
`${escapeCommandArg(binary)} ${globalFlags.join(" ")} ssh ${app.workspace_name}`,
593574
);

src/core/cliUtils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,14 @@ export async function speedtest(
8080
binPath: string,
8181
globalFlags: string[],
8282
workspaceName: string,
83-
options?: { signal?: AbortSignal; duration?: string },
83+
options: { signal?: AbortSignal; duration?: string },
8484
): Promise<string> {
8585
const args = [...globalFlags, "speedtest", workspaceName, "--output", "json"];
86-
if (options?.duration) {
86+
if (options.duration) {
8787
args.push("-t", options.duration);
8888
}
8989
const result = await promisify(execFile)(binPath, args, {
90-
signal: options?.signal,
90+
signal: options.signal,
9191
});
9292
return result.stdout;
9393
}

src/remote/remote.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ import { CoderApi } from "../api/coderApi";
2222
import { needToken } from "../api/utils";
2323
import {
2424
type CliAuth,
25-
getGlobalFlags,
2625
getGlobalFlagsRaw,
26+
getGlobalShellFlags,
2727
getSshFlags,
2828
resolveCliAuth,
2929
} from "../cliConfig";
@@ -673,7 +673,7 @@ export class Remote {
673673
const vscodeConfig = vscode.workspace.getConfiguration();
674674

675675
const escapedBinaryPath = escapeCommandArg(binaryPath);
676-
const globalConfig = getGlobalFlags(vscodeConfig, cliAuth);
676+
const globalConfig = getGlobalShellFlags(vscodeConfig, cliAuth);
677677
const logArgs = await this.getLogArgs(logDir);
678678

679679
if (useWildcardSSH) {
@@ -865,7 +865,9 @@ export class Remote {
865865
const titleMap = new Map(settings.map((s) => [s.setting, s.title]));
866866

867867
return watchConfigurationChanges(settings, (changedSettings) => {
868-
const changedTitles = changedSettings.map((s) => titleMap.get(s)!);
868+
const changedTitles = changedSettings
869+
.map((s) => titleMap.get(s))
870+
.filter((t) => t !== undefined);
869871

870872
const message =
871873
changedTitles.length === 1

src/util.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export function findPort(text: string): number | null {
3434
}
3535

3636
// Get the last match, which is the most recent port.
37-
const lastMatch = allMatches.at(-1)!;
37+
const lastMatch = allMatches[allMatches.length - 1];
3838
// Each capture group corresponds to a different Remote SSH extension log format:
3939
// [0] full match, [1] and [2] ms-vscode-remote.remote-ssh,
4040
// [3] windsurf/open-remote-ssh/antigravity, [4] anysphere.remote-ssh

test/unit/cliConfig.test.ts

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { it, expect, describe, vi } from "vitest";
55
import {
66
type CliAuth,
77
getGlobalFlags,
8-
getGlobalFlagsForExec,
98
getGlobalFlagsRaw,
9+
getGlobalShellFlags,
1010
getSshFlags,
1111
isKeyringEnabled,
1212
resolveCliAuth,
@@ -24,7 +24,7 @@ const globalConfigAuth: CliAuth = {
2424
};
2525

2626
describe("cliConfig", () => {
27-
describe("getGlobalFlags", () => {
27+
describe("getGlobalShellFlags", () => {
2828
const urlAuth: CliAuth = { mode: "url", url: "https://dev.coder.com" };
2929

3030
interface AuthFlagsCase {
@@ -48,7 +48,9 @@ describe("cliConfig", () => {
4848
"should return auth flags for $scenario",
4949
({ auth, expectedAuthFlags }) => {
5050
const config = new MockConfigurationProvider();
51-
expect(getGlobalFlags(config, auth)).toStrictEqual(expectedAuthFlags);
51+
expect(getGlobalShellFlags(config, auth)).toStrictEqual(
52+
expectedAuthFlags,
53+
);
5254
},
5355
);
5456

@@ -59,7 +61,7 @@ describe("cliConfig", () => {
5961
"--disable-direct-connections",
6062
]);
6163

62-
expect(getGlobalFlags(config, globalConfigAuth)).toStrictEqual([
64+
expect(getGlobalShellFlags(config, globalConfigAuth)).toStrictEqual([
6365
"--verbose",
6466
"--disable-direct-connections",
6567
"--global-config",
@@ -77,7 +79,7 @@ describe("cliConfig", () => {
7779
"--disable-direct-connections",
7880
]);
7981

80-
expect(getGlobalFlags(config, globalConfigAuth)).toStrictEqual([
82+
expect(getGlobalShellFlags(config, globalConfigAuth)).toStrictEqual([
8183
"--verbose",
8284
"--disable-direct-connections",
8385
"--global-config",
@@ -113,12 +115,12 @@ describe("cliConfig", () => {
113115
const config = new MockConfigurationProvider();
114116
config.set("coder.globalFlags", flags);
115117

116-
expect(getGlobalFlags(config, globalConfigAuth)).toStrictEqual([
118+
expect(getGlobalShellFlags(config, globalConfigAuth)).toStrictEqual([
117119
"-v",
118120
"--global-config",
119121
'"/config/dir"',
120122
]);
121-
expect(getGlobalFlags(config, urlAuth)).toStrictEqual([
123+
expect(getGlobalShellFlags(config, urlAuth)).toStrictEqual([
122124
"-v",
123125
"--url",
124126
'"https://dev.coder.com"',
@@ -130,7 +132,7 @@ describe("cliConfig", () => {
130132
const config = new MockConfigurationProvider();
131133
config.set("coder.globalFlags", ["--global-configs", "--use-keyrings"]);
132134

133-
expect(getGlobalFlags(config, globalConfigAuth)).toStrictEqual([
135+
expect(getGlobalShellFlags(config, globalConfigAuth)).toStrictEqual([
134136
"--global-configs",
135137
"--use-keyrings",
136138
"--global-config",
@@ -161,7 +163,7 @@ describe("cliConfig", () => {
161163
"--no-feature-warning",
162164
]);
163165

164-
expect(getGlobalFlags(config, auth)).toStrictEqual([
166+
expect(getGlobalShellFlags(config, auth)).toStrictEqual([
165167
"-v",
166168
"--header-command custom", // ignored by CLI
167169
"--no-feature-warning",
@@ -173,16 +175,16 @@ describe("cliConfig", () => {
173175
);
174176
});
175177

176-
describe("getGlobalFlagsForExec", () => {
178+
describe("getGlobalFlags", () => {
177179
const urlAuth: CliAuth = { mode: "url", url: "https://dev.coder.com" };
178180

179181
it("should not escape auth flags", () => {
180182
const config = new MockConfigurationProvider();
181-
expect(getGlobalFlagsForExec(config, globalConfigAuth)).toStrictEqual([
183+
expect(getGlobalFlags(config, globalConfigAuth)).toStrictEqual([
182184
"--global-config",
183185
"/config/dir",
184186
]);
185-
expect(getGlobalFlagsForExec(config, urlAuth)).toStrictEqual([
187+
expect(getGlobalFlags(config, urlAuth)).toStrictEqual([
186188
"--url",
187189
"https://dev.coder.com",
188190
]);
@@ -191,7 +193,7 @@ describe("cliConfig", () => {
191193
it("should still escape header-command flags", () => {
192194
const config = new MockConfigurationProvider();
193195
config.set("coder.headerCommand", "echo test");
194-
expect(getGlobalFlagsForExec(config, globalConfigAuth)).toStrictEqual([
196+
expect(getGlobalFlags(config, globalConfigAuth)).toStrictEqual([
195197
"--global-config",
196198
"/config/dir",
197199
"--header-command",
@@ -202,7 +204,7 @@ describe("cliConfig", () => {
202204
it("should include user global flags", () => {
203205
const config = new MockConfigurationProvider();
204206
config.set("coder.globalFlags", ["--verbose"]);
205-
expect(getGlobalFlagsForExec(config, globalConfigAuth)).toStrictEqual([
207+
expect(getGlobalFlags(config, globalConfigAuth)).toStrictEqual([
206208
"--verbose",
207209
"--global-config",
208210
"/config/dir",

test/unit/core/cliUtils.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ describe("CliUtils", () => {
163163
echoArgsBin,
164164
["--global-config", "/tmp/test-config"],
165165
"owner/workspace",
166+
{},
166167
);
167168
const args = result.trim().split("\n");
168169
expect(args).toEqual([
@@ -180,6 +181,7 @@ describe("CliUtils", () => {
180181
echoArgsBin,
181182
["--url", "http://localhost:3000"],
182183
"owner/workspace",
184+
{},
183185
);
184186
const args = result.trim().split("\n");
185187
expect(args).toEqual([
@@ -218,6 +220,7 @@ describe("CliUtils", () => {
218220
"/nonexistent/binary",
219221
["--global-config", "/tmp"],
220222
"owner/workspace",
223+
{},
221224
),
222225
).rejects.toThrow("ENOENT");
223226
});

0 commit comments

Comments
 (0)