@sudajs/cli 0.15.0 → 0.17.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
@@ -610,9 +886,14 @@ if (!ThemeRuntime) {
610
886
  throw new Error("Missing host theme runtime (__SUDA_THEME_RUNTIME__).");
611
887
  }
612
888
  export const {
889
+ SudaIcon,
613
890
  SudaLucideIcon,
891
+ getSudaSimpleIconSlug,
892
+ isSudaSimpleIconName,
893
+ normalizeSudaIconName,
614
894
  isSudaLucideIconName,
615
895
  normalizeSudaLucideIconName,
896
+ toSudaSimpleIconName,
616
897
  colorSchemeField,
617
898
  createColorSchemeDefault,
618
899
  createThemeCssVariables,
@@ -673,9 +954,14 @@ if (!ThemeIcons) {
673
954
  throw new Error("Missing host theme icon runtime (__SUDA_THEME_ICONS__).");
674
955
  }
675
956
  export const {
957
+ SudaIcon,
676
958
  SudaLucideIcon,
959
+ getSudaSimpleIconSlug,
960
+ isSudaSimpleIconName,
961
+ normalizeSudaIconName,
677
962
  isSudaLucideIconName,
678
963
  normalizeSudaLucideIconName,
964
+ toSudaSimpleIconName,
679
965
  } = ThemeIcons;
680
966
  `
681
967
  }));
@@ -732,16 +1018,10 @@ async function readJsonIfExists(filePath) {
732
1018
  }
733
1019
  return readJson(filePath);
734
1020
  }
735
- function protocolForHost2(host) {
736
- return host.includes("localhost") || host.includes("127.0.0.1") ? "http" : "https";
737
- }
738
1021
  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
- }
1022
+ const config = await requireAuthConfig();
743
1023
  return {
744
- baseUrl: `${protocolForHost2(config.host)}://${config.host}`,
1024
+ baseUrl: config.baseUrl,
745
1025
  token: config.sessionToken
746
1026
  };
747
1027
  }
@@ -2881,7 +3161,7 @@ ${lines.join("\n")}`;
2881
3161
  }
2882
3162
  return message;
2883
3163
  }
2884
- async function publishTheme(root, skipBuild, force) {
3164
+ async function publishTheme(root, skipBuild, force, host) {
2885
3165
  const theme = skipBuild ? await finalizeTheme(root) : await buildTheme(root);
2886
3166
  const ok = await runThemeCheck(theme);
2887
3167
  if (!ok) {
@@ -2899,13 +3179,11 @@ async function publishTheme(root, skipBuild, force) {
2899
3179
  );
2900
3180
  }
2901
3181
  }
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}`;
3182
+ const config = await selectAuthConfig(host, "Select a host to publish to:");
3183
+ const baseUrl = config.baseUrl;
2907
3184
  const { key, version } = theme.module.manifest;
2908
3185
  const digest = await checksum(files);
3186
+ console.log(info(`publishing ${themeRef(key, version)} to ${style.url(baseUrl)}`));
2909
3187
  if (force) {
2910
3188
  const forceRes = await fetch(`${baseUrl}/api/cli/themes/publish-force`, {
2911
3189
  method: "POST",
@@ -2988,7 +3266,7 @@ async function publishTheme(root, skipBuild, force) {
2988
3266
  if (!completeRes.ok) {
2989
3267
  throw new Error(`Failed to complete publish: ${await formatHttpErrorBody(completeRes)}`);
2990
3268
  }
2991
- console.log(success(`published ${themeRef(key, version)}`));
3269
+ console.log(success(`published ${themeRef(key, version)} to ${style.url(baseUrl)}`));
2992
3270
  }
2993
3271
  var cachedCliPackageVersions;
2994
3272
  async function readCliPackageVersions() {
@@ -3226,14 +3504,15 @@ function buildProgram() {
3226
3504
  ).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
3505
  await screenshotTheme(resolveThemeRoot(options), options);
3228
3506
  });
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(
3507
+ 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
3508
  "--force",
3231
3509
  "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
3510
  ).action(async (options) => {
3233
3511
  await publishTheme(
3234
3512
  resolveThemeRoot(options),
3235
3513
  options.skipBuild === true,
3236
- options.force === true
3514
+ options.force === true,
3515
+ options.host
3237
3516
  );
3238
3517
  });
3239
3518
  const agent = program.command("agent").description("Agent-friendly theme and page tooling.");
@@ -3299,19 +3578,28 @@ function buildProgram() {
3299
3578
  program.command("mcp").description("Run the Suda local MCP server over stdio.").action(async () => {
3300
3579
  await startMcpServer();
3301
3580
  });
3581
+ const hostCmd = program.command("host").description("Manage the current SudaCloud workspace host.");
3582
+ hostCmd.command("current").description("Show the current authenticated workspace host.").action(async () => {
3583
+ await showCurrentHost();
3584
+ });
3585
+ hostCmd.command("list").description("List authenticated workspace hosts.").action(async () => {
3586
+ await listHosts();
3587
+ });
3588
+ hostCmd.command("switch").description("Switch the current workspace host.").argument("<host>", "An existing authenticated SudaCloud workspace host.").action(async (host) => {
3589
+ await switchHost(host);
3590
+ });
3302
3591
  const authCmd = program.command("auth").description("Manage Suda authentication.");
3303
3592
  authCmd.command("login").description("Authenticate Suda CLI with a SudaCloud workspace.").option(
3304
3593
  "--host <host>",
3305
- "The SudaCloud workspace host to authenticate against.",
3306
- "app.sudayun.cn"
3594
+ "The SudaCloud workspace host to authenticate against."
3307
3595
  ).action(async (options) => {
3308
3596
  await login(options.host);
3309
3597
  });
3310
- authCmd.command("status").description("Check current authentication status.").action(async () => {
3311
- await status();
3598
+ authCmd.command("status").description("Check current authentication status.").option("--host <host>", "Check authentication for this workspace host.").action(async (options) => {
3599
+ await status(options.host);
3312
3600
  });
3313
- authCmd.command("logout").description("Clear local authentication configuration.").action(async () => {
3314
- await logout();
3601
+ authCmd.command("logout").description("Revoke and remove a workspace host credential.").option("--host <host>", "Log out from this workspace host.").action(async (options) => {
3602
+ await logout(options.host);
3315
3603
  });
3316
3604
  program.showHelpAfterError();
3317
3605
  return program;