@sudajs/cli 0.15.0 → 0.16.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/dist/index.js CHANGED
@@ -15,6 +15,7 @@ import { build } from 'esbuild';
15
15
  import { z } from 'zod';
16
16
  import os from 'os';
17
17
  import pc from 'picocolors';
18
+ import readline from 'readline';
18
19
 
19
20
  var style = {
20
21
  success: pc.green,
@@ -24,7 +25,8 @@ var style = {
24
25
  url: pc.cyan,
25
26
  code: pc.bold,
26
27
  value: pc.bold,
27
- path: pc.magenta
28
+ path: pc.magenta,
29
+ selected: (value) => pc.bgCyan(pc.black(value))
28
30
  };
29
31
  function success(message) {
30
32
  return style.success(message);
@@ -41,72 +43,306 @@ function info(message) {
41
43
  function themeRef(key, version) {
42
44
  return style.value(`${key}@${version}`);
43
45
  }
46
+ var selectChoice = async (message, choices, defaultValue) => {
47
+ if (choices.length === 0) {
48
+ throw new Error("No choices are available.");
49
+ }
50
+ const defaultIndex = Math.max(
51
+ 0,
52
+ choices.findIndex((choice) => choice.value === defaultValue)
53
+ );
54
+ const fallback = choices[defaultIndex];
55
+ if (choices.length === 1 || !process.stdin.isTTY || !process.stdout.isTTY) {
56
+ return fallback.value;
57
+ }
58
+ const input = process.stdin;
59
+ const output = process.stdout;
60
+ const wasRaw = input.isRaw;
61
+ const wasFlowing = input.readableFlowing;
62
+ let selectedIndex = defaultIndex;
63
+ readline.emitKeypressEvents(input);
64
+ input.setRawMode(true);
65
+ input.resume();
66
+ const render = (replace) => {
67
+ if (replace) {
68
+ readline.moveCursor(output, 0, -(choices.length + 1));
69
+ readline.cursorTo(output, 0);
70
+ readline.clearScreenDown(output);
71
+ }
72
+ output.write(`${style.info("?")} ${message}
73
+ `);
74
+ for (const [index, choice] of choices.entries()) {
75
+ if (index === selectedIndex) {
76
+ output.write(`${style.info("\u276F")} ${style.selected(` ${choice.label} `)}
77
+ `);
78
+ } else {
79
+ output.write(` ${choice.label}
80
+ `);
81
+ }
82
+ }
83
+ };
84
+ render(false);
85
+ return new Promise((resolve, reject) => {
86
+ const cleanup = () => {
87
+ input.off("keypress", onKeypress);
88
+ input.setRawMode(wasRaw);
89
+ if (wasFlowing !== true) {
90
+ input.pause();
91
+ }
92
+ };
93
+ const finish = () => {
94
+ readline.moveCursor(output, 0, -(choices.length + 1));
95
+ readline.cursorTo(output, 0);
96
+ readline.clearScreenDown(output);
97
+ const selected = choices[selectedIndex];
98
+ output.write(`${success("\u2714")} ${message} ${style.url(selected.value)}
99
+ `);
100
+ cleanup();
101
+ resolve(selected.value);
102
+ };
103
+ const onKeypress = (_character, key) => {
104
+ if (key.ctrl === true && key.name === "c") {
105
+ cleanup();
106
+ reject(new Error("Host selection cancelled."));
107
+ return;
108
+ }
109
+ if (key.name === "return" || key.name === "enter") {
110
+ finish();
111
+ return;
112
+ }
113
+ if (key.name === "up") {
114
+ selectedIndex = (selectedIndex - 1 + choices.length) % choices.length;
115
+ render(true);
116
+ return;
117
+ }
118
+ if (key.name === "down") {
119
+ selectedIndex = (selectedIndex + 1) % choices.length;
120
+ render(true);
121
+ }
122
+ };
123
+ input.on("keypress", onKeypress);
124
+ });
125
+ };
44
126
 
45
127
  // src/auth.ts
128
+ var DEFAULT_HOST = "app.sudayun.cn";
129
+ var GLOBAL_HOST = "app.sudaweb.ai";
130
+ var officialHostChoices = [
131
+ { label: `\u56FD\u5185(${DEFAULT_HOST})`, value: DEFAULT_HOST },
132
+ { label: `\u6D77\u5916(${GLOBAL_HOST})`, value: GLOBAL_HOST }
133
+ ];
134
+ var credentialSchema = z.object({
135
+ sessionToken: z.string().min(1)
136
+ });
137
+ var authStoreSchema = z.object({
138
+ currentHost: z.string().min(1).optional(),
139
+ hosts: z.record(z.string().min(1), credentialSchema)
140
+ });
141
+ var legacyAuthConfigSchema = z.object({
142
+ sessionToken: z.string().min(1),
143
+ host: z.string().min(1)
144
+ });
46
145
  function getConfigPath() {
47
146
  return path2.join(os.homedir(), ".config", "suda", "config.json");
48
147
  }
49
- async function readAuthConfig() {
148
+ function normalizeHost(value) {
149
+ const host = value.trim();
150
+ if (host.length === 0 || host.includes("://") || /[/?#@]/.test(host)) {
151
+ throw new Error(`Invalid host "${value}". Use a bare host name such as ${DEFAULT_HOST}.`);
152
+ }
153
+ try {
154
+ return new URL(`https://${host}`).host;
155
+ } catch {
156
+ throw new Error(`Invalid host "${value}". Use a bare host name such as ${DEFAULT_HOST}.`);
157
+ }
158
+ }
159
+ function protocolForHost(host) {
160
+ const hostname = new URL(`https://${host}`).hostname;
161
+ return hostname === "localhost" || hostname === "127.0.0.1" ? "http" : "https";
162
+ }
163
+ function baseUrlForHost(host) {
164
+ return `${protocolForHost(host)}://${host}`;
165
+ }
166
+ function parseAuthStore(value) {
167
+ const current = authStoreSchema.safeParse(value);
168
+ if (current.success) {
169
+ const hosts = {};
170
+ for (const [host2, credential] of Object.entries(current.data.hosts)) {
171
+ hosts[normalizeHost(host2)] = credential;
172
+ }
173
+ const store = { hosts };
174
+ if (current.data.currentHost !== void 0) {
175
+ store.currentHost = normalizeHost(current.data.currentHost);
176
+ }
177
+ return store;
178
+ }
179
+ const legacy = legacyAuthConfigSchema.parse(value);
180
+ const host = normalizeHost(legacy.host);
181
+ return {
182
+ currentHost: host,
183
+ hosts: {
184
+ [host]: { sessionToken: legacy.sessionToken }
185
+ }
186
+ };
187
+ }
188
+ async function readAuthStore() {
50
189
  try {
51
- const configPath = getConfigPath();
52
- const content = await fs.readFile(configPath, "utf-8");
53
- return JSON.parse(content);
190
+ const content = await fs.readFile(getConfigPath(), "utf8");
191
+ return parseAuthStore(JSON.parse(content));
54
192
  } catch {
55
193
  return null;
56
194
  }
57
195
  }
58
- async function writeAuthConfig(config) {
196
+ async function writeAuthStore(store) {
197
+ const validated = authStoreSchema.parse(store);
59
198
  const configPath = getConfigPath();
60
199
  await fs.mkdir(path2.dirname(configPath), { recursive: true });
61
200
  await fs.chmod(path2.dirname(configPath), 448).catch(() => void 0);
62
- await fs.writeFile(configPath, JSON.stringify(config, null, 2), { mode: 384 });
201
+ await fs.writeFile(configPath, `${JSON.stringify(validated, null, 2)}
202
+ `, { mode: 384 });
203
+ await fs.chmod(configPath, 384).catch(() => void 0);
63
204
  }
64
- async function clearAuthConfig() {
65
- const configPath = getConfigPath();
205
+ async function removeConfigFile() {
66
206
  try {
67
- await fs.unlink(configPath);
207
+ await fs.unlink(getConfigPath());
68
208
  } catch (err) {
69
209
  if (err instanceof Error && "code" in err && err.code !== "ENOENT") {
70
210
  throw err;
71
211
  }
72
212
  }
73
213
  }
74
- function protocolForHost(host) {
75
- return host.includes("localhost") || host.includes("127.0.0.1") ? "http" : "https";
214
+ async function saveCredential(hostValue, sessionToken) {
215
+ const host = normalizeHost(hostValue);
216
+ const store = await readAuthStore() ?? { hosts: {} };
217
+ store.hosts[host] = credentialSchema.parse({ sessionToken });
218
+ store.currentHost = host;
219
+ await writeAuthStore(store);
220
+ return { host, sessionToken, baseUrl: baseUrlForHost(host) };
76
221
  }
77
- var CliAuthExpiredError = class extends Error {
78
- code;
79
- constructor(code, message) {
80
- super(message);
81
- this.name = "CliAuthExpiredError";
82
- this.code = code;
222
+ async function removeCredential(hostValue) {
223
+ const host = normalizeHost(hostValue);
224
+ const store = await readAuthStore();
225
+ if (!store?.hosts[host]) {
226
+ return;
83
227
  }
84
- };
85
- async function cliAuthFetch(pathname, init = {}) {
86
- const config = await readAuthConfig();
87
- if (!config) {
228
+ delete store.hosts[host];
229
+ if (store.currentHost === host) {
230
+ delete store.currentHost;
231
+ }
232
+ if (Object.keys(store.hosts).length === 0) {
233
+ await removeConfigFile();
234
+ return;
235
+ }
236
+ await writeAuthStore(store);
237
+ }
238
+ function hostLabel(host) {
239
+ if (host === DEFAULT_HOST) {
240
+ return `\u56FD\u5185(${host})`;
241
+ }
242
+ if (host === GLOBAL_HOST) {
243
+ return `\u6D77\u5916(${host})`;
244
+ }
245
+ return host;
246
+ }
247
+ function createHostChoices(hosts) {
248
+ return hosts.map((host) => ({ label: hostLabel(host), value: host }));
249
+ }
250
+ async function resolveLoginHost(hostValue, prompt = selectChoice) {
251
+ if (hostValue !== void 0) {
252
+ return normalizeHost(hostValue);
253
+ }
254
+ const store = await readAuthStore();
255
+ const hosts = new Set(officialHostChoices.map((choice) => choice.value));
256
+ for (const host of Object.keys(store?.hosts ?? {})) {
257
+ hosts.add(host);
258
+ }
259
+ const defaultHost = store?.currentHost ?? DEFAULT_HOST;
260
+ return prompt("Select a SudaCloud host to log in to:", createHostChoices([...hosts]), defaultHost);
261
+ }
262
+ async function resolveAuthenticatedHost(hostValue, message, prompt = selectChoice) {
263
+ if (hostValue !== void 0) {
264
+ return normalizeHost(hostValue);
265
+ }
266
+ const store = await readAuthStore();
267
+ const hosts = Object.keys(store?.hosts ?? {}).sort();
268
+ if (hosts.length === 0) {
269
+ return void 0;
270
+ }
271
+ return prompt(message, createHostChoices(hosts), store?.currentHost ?? hosts[0]);
272
+ }
273
+ async function readAuthConfig(hostValue) {
274
+ const store = await readAuthStore();
275
+ if (!store) {
276
+ return null;
277
+ }
278
+ const host = hostValue !== void 0 ? normalizeHost(hostValue) : store.currentHost;
279
+ if (host === void 0) {
280
+ return null;
281
+ }
282
+ const credential = store.hosts[host];
283
+ if (!credential) {
284
+ return null;
285
+ }
286
+ return {
287
+ host,
288
+ sessionToken: credential.sessionToken,
289
+ baseUrl: baseUrlForHost(host)
290
+ };
291
+ }
292
+ async function requireAuthConfig(hostValue) {
293
+ const config = await readAuthConfig(hostValue);
294
+ if (config) {
295
+ return config;
296
+ }
297
+ if (hostValue !== void 0) {
298
+ const host = normalizeHost(hostValue);
299
+ throw new Error(
300
+ `Not logged in to ${host}. Run \`suda auth login --host ${host}\` to authenticate first.`
301
+ );
302
+ }
303
+ throw new Error(
304
+ "No current authenticated host. Run `suda auth login` or `suda host switch <host>` first."
305
+ );
306
+ }
307
+ async function selectAuthConfig(hostValue, message) {
308
+ const host = await resolveAuthenticatedHost(hostValue, message);
309
+ return requireAuthConfig(host);
310
+ }
311
+ async function cliAuthFetch(pathname, init = {}, hostValue) {
312
+ let config;
313
+ try {
314
+ config = await requireAuthConfig(hostValue);
315
+ } catch (err) {
88
316
  throw new CliAuthExpiredError(
89
317
  "unauthorized",
90
- "Not logged in. Run `suda auth login` to authenticate first."
318
+ err instanceof Error ? err.message : "Not logged in. Run `suda auth login` first."
91
319
  );
92
320
  }
93
- const baseUrl = `${protocolForHost(config.host)}://${config.host}`;
94
321
  const headers = new Headers(init.headers);
95
322
  headers.set("Authorization", `Bearer ${config.sessionToken}`);
96
- const res = await fetch(`${baseUrl}${pathname}`, { ...init, headers });
323
+ const res = await fetch(`${config.baseUrl}${pathname}`, { ...init, headers });
97
324
  if (res.status === 401) {
98
325
  const body = await res.clone().json().catch(() => ({}));
99
326
  const code = ["token_invalid", "token_revoked", "token_expired"].includes(body.error ?? "") ? body.error : "unauthorized";
100
- await clearAuthConfig();
327
+ await removeCredential(config.host);
101
328
  throw new CliAuthExpiredError(
102
329
  code,
103
- body.message ?? "Authentication is no longer valid. Run `suda auth login` to authenticate again."
330
+ body.message ?? `Authentication for ${config.host} is no longer valid. Run \`suda auth login --host ${config.host}\` to authenticate again.`
104
331
  );
105
332
  }
106
333
  return res;
107
334
  }
108
- async function login(host = "app.sudayun.cn") {
109
- const baseUrl = `${protocolForHost(host)}://${host}`;
335
+ var CliAuthExpiredError = class extends Error {
336
+ code;
337
+ constructor(code, message) {
338
+ super(message);
339
+ this.name = "CliAuthExpiredError";
340
+ this.code = code;
341
+ }
342
+ };
343
+ async function login(hostValue) {
344
+ const host = await resolveLoginHost(hostValue);
345
+ const baseUrl = baseUrlForHost(host);
110
346
  console.log(`Requesting device authorization from ${style.url(baseUrl)}...`);
111
347
  const deviceRes = await fetch(`${baseUrl}/api/cli-auth/device`, {
112
348
  method: "POST"
@@ -116,9 +352,7 @@ async function login(host = "app.sudayun.cn") {
116
352
  }
117
353
  const { deviceCode, userCode, verificationUri, interval, expiresIn } = await deviceRes.json();
118
354
  const authUrl = `${verificationUri}?code=${userCode}`;
119
- console.log(`
120
- Please open the following URL in your browser to authorize Suda CLI:
121
- `);
355
+ console.log("\nPlease open the following URL in your browser to authorize Suda CLI:\n");
122
356
  console.log(` ${style.url(authUrl)}
123
357
  `);
124
358
  console.log(`Your confirmation code is: ${style.code(userCode)}
@@ -141,8 +375,8 @@ Please open the following URL in your browser to authorize Suda CLI:
141
375
  });
142
376
  const data = await pollRes.json();
143
377
  if (pollRes.ok && data.status === "approved" && data.token) {
144
- await writeAuthConfig({ sessionToken: data.token, host });
145
- console.log(success("Successfully authorized!"));
378
+ await saveCredential(host, data.token);
379
+ console.log(success(`Successfully authorized ${style.url(host)} and set it as current.`));
146
380
  return;
147
381
  }
148
382
  if (data.error === "authorization_pending") {
@@ -152,15 +386,17 @@ Please open the following URL in your browser to authorize Suda CLI:
152
386
  }
153
387
  throw new Error("Authorization timed out.");
154
388
  }
155
- async function status() {
156
- const config = await readAuthConfig();
157
- if (!config) {
158
- console.log(warning("Not logged in. Run `suda auth login` to authenticate."));
389
+ async function status(hostValue) {
390
+ let config;
391
+ try {
392
+ config = await selectAuthConfig(hostValue, "Select a host to check:");
393
+ } catch (err) {
394
+ console.log(warning(err instanceof Error ? err.message : "Not logged in."));
159
395
  return;
160
396
  }
161
397
  let res;
162
398
  try {
163
- res = await cliAuthFetch("/api/cli-auth/whoami");
399
+ res = await cliAuthFetch("/api/cli-auth/whoami", {}, config.host);
164
400
  } catch (err) {
165
401
  if (err instanceof CliAuthExpiredError) {
166
402
  console.log(warning(err.message));
@@ -171,7 +407,9 @@ async function status() {
171
407
  }
172
408
  if (!res.ok) {
173
409
  console.error(
174
- error(`Failed to verify session (${res.status} ${res.statusText}). Try again later.`)
410
+ error(
411
+ `Failed to verify session on ${config.host} (${res.status} ${res.statusText}). Try again later.`
412
+ )
175
413
  );
176
414
  return;
177
415
  }
@@ -179,27 +417,65 @@ async function status() {
179
417
  const label = data.user?.email ?? data.user?.name ?? "your account";
180
418
  console.log(success(`Logged in as ${style.value(label)} on ${style.url(config.host)}`));
181
419
  }
182
- async function logout() {
183
- const config = await readAuthConfig();
184
- if (!config) {
185
- console.log(warning("Not logged in."));
420
+ async function logout(hostValue) {
421
+ let config;
422
+ try {
423
+ config = await selectAuthConfig(hostValue, "Select a host to log out from:");
424
+ } catch (err) {
425
+ console.log(warning(err instanceof Error ? err.message : "Not logged in."));
186
426
  return;
187
427
  }
188
428
  try {
189
- await cliAuthFetch("/api/cli-auth/revoke", { method: "POST" });
429
+ await cliAuthFetch("/api/cli-auth/revoke", { method: "POST" }, config.host);
190
430
  } catch (err) {
191
431
  if (err instanceof CliAuthExpiredError) {
192
- console.log(warning("Logged out (token was already invalid)."));
432
+ console.log(warning(`Logged out from ${config.host} (token was already invalid).`));
193
433
  return;
194
434
  }
195
435
  console.warn(
196
436
  warning(
197
- `Could not contact ${config.host} to revoke token. The token will be cleared locally only.`
437
+ `Could not contact ${config.host} to revoke token. Its credential will be cleared locally only.`
198
438
  )
199
439
  );
200
440
  }
201
- await clearAuthConfig();
202
- console.log(success("Logged out successfully."));
441
+ await removeCredential(config.host);
442
+ console.log(success(`Logged out from ${style.url(config.host)} successfully.`));
443
+ }
444
+ async function showCurrentHost() {
445
+ const store = await readAuthStore();
446
+ if (!store?.currentHost || !store.hosts[store.currentHost]) {
447
+ console.log(
448
+ warning("No current authenticated host. Run `suda auth login` or `suda host switch <host>` first.")
449
+ );
450
+ return;
451
+ }
452
+ console.log(`Current host: ${style.url(store.currentHost)}`);
453
+ }
454
+ async function listHosts() {
455
+ const store = await readAuthStore();
456
+ const hosts = Object.keys(store?.hosts ?? {}).sort();
457
+ if (hosts.length === 0) {
458
+ console.log(warning("No authenticated hosts. Run `suda auth login` first."));
459
+ return;
460
+ }
461
+ console.log("Authenticated hosts:");
462
+ for (const host of hosts) {
463
+ const marker = host === store?.currentHost ? success("*") : " ";
464
+ const suffix = host === store?.currentHost ? style.value(" (current)") : "";
465
+ console.log(` ${marker} ${style.url(host)}${suffix}`);
466
+ }
467
+ }
468
+ async function switchHost(hostValue) {
469
+ const host = normalizeHost(hostValue);
470
+ const store = await readAuthStore();
471
+ if (!store?.hosts[host]) {
472
+ throw new Error(
473
+ `Not logged in to ${host}. Run \`suda auth login --host ${host}\` to authenticate first.`
474
+ );
475
+ }
476
+ store.currentHost = host;
477
+ await writeAuthStore(store);
478
+ console.log(success(`Switched current host to ${style.url(host)}.`));
203
479
  }
204
480
 
205
481
  // src/index.ts
@@ -732,16 +1008,10 @@ async function readJsonIfExists(filePath) {
732
1008
  }
733
1009
  return readJson(filePath);
734
1010
  }
735
- function protocolForHost2(host) {
736
- return host.includes("localhost") || host.includes("127.0.0.1") ? "http" : "https";
737
- }
738
1011
  async function requireCliBaseUrl() {
739
- const config = await readAuthConfig();
740
- if (!config) {
741
- throw new Error("Not logged in. Run `suda auth login` to authenticate first.");
742
- }
1012
+ const config = await requireAuthConfig();
743
1013
  return {
744
- baseUrl: `${protocolForHost2(config.host)}://${config.host}`,
1014
+ baseUrl: config.baseUrl,
745
1015
  token: config.sessionToken
746
1016
  };
747
1017
  }
@@ -2881,7 +3151,7 @@ ${lines.join("\n")}`;
2881
3151
  }
2882
3152
  return message;
2883
3153
  }
2884
- async function publishTheme(root, skipBuild, force) {
3154
+ async function publishTheme(root, skipBuild, force, host) {
2885
3155
  const theme = skipBuild ? await finalizeTheme(root) : await buildTheme(root);
2886
3156
  const ok = await runThemeCheck(theme);
2887
3157
  if (!ok) {
@@ -2899,13 +3169,11 @@ async function publishTheme(root, skipBuild, force) {
2899
3169
  );
2900
3170
  }
2901
3171
  }
2902
- const config = await readAuthConfig();
2903
- if (!config) {
2904
- throw new Error("Not logged in. Run `suda auth login` to authenticate first.");
2905
- }
2906
- const baseUrl = `${protocolForHost2(config.host)}://${config.host}`;
3172
+ const config = await selectAuthConfig(host, "Select a host to publish to:");
3173
+ const baseUrl = config.baseUrl;
2907
3174
  const { key, version } = theme.module.manifest;
2908
3175
  const digest = await checksum(files);
3176
+ console.log(info(`publishing ${themeRef(key, version)} to ${style.url(baseUrl)}`));
2909
3177
  if (force) {
2910
3178
  const forceRes = await fetch(`${baseUrl}/api/cli/themes/publish-force`, {
2911
3179
  method: "POST",
@@ -2988,7 +3256,7 @@ async function publishTheme(root, skipBuild, force) {
2988
3256
  if (!completeRes.ok) {
2989
3257
  throw new Error(`Failed to complete publish: ${await formatHttpErrorBody(completeRes)}`);
2990
3258
  }
2991
- console.log(success(`published ${themeRef(key, version)}`));
3259
+ console.log(success(`published ${themeRef(key, version)} to ${style.url(baseUrl)}`));
2992
3260
  }
2993
3261
  var cachedCliPackageVersions;
2994
3262
  async function readCliPackageVersions() {
@@ -3226,14 +3494,15 @@ function buildProgram() {
3226
3494
  ).option("--output <path>", "Output PNG path relative to theme root. Requires exactly one device.").option("--width <px>", "Override viewport width in pixels.").option("--height <px>", "Override viewport height in pixels.").option("--full-page", "Capture the full page instead of the viewport.").option("--port <port>", "Preview server port used during capture.", "4178").action(async (options) => {
3227
3495
  await screenshotTheme(resolveThemeRoot(options), options);
3228
3496
  });
3229
- theme.command("publish").description("Upload artifact to S3 and upsert ThemePackage/ThemeVersion.").option("--theme-root <path>", "Theme source/artifact root.").option("--skip-build", "Publish existing dist files without rebuilding.").option(
3497
+ theme.command("publish").description("Upload artifact to S3 and upsert ThemePackage/ThemeVersion.").option("--theme-root <path>", "Theme source/artifact root.").option("--skip-build", "Publish existing dist files without rebuilding.").option("--host <host>", "Publish to this authenticated SudaCloud workspace host.").option(
3230
3498
  "--force",
3231
3499
  "Development recovery only: clear the existing themes/<key>/<version>/ prefix and ThemeVersion row before republishing. Re-published clients pinned to this version are unavailable until the new publish completes."
3232
3500
  ).action(async (options) => {
3233
3501
  await publishTheme(
3234
3502
  resolveThemeRoot(options),
3235
3503
  options.skipBuild === true,
3236
- options.force === true
3504
+ options.force === true,
3505
+ options.host
3237
3506
  );
3238
3507
  });
3239
3508
  const agent = program.command("agent").description("Agent-friendly theme and page tooling.");
@@ -3299,19 +3568,28 @@ function buildProgram() {
3299
3568
  program.command("mcp").description("Run the Suda local MCP server over stdio.").action(async () => {
3300
3569
  await startMcpServer();
3301
3570
  });
3571
+ const hostCmd = program.command("host").description("Manage the current SudaCloud workspace host.");
3572
+ hostCmd.command("current").description("Show the current authenticated workspace host.").action(async () => {
3573
+ await showCurrentHost();
3574
+ });
3575
+ hostCmd.command("list").description("List authenticated workspace hosts.").action(async () => {
3576
+ await listHosts();
3577
+ });
3578
+ hostCmd.command("switch").description("Switch the current workspace host.").argument("<host>", "An existing authenticated SudaCloud workspace host.").action(async (host) => {
3579
+ await switchHost(host);
3580
+ });
3302
3581
  const authCmd = program.command("auth").description("Manage Suda authentication.");
3303
3582
  authCmd.command("login").description("Authenticate Suda CLI with a SudaCloud workspace.").option(
3304
3583
  "--host <host>",
3305
- "The SudaCloud workspace host to authenticate against.",
3306
- "app.sudayun.cn"
3584
+ "The SudaCloud workspace host to authenticate against."
3307
3585
  ).action(async (options) => {
3308
3586
  await login(options.host);
3309
3587
  });
3310
- authCmd.command("status").description("Check current authentication status.").action(async () => {
3311
- await status();
3588
+ authCmd.command("status").description("Check current authentication status.").option("--host <host>", "Check authentication for this workspace host.").action(async (options) => {
3589
+ await status(options.host);
3312
3590
  });
3313
- authCmd.command("logout").description("Clear local authentication configuration.").action(async () => {
3314
- await logout();
3591
+ authCmd.command("logout").description("Revoke and remove a workspace host credential.").option("--host <host>", "Log out from this workspace host.").action(async (options) => {
3592
+ await logout(options.host);
3315
3593
  });
3316
3594
  program.showHelpAfterError();
3317
3595
  return program;