@vellumai/cli 0.10.11-dev.202607220509.38c4eff → 0.10.11-dev.202607220859.296f4b0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/cli",
3
- "version": "0.10.11-dev.202607220509.38c4eff",
3
+ "version": "0.10.11-dev.202607220859.296f4b0",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "exports": {
@@ -188,14 +188,7 @@ describe("vellum flags --assistant routing", () => {
188
188
  ],
189
189
  "alice-1",
190
190
  );
191
- process.argv = [
192
- "bun",
193
- "vellum",
194
- "flags",
195
- "set",
196
- "voice-mode",
197
- "true",
198
- ];
191
+ process.argv = ["bun", "vellum", "flags", "set", "voice-mode", "true"];
199
192
 
200
193
  await flags();
201
194
 
@@ -1,10 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import {
3
- existsSync,
4
- mkdtempSync,
5
- readFileSync,
6
- rmSync,
7
- } from "node:fs";
2
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
8
3
  import { homedir, tmpdir } from "node:os";
9
4
  import { join } from "node:path";
10
5
 
@@ -3,7 +3,9 @@ import { describe, expect, test } from "bun:test";
3
3
  import { startLoopbackListener } from "../commands/login.js";
4
4
 
5
5
  /** Resolve "settled"/"pending" — proves whether `waitForCode` resolved. */
6
- async function settleState(p: Promise<unknown>): Promise<"settled" | "pending"> {
6
+ async function settleState(
7
+ p: Promise<unknown>,
8
+ ): Promise<"settled" | "pending"> {
7
9
  return Promise.race([
8
10
  p.then(
9
11
  () => "settled" as const,
@@ -49,7 +51,9 @@ describe("startLoopbackListener", () => {
49
51
  () => null,
50
52
  (e: Error) => e,
51
53
  );
52
- const res = await fetch(`${listener.redirectUri}?error=access_denied&state=st`);
54
+ const res = await fetch(
55
+ `${listener.redirectUri}?error=access_denied&state=st`,
56
+ );
53
57
  expect(res.status).toBe(400);
54
58
  const err = await settled;
55
59
  expect(err?.message).toMatch(/access_denied/);
@@ -15,7 +15,7 @@ import { join } from "node:path";
15
15
  const testDir = mkdtempSync(join(tmpdir(), "pair-command-test-"));
16
16
  process.env.VELLUM_LOCKFILE_DIR = testDir;
17
17
 
18
- import { pair } from "../commands/pair.js";
18
+ import { buildAppConnectUrl, pair } from "../commands/pair.js";
19
19
 
20
20
  // Distinct loopback (mint) vs reachable (advertised) URLs to verify the split.
21
21
  const LOCAL_URL = "http://127.0.0.1:7830";
@@ -800,4 +800,135 @@ describe("pair command", () => {
800
800
  `${LOCAL_URL}/v1/assistants/pair-test/feature-flags`,
801
801
  );
802
802
  });
803
+
804
+ test("buildAppConnectUrl composes and encodes the connect link", () => {
805
+ expect(
806
+ buildAppConnectUrl(
807
+ "vellum-assistant",
808
+ "https://pair.example.ts.net",
809
+ "device-code",
810
+ ),
811
+ ).toBe(
812
+ "vellum-assistant://connect?url=https%3A%2F%2Fpair.example.ts.net&code=device-code",
813
+ );
814
+ // Path prefixes and fragment-hostile characters survive the encoding.
815
+ expect(
816
+ buildAppConnectUrl(
817
+ "vellum-assistant-dev",
818
+ "https://host.example.ts.net/assistant-123",
819
+ "a+b/c=",
820
+ ),
821
+ ).toBe(
822
+ "vellum-assistant-dev://connect?url=https%3A%2F%2Fhost.example.ts.net%2Fassistant-123&code=a%2Bb%2Fc%3D",
823
+ );
824
+ });
825
+
826
+ test("--qr --app emits an app connect URL alongside the browser URL", async () => {
827
+ const origFetch = globalThis.fetch;
828
+ globalThis.fetch = (async (url: string) => {
829
+ if (url === `${LOCAL_URL}/v1/assistants/pair-test/feature-flags`) {
830
+ return new Response(
831
+ JSON.stringify({
832
+ flags: [{ key: "web-remote-ingress", enabled: true }],
833
+ }),
834
+ { status: 200, headers: { "content-type": "application/json" } },
835
+ );
836
+ }
837
+ if (url === `${LOCAL_URL}/v1/remote-web/pairing-challenge`) {
838
+ return new Response(
839
+ JSON.stringify({
840
+ deviceCode: "device-code",
841
+ userCode: "ABCD-EFGH",
842
+ verificationUri: "https://pair.example.ts.net/assistant/pair",
843
+ expiresAt: "2026-06-04T00:10:00.000Z",
844
+ expiresInSeconds: 600,
845
+ }),
846
+ { status: 200, headers: { "content-type": "application/json" } },
847
+ );
848
+ }
849
+ if (url === `${LOCAL_URL}/v1/remote-web/pairing-verification`) {
850
+ return new Response(
851
+ JSON.stringify({
852
+ status: "approved",
853
+ verificationUri: "https://pair.example.ts.net/assistant/pair",
854
+ expiresAt: "2026-06-04T00:10:00.000Z",
855
+ }),
856
+ { status: 200, headers: { "content-type": "application/json" } },
857
+ );
858
+ }
859
+ return new Response("not found", { status: 404 });
860
+ }) as unknown as typeof fetch;
861
+
862
+ const logs: string[] = [];
863
+ const logSpy = spyOn(console, "log").mockImplementation(
864
+ (...a: unknown[]) => {
865
+ logs.push(a.join(" "));
866
+ },
867
+ );
868
+
869
+ process.argv = [
870
+ "bun",
871
+ "vellum",
872
+ "pair",
873
+ "--qr",
874
+ "--app",
875
+ "--app-scheme",
876
+ "vellum-assistant-dev",
877
+ "--url",
878
+ "https://pair.example.ts.net",
879
+ "--json",
880
+ ];
881
+ try {
882
+ await pair();
883
+ } finally {
884
+ logSpy.mockRestore();
885
+ globalThis.fetch = origFetch;
886
+ }
887
+
888
+ const out = JSON.parse(logs.join("\n"));
889
+ expect(out.appUrl).toBe(
890
+ "vellum-assistant-dev://connect?url=https%3A%2F%2Fpair.example.ts.net&code=device-code",
891
+ );
892
+ // The browser URL stays available as the no-app fallback.
893
+ expect(out.pairUrl).toBe(
894
+ "https://pair.example.ts.net/assistant/pair#device_code=device-code",
895
+ );
896
+ expect(out.deviceCode).toBe("device-code");
897
+ });
898
+
899
+ test("--app without --qr is refused", async () => {
900
+ let fetchCalled = false;
901
+ const origFetch = globalThis.fetch;
902
+ globalThis.fetch = (async () => {
903
+ fetchCalled = true;
904
+ return new Response("{}", { status: 200 });
905
+ }) as unknown as typeof fetch;
906
+ const errors: string[] = [];
907
+ const errSpy = spyOn(console, "error").mockImplementation(
908
+ (...a: unknown[]) => {
909
+ errors.push(a.join(" "));
910
+ },
911
+ );
912
+ const exitSpy = spyOn(process, "exit").mockImplementation(((
913
+ code?: number,
914
+ ) => {
915
+ throw new Error(`exit:${code}`);
916
+ }) as never);
917
+
918
+ process.argv = ["bun", "vellum", "pair", "--app"];
919
+ let exited = false;
920
+ try {
921
+ await pair();
922
+ } catch (e) {
923
+ exited = (e as Error).message === "exit:1";
924
+ } finally {
925
+ errSpy.mockRestore();
926
+ exitSpy.mockRestore();
927
+ globalThis.fetch = origFetch;
928
+ }
929
+
930
+ expect(exited).toBe(true);
931
+ expect(errors.join("\n")).toContain("--qr");
932
+ expect(fetchCalled).toBe(false);
933
+ });
803
934
  });
@@ -240,7 +240,11 @@ describe("fetchAssistantDetail / fetchUpgradeInProgress", () => {
240
240
  current_release_version: "0.7.0",
241
241
  release_channel: "preview",
242
242
  });
243
- const detail = await fetchAssistantDetail(TOKEN, ASSISTANT_ID, PLATFORM_URL);
243
+ const detail = await fetchAssistantDetail(
244
+ TOKEN,
245
+ ASSISTANT_ID,
246
+ PLATFORM_URL,
247
+ );
244
248
  expect(detail).toEqual({
245
249
  currentReleaseVersion: "0.7.0",
246
250
  releaseChannel: "preview",
@@ -251,7 +255,11 @@ describe("fetchAssistantDetail / fetchUpgradeInProgress", () => {
251
255
 
252
256
  test("fetchAssistantDetail defaults missing fields", async () => {
253
257
  mockFetchJson({});
254
- const detail = await fetchAssistantDetail(TOKEN, ASSISTANT_ID, PLATFORM_URL);
258
+ const detail = await fetchAssistantDetail(
259
+ TOKEN,
260
+ ASSISTANT_ID,
261
+ PLATFORM_URL,
262
+ );
255
263
  expect(detail).toEqual({
256
264
  currentReleaseVersion: null,
257
265
  releaseChannel: "stable",
@@ -56,7 +56,9 @@ describe("getBuilderManagedEnvKeys", () => {
56
56
 
57
57
  test("gateway hostForwarded equals the three spec host entries", () => {
58
58
  const { hostForwarded } = getBuilderManagedEnvKeys("gateway");
59
- const sorted = [...hostForwarded].sort((a, b) => a.name.localeCompare(b.name));
59
+ const sorted = [...hostForwarded].sort((a, b) =>
60
+ a.name.localeCompare(b.name),
61
+ );
60
62
  expect(sorted).toEqual([
61
63
  { name: "VELAY_BASE_URL", hostVar: "VELAY_BASE_URL" },
62
64
  { name: "VELLUM_ENVIRONMENT", hostVar: "VELLUM_ENVIRONMENT" },
@@ -0,0 +1,229 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ import {
7
+ normalizeDnsName,
8
+ resolveServeHostname,
9
+ shouldClearIngressUrl,
10
+ startTailscaleServe,
11
+ stopTailscaleServe,
12
+ type TailscaleCommandResult,
13
+ type TailscaleDeps,
14
+ } from "../lib/tailscale-tunnel.js";
15
+
16
+ // ── Helpers ───────────────────────────────────────────────────────────────────
17
+
18
+ const tempDirs: string[] = [];
19
+
20
+ function makeWorkspace(): string {
21
+ const dir = mkdtempSync(join(tmpdir(), "vellum-tailscale-test-"));
22
+ tempDirs.push(dir);
23
+ return dir;
24
+ }
25
+
26
+ const RUNNING_STATUS = JSON.stringify({
27
+ BackendState: "Running",
28
+ Self: { DNSName: "my-host.tail-scale.ts.net.", Online: true },
29
+ });
30
+
31
+ const ok = (stdout = "", stderr = ""): TailscaleCommandResult => ({
32
+ status: 0,
33
+ stdout,
34
+ stderr,
35
+ });
36
+
37
+ /**
38
+ * Build a fake {@link TailscaleDeps}. `binary` defaults to a present binary;
39
+ * pass `null` to simulate a missing install. `responses` is keyed by the
40
+ * space-joined args; unmatched calls return an empty success result.
41
+ */
42
+ function makeDeps(opts: {
43
+ binary?: string | null;
44
+ responses?: Record<string, TailscaleCommandResult>;
45
+ }): { deps: TailscaleDeps; calls: string[][] } {
46
+ const calls: string[][] = [];
47
+ const binary = opts.binary === undefined ? "tailscale" : opts.binary;
48
+ const deps: TailscaleDeps = {
49
+ findBinary: () => binary,
50
+ run: (_bin, args) => {
51
+ calls.push(args);
52
+ return opts.responses?.[args.join(" ")] ?? ok();
53
+ },
54
+ };
55
+ return { deps, calls };
56
+ }
57
+
58
+ afterEach(() => {
59
+ for (const dir of tempDirs.splice(0)) {
60
+ rmSync(dir, { recursive: true, force: true });
61
+ }
62
+ });
63
+
64
+ // ── normalizeDnsName ────────────────────────────────────────────────────────
65
+
66
+ describe("normalizeDnsName", () => {
67
+ test("trims the trailing MagicDNS dot", () => {
68
+ expect(normalizeDnsName("host.tailnet.ts.net.")).toBe(
69
+ "host.tailnet.ts.net",
70
+ );
71
+ });
72
+
73
+ test("leaves a name without a trailing dot unchanged", () => {
74
+ expect(normalizeDnsName("host.tailnet.ts.net")).toBe("host.tailnet.ts.net");
75
+ });
76
+ });
77
+
78
+ // ── resolveServeHostname ──────────────────────────────────────────────────────
79
+
80
+ describe("resolveServeHostname", () => {
81
+ test("returns the trimmed Self.DNSName for a running node", () => {
82
+ expect(resolveServeHostname(RUNNING_STATUS)).toBe(
83
+ "my-host.tail-scale.ts.net",
84
+ );
85
+ });
86
+
87
+ test("throws a login hint when logged out", () => {
88
+ const json = JSON.stringify({ BackendState: "NeedsLogin", Self: {} });
89
+ expect(() => resolveServeHostname(json)).toThrow("not logged in");
90
+ });
91
+
92
+ test("throws when the backend is not running", () => {
93
+ const json = JSON.stringify({
94
+ BackendState: "Stopped",
95
+ Self: { DNSName: "host.ts.net." },
96
+ });
97
+ expect(() => resolveServeHostname(json)).toThrow("not ready");
98
+ });
99
+
100
+ test("throws when the DNS name is missing", () => {
101
+ const json = JSON.stringify({ BackendState: "Running", Self: {} });
102
+ expect(() => resolveServeHostname(json)).toThrow("DNS name");
103
+ });
104
+
105
+ test("throws on unparseable JSON", () => {
106
+ expect(() => resolveServeHostname("not json")).toThrow("Could not parse");
107
+ });
108
+ });
109
+
110
+ // ── startTailscaleServe ───────────────────────────────────────────────────────
111
+
112
+ describe("startTailscaleServe", () => {
113
+ test("throws install guidance when the binary is missing", async () => {
114
+ const { deps, calls } = makeDeps({ binary: null });
115
+ await expect(
116
+ startTailscaleServe({ port: 7840, workspaceDir: makeWorkspace() }, deps),
117
+ ).rejects.toThrow("Tailscale is not installed");
118
+ expect(calls).toHaveLength(0);
119
+ });
120
+
121
+ test("throws a login hint when logged out and never calls serve", async () => {
122
+ const { deps, calls } = makeDeps({
123
+ responses: {
124
+ "status --json": ok(
125
+ JSON.stringify({ BackendState: "NeedsLogin", Self: {} }),
126
+ ),
127
+ },
128
+ });
129
+ await expect(
130
+ startTailscaleServe({ port: 7840, workspaceDir: makeWorkspace() }, deps),
131
+ ).rejects.toThrow("not logged in");
132
+ expect(calls).not.toContainEqual(["serve", "--bg", "7840"]);
133
+ });
134
+
135
+ test("throws when the status query fails (daemon down)", async () => {
136
+ const { deps } = makeDeps({
137
+ responses: {
138
+ "status --json": {
139
+ status: 1,
140
+ stdout: "",
141
+ stderr: "failed to connect to local tailscaled",
142
+ },
143
+ },
144
+ });
145
+ await expect(
146
+ startTailscaleServe({ port: 7840, workspaceDir: makeWorkspace() }, deps),
147
+ ).rejects.toThrow("Could not query Tailscale");
148
+ });
149
+
150
+ test("serves the target port and persists the ingress URL on success", async () => {
151
+ const workspaceDir = makeWorkspace();
152
+ const { deps, calls } = makeDeps({
153
+ responses: {
154
+ "status --json": ok(RUNNING_STATUS),
155
+ "serve --bg 7840": ok(),
156
+ },
157
+ });
158
+
159
+ const info = await startTailscaleServe({ port: 7840, workspaceDir }, deps);
160
+
161
+ expect(info.publicUrl).toBe("https://my-host.tail-scale.ts.net");
162
+ expect(info.port).toBe(7840);
163
+ expect(info.viaIngress).toBe(false);
164
+ expect(calls).toContainEqual(["serve", "--bg", "7840"]);
165
+
166
+ const config = JSON.parse(
167
+ readFileSync(join(workspaceDir, "config.json"), "utf-8"),
168
+ );
169
+ expect(config.ingress.publicBaseUrl).toBe(
170
+ "https://my-host.tail-scale.ts.net",
171
+ );
172
+ expect(config.ingress.enabled).toBe(true);
173
+ });
174
+
175
+ test("surfaces tailscale's enable-URL guidance when serve is not enabled", async () => {
176
+ const workspaceDir = makeWorkspace();
177
+ const enableUrl = "https://login.tailscale.com/f/serve?node=abc123";
178
+ const { deps } = makeDeps({
179
+ responses: {
180
+ "status --json": ok(RUNNING_STATUS),
181
+ "serve --bg 7840": {
182
+ status: 1,
183
+ stdout: "",
184
+ stderr: `error: Serve is not enabled on your tailnet.\n\nTo enable, visit:\n ${enableUrl}`,
185
+ },
186
+ },
187
+ });
188
+
189
+ await expect(
190
+ startTailscaleServe({ port: 7840, workspaceDir }, deps),
191
+ ).rejects.toThrow(enableUrl);
192
+
193
+ // A failed serve must not persist an ingress URL.
194
+ expect(existsSync(join(workspaceDir, "config.json"))).toBe(false);
195
+ });
196
+ });
197
+
198
+ // ── stopTailscaleServe ────────────────────────────────────────────────────────
199
+
200
+ describe("stopTailscaleServe", () => {
201
+ test("turns off the HTTPS:443 serve (narrow off form)", () => {
202
+ const { deps, calls } = makeDeps({
203
+ responses: { "serve --https=443 off": ok() },
204
+ });
205
+ const result = stopTailscaleServe("tailscale", deps);
206
+ expect(result.status).toBe(0);
207
+ expect(calls).toContainEqual(["serve", "--https=443", "off"]);
208
+ });
209
+ });
210
+
211
+ // ── shouldClearIngressUrl ─────────────────────────────────────────────────────
212
+
213
+ describe("shouldClearIngressUrl", () => {
214
+ test("clears only after a confirmed successful stop", () => {
215
+ expect(shouldClearIngressUrl({ status: 0, stdout: "", stderr: "" })).toBe(
216
+ true,
217
+ );
218
+ });
219
+
220
+ test("keeps the URL when the stop command fails", () => {
221
+ expect(
222
+ shouldClearIngressUrl({ status: 1, stdout: "", stderr: "boom" }),
223
+ ).toBe(false);
224
+ });
225
+
226
+ test("keeps the URL when the stop command threw before running", () => {
227
+ expect(shouldClearIngressUrl(null)).toBe(false);
228
+ });
229
+ });
@@ -27,9 +27,8 @@ const realLocal = { ...local };
27
27
  const realNgrok = { ...ngrok };
28
28
  const realProcessLib = { ...processLib };
29
29
 
30
- const resolveTargetAssistantMock = mock<
31
- typeof assistantConfig.resolveTargetAssistant
32
- >();
30
+ const resolveTargetAssistantMock =
31
+ mock<typeof assistantConfig.resolveTargetAssistant>();
33
32
  const saveAssistantEntryMock = mock<typeof assistantConfig.saveAssistantEntry>(
34
33
  () => {},
35
34
  );
@@ -61,9 +60,10 @@ const seedGuardianTokenFromSiblingEnvMock = mock<
61
60
  // Default: a token exists, so the re-provision recovery path is skipped. Tests
62
61
  // that exercise recovery override loadGuardianToken to return null.
63
62
  const loadGuardianTokenMock = mock<typeof guardianToken.loadGuardianToken>(
64
- () => ({ accessToken: "existing" }) as ReturnType<
65
- typeof guardianToken.loadGuardianToken
66
- >,
63
+ () =>
64
+ ({ accessToken: "existing" }) as ReturnType<
65
+ typeof guardianToken.loadGuardianToken
66
+ >,
67
67
  );
68
68
  const resetGuardianBootstrapMock = mock<
69
69
  typeof guardianToken.resetGuardianBootstrap
@@ -111,7 +111,9 @@ const isAssistantWatchModeAvailableMock = mock<
111
111
  const isGatewayWatchModeAvailableMock = mock<
112
112
  typeof local.isGatewayWatchModeAvailable
113
113
  >(() => false);
114
- const startLocalDaemonMock = mock<typeof local.startLocalDaemon>(async () => {});
114
+ const startLocalDaemonMock = mock<typeof local.startLocalDaemon>(
115
+ async () => {},
116
+ );
115
117
  const startGatewayMock = mock<typeof local.startGateway>(
116
118
  async () => "http://127.0.0.1:7830",
117
119
  );
@@ -175,10 +177,12 @@ beforeEach(() => {
175
177
  join(resources!.instanceDir, ".vellum", "daemon.pid"),
176
178
  );
177
179
  resolveProcessStateMock.mockReset();
178
- resolveProcessStateMock.mockImplementation(async (_pidFile, _port, label) => ({
179
- status: "healthy",
180
- pid: label === "Gateway" ? 456 : 123,
181
- }));
180
+ resolveProcessStateMock.mockImplementation(
181
+ async (_pidFile, _port, label) => ({
182
+ status: "healthy",
183
+ pid: label === "Gateway" ? 456 : 123,
184
+ }),
185
+ );
182
186
  stopProcessByPidFileMock.mockReset();
183
187
  stopProcessByPidFileMock.mockResolvedValue(true);
184
188
  generateLocalSigningKeyMock.mockReset();
@@ -198,9 +202,9 @@ beforeEach(() => {
198
202
  seedGuardianTokenFromSiblingEnvMock.mockReset();
199
203
  seedGuardianTokenFromSiblingEnvMock.mockReturnValue(false);
200
204
  loadGuardianTokenMock.mockReset();
201
- loadGuardianTokenMock.mockReturnValue({ accessToken: "existing" } as ReturnType<
202
- typeof guardianToken.loadGuardianToken
203
- >);
205
+ loadGuardianTokenMock.mockReturnValue({
206
+ accessToken: "existing",
207
+ } as ReturnType<typeof guardianToken.loadGuardianToken>);
204
208
  resetGuardianBootstrapMock.mockReset();
205
209
  resetGuardianBootstrapMock.mockResolvedValue(undefined);
206
210
  leaseGuardianTokenMock.mockReset();
@@ -252,7 +256,13 @@ describe("vellum wake", () => {
252
256
  });
253
257
 
254
258
  test("re-provisions the guardian token when missing and --repair-guardian is passed", async () => {
255
- process.argv = ["bun", "vellum", "wake", "--repair-guardian", "local-assistant"];
259
+ process.argv = [
260
+ "bun",
261
+ "vellum",
262
+ "wake",
263
+ "--repair-guardian",
264
+ "local-assistant",
265
+ ];
256
266
  loadGuardianTokenMock.mockReturnValue(null);
257
267
 
258
268
  await wake();
@@ -288,7 +298,13 @@ describe("vellum wake", () => {
288
298
  // (revoked, mis-seeded, wrong principal). The user explicitly confirmed
289
299
  // the destructive repair, so the flag forces a re-lease instead of
290
300
  // guessing from local token state and recreating the no-op loop.
291
- process.argv = ["bun", "vellum", "wake", "--repair-guardian", "local-assistant"];
301
+ process.argv = [
302
+ "bun",
303
+ "vellum",
304
+ "wake",
305
+ "--repair-guardian",
306
+ "local-assistant",
307
+ ];
292
308
  // loadGuardianToken returns a healthy-looking token by default.
293
309
  await wake();
294
310
 
@@ -134,15 +134,18 @@ export async function backup(): Promise<void> {
134
134
  // Call the export endpoint
135
135
  let response: Response;
136
136
  try {
137
- response = await loopbackSafeFetch(`${entry.runtimeUrl}/v1/migrations/export`, {
138
- method: "POST",
139
- headers: {
140
- Authorization: `Bearer ${accessToken}`,
141
- "Content-Type": "application/json",
137
+ response = await loopbackSafeFetch(
138
+ `${entry.runtimeUrl}/v1/migrations/export`,
139
+ {
140
+ method: "POST",
141
+ headers: {
142
+ Authorization: `Bearer ${accessToken}`,
143
+ "Content-Type": "application/json",
144
+ },
145
+ body: JSON.stringify({ description: "CLI backup" }),
146
+ signal: AbortSignal.timeout(exportTimeoutMs),
142
147
  },
143
- body: JSON.stringify({ description: "CLI backup" }),
144
- signal: AbortSignal.timeout(exportTimeoutMs),
145
- });
148
+ );
146
149
 
147
150
  // Retry once with a fresh token on 401 — the cached token may be stale
148
151
  // after a container restart that generated a new gateway signing key.
@@ -160,15 +163,18 @@ export async function backup(): Promise<void> {
160
163
  }
161
164
  if (refreshedToken) {
162
165
  accessToken = refreshedToken;
163
- response = await loopbackSafeFetch(`${entry.runtimeUrl}/v1/migrations/export`, {
164
- method: "POST",
165
- headers: {
166
- Authorization: `Bearer ${accessToken}`,
167
- "Content-Type": "application/json",
166
+ response = await loopbackSafeFetch(
167
+ `${entry.runtimeUrl}/v1/migrations/export`,
168
+ {
169
+ method: "POST",
170
+ headers: {
171
+ Authorization: `Bearer ${accessToken}`,
172
+ "Content-Type": "application/json",
173
+ },
174
+ body: JSON.stringify({ description: "CLI backup" }),
175
+ signal: AbortSignal.timeout(exportTimeoutMs),
168
176
  },
169
- body: JSON.stringify({ description: "CLI backup" }),
170
- signal: AbortSignal.timeout(exportTimeoutMs),
171
- });
177
+ );
172
178
  }
173
179
  }
174
180
  } catch (err) {
@@ -220,7 +220,11 @@ export async function exec(): Promise<void> {
220
220
 
221
221
  if (interactive) {
222
222
  // Interactive mode: shell-escape argv and delegate to full terminal
223
- await interactiveSession(assistant, shellEscapeArgs(command), serviceParam);
223
+ await interactiveSession(
224
+ assistant,
225
+ shellEscapeArgs(command),
226
+ serviceParam,
227
+ );
224
228
  return;
225
229
  }
226
230