@sudajs/cli 0.14.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
|
-
|
|
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
|
|
52
|
-
|
|
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
|
|
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(
|
|
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
|
|
65
|
-
const configPath = getConfigPath();
|
|
205
|
+
async function removeConfigFile() {
|
|
66
206
|
try {
|
|
67
|
-
await fs.unlink(
|
|
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
|
|
75
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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`
|
|
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
|
|
327
|
+
await removeCredential(config.host);
|
|
101
328
|
throw new CliAuthExpiredError(
|
|
102
329
|
code,
|
|
103
|
-
body.message ??
|
|
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
|
-
|
|
109
|
-
|
|
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
|
|
145
|
-
console.log(success(
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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(
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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(
|
|
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.
|
|
437
|
+
`Could not contact ${config.host} to revoke token. Its credential will be cleared locally only.`
|
|
198
438
|
)
|
|
199
439
|
);
|
|
200
440
|
}
|
|
201
|
-
await
|
|
202
|
-
console.log(success(
|
|
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
|
|
@@ -580,85 +856,105 @@ export const { createRoot, hydrateRoot, version } = ReactDOMClient;
|
|
|
580
856
|
}
|
|
581
857
|
};
|
|
582
858
|
}
|
|
583
|
-
function
|
|
859
|
+
function createRejectEditorRuntimeDependencyPlugin() {
|
|
584
860
|
return {
|
|
585
|
-
name: "suda-
|
|
861
|
+
name: "suda-reject-editor-runtime-dependency",
|
|
586
862
|
setup(build) {
|
|
587
|
-
build.onResolve({ filter: /^@puckeditor\/core
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
contents: `
|
|
594
|
-
const PuckCore = globalThis.__SUDA_PUCK_CORE__;
|
|
595
|
-
if (!PuckCore) {
|
|
596
|
-
throw new Error("Missing host Puck core runtime (__SUDA_PUCK_CORE__).");
|
|
597
|
-
}
|
|
598
|
-
export default PuckCore;
|
|
599
|
-
export const {
|
|
600
|
-
Action,
|
|
601
|
-
ActionBar,
|
|
602
|
-
AutoField,
|
|
603
|
-
Button,
|
|
604
|
-
Drawer,
|
|
605
|
-
DropZone,
|
|
606
|
-
FieldLabel,
|
|
607
|
-
Group,
|
|
608
|
-
IconButton,
|
|
609
|
-
Label,
|
|
610
|
-
Puck,
|
|
611
|
-
Render,
|
|
612
|
-
RichTextMenu,
|
|
613
|
-
Separator,
|
|
614
|
-
blocksPlugin,
|
|
615
|
-
createUsePuck,
|
|
616
|
-
fieldsPlugin,
|
|
617
|
-
legacySideBarPlugin,
|
|
618
|
-
migrate,
|
|
619
|
-
outlinePlugin,
|
|
620
|
-
overrideKeys,
|
|
621
|
-
registerOverlayPortal,
|
|
622
|
-
renderContext,
|
|
623
|
-
resolveAllData,
|
|
624
|
-
setDeep,
|
|
625
|
-
transformProps,
|
|
626
|
-
useGetPuck,
|
|
627
|
-
usePuck,
|
|
628
|
-
walkTree,
|
|
629
|
-
} = PuckCore;
|
|
630
|
-
`
|
|
863
|
+
build.onResolve({ filter: /^@puckeditor\/(?:core|plugin-ai)(?:\/.*)?$/ }, (args) => ({
|
|
864
|
+
errors: [
|
|
865
|
+
{
|
|
866
|
+
text: `${args.path} is an editor dependency and cannot be imported by the public theme client runtime.`
|
|
867
|
+
}
|
|
868
|
+
]
|
|
631
869
|
}));
|
|
632
870
|
}
|
|
633
871
|
};
|
|
634
872
|
}
|
|
635
|
-
function
|
|
873
|
+
function createHostThemeRuntimeShimPlugin() {
|
|
636
874
|
return {
|
|
637
|
-
name: "suda-theme-
|
|
875
|
+
name: "suda-host-theme-runtime-shim",
|
|
638
876
|
setup(build) {
|
|
639
|
-
const themeRequire = createRequire(path2.join(root, "package.json"));
|
|
640
|
-
const runtimeEntry = themeRequire.resolve("@sudajs/theme-engine/runtime");
|
|
641
877
|
build.onResolve({ filter: /^@sudajs\/theme-engine\/runtime$/ }, () => ({
|
|
642
|
-
path:
|
|
878
|
+
path: "theme-runtime-host-shim",
|
|
879
|
+
namespace: "suda-shim"
|
|
880
|
+
}));
|
|
881
|
+
build.onLoad({ filter: /^theme-runtime-host-shim$/, namespace: "suda-shim" }, () => ({
|
|
882
|
+
loader: "js",
|
|
883
|
+
contents: `
|
|
884
|
+
const ThemeRuntime = globalThis.__SUDA_THEME_RUNTIME__;
|
|
885
|
+
if (!ThemeRuntime) {
|
|
886
|
+
throw new Error("Missing host theme runtime (__SUDA_THEME_RUNTIME__).");
|
|
887
|
+
}
|
|
888
|
+
export const {
|
|
889
|
+
SudaLucideIcon,
|
|
890
|
+
isSudaLucideIconName,
|
|
891
|
+
normalizeSudaLucideIconName,
|
|
892
|
+
colorSchemeField,
|
|
893
|
+
createColorSchemeDefault,
|
|
894
|
+
createThemeCssVariables,
|
|
895
|
+
designSystemField,
|
|
896
|
+
resolveColorSchemeTokens,
|
|
897
|
+
SUDA_COLOR_TOKEN_KEYS,
|
|
898
|
+
createThemeDesignCssVariables,
|
|
899
|
+
createThemeDesignDefault,
|
|
900
|
+
resolveThemeDesignTokens,
|
|
901
|
+
SUDA_CUSTOM_DESIGN_PRESET_ID,
|
|
902
|
+
SUDA_DESIGN_COLOR_KEYS,
|
|
903
|
+
SUDA_DESIGN_RADIUS_KEYS,
|
|
904
|
+
compileSudaBlockSlots,
|
|
905
|
+
isSudaLocalBlockComponent,
|
|
906
|
+
SUDA_LOCAL_BLOCK_CATEGORY,
|
|
907
|
+
SUDA_LOCAL_BLOCK_TYPE_PREFIX,
|
|
908
|
+
cmsPaginationUrl,
|
|
909
|
+
getIcpRecord,
|
|
910
|
+
getCmsContent,
|
|
911
|
+
getContactForm,
|
|
912
|
+
getPostResource,
|
|
913
|
+
getWhiteLabel,
|
|
914
|
+
resolveAsset,
|
|
915
|
+
postResourceKey,
|
|
916
|
+
getPageSlot,
|
|
917
|
+
THEME_PAGE_SLOT,
|
|
918
|
+
createThemeAsset,
|
|
919
|
+
createThemeAssetPath,
|
|
920
|
+
createThemeAssetResolver,
|
|
921
|
+
resolveThemeClientRuntimeUrl,
|
|
922
|
+
resolveThemeRuntimeUrl,
|
|
923
|
+
resolveAssetPath,
|
|
924
|
+
THEME_CLIENT_RUNTIME_ARTIFACT,
|
|
925
|
+
clearThemeLocale,
|
|
926
|
+
createThemeTranslate,
|
|
927
|
+
installThemeLocale,
|
|
928
|
+
clearThemePreviewLocale,
|
|
929
|
+
createThemePreviewTranslate,
|
|
930
|
+
installThemePreviewLocale,
|
|
931
|
+
} = ThemeRuntime;
|
|
932
|
+
`
|
|
643
933
|
}));
|
|
644
934
|
}
|
|
645
935
|
};
|
|
646
936
|
}
|
|
647
|
-
function
|
|
937
|
+
function createHostThemeIconsShimPlugin() {
|
|
648
938
|
return {
|
|
649
|
-
name: "suda-theme-
|
|
939
|
+
name: "suda-host-theme-icons-shim",
|
|
650
940
|
setup(build) {
|
|
651
|
-
const themeRequire = createRequire(path2.join(root, "package.json"));
|
|
652
|
-
const themeEngineEntry = themeRequire.resolve("@sudajs/theme-engine");
|
|
653
|
-
const entryDir = path2.dirname(themeEngineEntry);
|
|
654
|
-
const packageRoot2 = path2.basename(entryDir) === "dist" || path2.basename(entryDir) === "src" ? path2.dirname(entryDir) : entryDir;
|
|
655
|
-
const sourceEntry = path2.join(packageRoot2, "src", "icons", "lucide-icon.hydrate.client.tsx");
|
|
656
941
|
build.onResolve({ filter: /^@sudajs\/theme-engine\/icons$/ }, () => {
|
|
657
|
-
|
|
658
|
-
return { path: sourceEntry };
|
|
659
|
-
}
|
|
660
|
-
return { path: themeRequire.resolve("@sudajs/theme-engine/icons") };
|
|
942
|
+
return { path: "theme-icons-host-shim", namespace: "suda-shim" };
|
|
661
943
|
});
|
|
944
|
+
build.onLoad({ filter: /^theme-icons-host-shim$/, namespace: "suda-shim" }, () => ({
|
|
945
|
+
loader: "js",
|
|
946
|
+
contents: `
|
|
947
|
+
const ThemeIcons = globalThis.__SUDA_THEME_ICONS__;
|
|
948
|
+
if (!ThemeIcons) {
|
|
949
|
+
throw new Error("Missing host theme icon runtime (__SUDA_THEME_ICONS__).");
|
|
950
|
+
}
|
|
951
|
+
export const {
|
|
952
|
+
SudaLucideIcon,
|
|
953
|
+
isSudaLucideIconName,
|
|
954
|
+
normalizeSudaLucideIconName,
|
|
955
|
+
} = ThemeIcons;
|
|
956
|
+
`
|
|
957
|
+
}));
|
|
662
958
|
}
|
|
663
959
|
};
|
|
664
960
|
}
|
|
@@ -712,16 +1008,10 @@ async function readJsonIfExists(filePath) {
|
|
|
712
1008
|
}
|
|
713
1009
|
return readJson(filePath);
|
|
714
1010
|
}
|
|
715
|
-
function protocolForHost2(host) {
|
|
716
|
-
return host.includes("localhost") || host.includes("127.0.0.1") ? "http" : "https";
|
|
717
|
-
}
|
|
718
1011
|
async function requireCliBaseUrl() {
|
|
719
|
-
const config = await
|
|
720
|
-
if (!config) {
|
|
721
|
-
throw new Error("Not logged in. Run `suda auth login` to authenticate first.");
|
|
722
|
-
}
|
|
1012
|
+
const config = await requireAuthConfig();
|
|
723
1013
|
return {
|
|
724
|
-
baseUrl:
|
|
1014
|
+
baseUrl: config.baseUrl,
|
|
725
1015
|
token: config.sessionToken
|
|
726
1016
|
};
|
|
727
1017
|
}
|
|
@@ -779,9 +1069,9 @@ ${issues}`);
|
|
|
779
1069
|
if (module.manifest.entry !== "index.js") {
|
|
780
1070
|
throw new Error('manifest.entry must be artifact-local: "index.js".');
|
|
781
1071
|
}
|
|
782
|
-
if (
|
|
1072
|
+
if ("clientEntry" in module.manifest) {
|
|
783
1073
|
throw new Error(
|
|
784
|
-
|
|
1074
|
+
"manifest.clientEntry is no longer supported. The CLI owns the runtime.client.js artifact."
|
|
785
1075
|
);
|
|
786
1076
|
}
|
|
787
1077
|
assertRecord(module.pageConfig?.components, "pageConfig.components");
|
|
@@ -1001,18 +1291,6 @@ async function loadThemeVite(root) {
|
|
|
1001
1291
|
}
|
|
1002
1292
|
return await import(pathToFileURL(viteEntry).href);
|
|
1003
1293
|
}
|
|
1004
|
-
async function findClientEntry(root) {
|
|
1005
|
-
const candidates = [
|
|
1006
|
-
path2.join(root, "src", "runtime.client.tsx"),
|
|
1007
|
-
path2.join(root, "src", "runtime.client.ts")
|
|
1008
|
-
];
|
|
1009
|
-
for (const candidate of candidates) {
|
|
1010
|
-
if (await pathExists(candidate)) {
|
|
1011
|
-
return candidate;
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
throw new Error(`Missing runtime entry. Add src/runtime.client.ts(x) in ${root}.`);
|
|
1015
|
-
}
|
|
1016
1294
|
async function findServerEntry(root) {
|
|
1017
1295
|
const candidates = [path2.join(root, "src", "index.tsx"), path2.join(root, "src", "index.ts")];
|
|
1018
1296
|
for (const candidate of candidates) {
|
|
@@ -1113,38 +1391,74 @@ async function copyThemeLocales(root) {
|
|
|
1113
1391
|
await copyDirectory(sourceLocales, path2.join(root, "dist", "locales"));
|
|
1114
1392
|
}
|
|
1115
1393
|
async function buildClientRuntime(root, minify) {
|
|
1116
|
-
const
|
|
1117
|
-
|
|
1118
|
-
|
|
1394
|
+
const legacyEntries = [
|
|
1395
|
+
path2.join(root, "src", "runtime.client.ts"),
|
|
1396
|
+
path2.join(root, "src", "runtime.client.tsx")
|
|
1397
|
+
];
|
|
1398
|
+
if (legacyEntries.some((entry) => existsSync(entry))) {
|
|
1399
|
+
throw new Error(
|
|
1400
|
+
"Theme-authored src/runtime.client.ts(x) is no longer supported. Move optional browser setup to src/client.ts as clientHooks; the CLI owns runtime.client.js."
|
|
1401
|
+
);
|
|
1119
1402
|
}
|
|
1120
1403
|
const packageJson = JSON.parse(await readFile(path2.join(root, "package.json"), "utf8"));
|
|
1121
1404
|
const themeVersion = typeof packageJson.version === "string" ? packageJson.version : "0.0.0";
|
|
1405
|
+
const clientHooksPath = path2.join(root, "src", "client.ts");
|
|
1406
|
+
const generatedEntry = path2.join(root, ".suda-build", "runtime.client.ts");
|
|
1407
|
+
const clientHooksImport = await pathExists(clientHooksPath) ? 'import { clientHooks } from "../src/client.js";' : "const clientHooks = {};";
|
|
1408
|
+
await mkdir(path2.dirname(generatedEntry), { recursive: true });
|
|
1409
|
+
await writeFile(
|
|
1410
|
+
generatedEntry,
|
|
1411
|
+
[
|
|
1412
|
+
'"use client";',
|
|
1413
|
+
'import type { ThemeClientHooks, ThemeClientRuntime, ThemeRenderModule } from "@sudajs/theme-engine/runtime";',
|
|
1414
|
+
'import * as themeConfig from "../src/config.js";',
|
|
1415
|
+
clientHooksImport,
|
|
1416
|
+
"const theme: ThemeRenderModule = {",
|
|
1417
|
+
" pageConfig: themeConfig.pageConfig,",
|
|
1418
|
+
" layoutConfig: themeConfig.layoutConfig,",
|
|
1419
|
+
"};",
|
|
1420
|
+
'if ("rendering" in themeConfig && themeConfig.rendering !== undefined) {',
|
|
1421
|
+
" theme.rendering = themeConfig.rendering;",
|
|
1422
|
+
"}",
|
|
1423
|
+
"const hooks: ThemeClientHooks = clientHooks;",
|
|
1424
|
+
"const runtime: ThemeClientRuntime = { theme };",
|
|
1425
|
+
"if (hooks.initialize !== undefined) {",
|
|
1426
|
+
" runtime.initialize = hooks.initialize;",
|
|
1427
|
+
"}",
|
|
1428
|
+
"export default runtime;",
|
|
1429
|
+
""
|
|
1430
|
+
].join("\n"),
|
|
1431
|
+
"utf8"
|
|
1432
|
+
);
|
|
1122
1433
|
await mkdir(path2.join(root, "dist"), { recursive: true });
|
|
1123
1434
|
const hostReactShimPlugin = createHostReactShimPlugin();
|
|
1124
|
-
const
|
|
1125
|
-
const
|
|
1126
|
-
const
|
|
1435
|
+
const rejectEditorRuntimeDependencyPlugin = createRejectEditorRuntimeDependencyPlugin();
|
|
1436
|
+
const hostThemeRuntimeShimPlugin = createHostThemeRuntimeShimPlugin();
|
|
1437
|
+
const hostThemeIconsShimPlugin = createHostThemeIconsShimPlugin();
|
|
1127
1438
|
const themeDependencyResolverPlugin = createThemeDependencyResolverPlugin(root);
|
|
1128
1439
|
await build({
|
|
1129
1440
|
bundle: true,
|
|
1130
|
-
entryPoints:
|
|
1441
|
+
entryPoints: { "runtime.client": generatedEntry },
|
|
1131
1442
|
external: ["react", "react/*", "react-dom", "react-dom/*"],
|
|
1132
1443
|
format: "esm",
|
|
1133
1444
|
jsx: "automatic",
|
|
1445
|
+
logOverride: { "import-is-undefined": "silent" },
|
|
1134
1446
|
minify,
|
|
1135
|
-
|
|
1447
|
+
outdir: path2.join(root, "dist"),
|
|
1136
1448
|
platform: "browser",
|
|
1137
1449
|
define: {
|
|
1138
1450
|
__SUDA_THEME_VERSION__: JSON.stringify(themeVersion)
|
|
1139
1451
|
},
|
|
1140
1452
|
plugins: [
|
|
1141
1453
|
hostReactShimPlugin,
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1454
|
+
rejectEditorRuntimeDependencyPlugin,
|
|
1455
|
+
hostThemeRuntimeShimPlugin,
|
|
1456
|
+
hostThemeIconsShimPlugin,
|
|
1145
1457
|
themeDependencyResolverPlugin
|
|
1146
1458
|
],
|
|
1147
1459
|
sourcemap: false,
|
|
1460
|
+
splitting: true,
|
|
1461
|
+
chunkNames: "chunks/[name]-[hash]",
|
|
1148
1462
|
target: "es2022",
|
|
1149
1463
|
treeShaking: true
|
|
1150
1464
|
});
|
|
@@ -2837,7 +3151,7 @@ ${lines.join("\n")}`;
|
|
|
2837
3151
|
}
|
|
2838
3152
|
return message;
|
|
2839
3153
|
}
|
|
2840
|
-
async function publishTheme(root, skipBuild, force) {
|
|
3154
|
+
async function publishTheme(root, skipBuild, force, host) {
|
|
2841
3155
|
const theme = skipBuild ? await finalizeTheme(root) : await buildTheme(root);
|
|
2842
3156
|
const ok = await runThemeCheck(theme);
|
|
2843
3157
|
if (!ok) {
|
|
@@ -2855,13 +3169,11 @@ async function publishTheme(root, skipBuild, force) {
|
|
|
2855
3169
|
);
|
|
2856
3170
|
}
|
|
2857
3171
|
}
|
|
2858
|
-
const config = await
|
|
2859
|
-
|
|
2860
|
-
throw new Error("Not logged in. Run `suda auth login` to authenticate first.");
|
|
2861
|
-
}
|
|
2862
|
-
const baseUrl = `${protocolForHost2(config.host)}://${config.host}`;
|
|
3172
|
+
const config = await selectAuthConfig(host, "Select a host to publish to:");
|
|
3173
|
+
const baseUrl = config.baseUrl;
|
|
2863
3174
|
const { key, version } = theme.module.manifest;
|
|
2864
3175
|
const digest = await checksum(files);
|
|
3176
|
+
console.log(info(`publishing ${themeRef(key, version)} to ${style.url(baseUrl)}`));
|
|
2865
3177
|
if (force) {
|
|
2866
3178
|
const forceRes = await fetch(`${baseUrl}/api/cli/themes/publish-force`, {
|
|
2867
3179
|
method: "POST",
|
|
@@ -2944,7 +3256,7 @@ async function publishTheme(root, skipBuild, force) {
|
|
|
2944
3256
|
if (!completeRes.ok) {
|
|
2945
3257
|
throw new Error(`Failed to complete publish: ${await formatHttpErrorBody(completeRes)}`);
|
|
2946
3258
|
}
|
|
2947
|
-
console.log(success(`published ${themeRef(key, version)}`));
|
|
3259
|
+
console.log(success(`published ${themeRef(key, version)} to ${style.url(baseUrl)}`));
|
|
2948
3260
|
}
|
|
2949
3261
|
var cachedCliPackageVersions;
|
|
2950
3262
|
async function readCliPackageVersions() {
|
|
@@ -3182,14 +3494,15 @@ function buildProgram() {
|
|
|
3182
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) => {
|
|
3183
3495
|
await screenshotTheme(resolveThemeRoot(options), options);
|
|
3184
3496
|
});
|
|
3185
|
-
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(
|
|
3186
3498
|
"--force",
|
|
3187
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."
|
|
3188
3500
|
).action(async (options) => {
|
|
3189
3501
|
await publishTheme(
|
|
3190
3502
|
resolveThemeRoot(options),
|
|
3191
3503
|
options.skipBuild === true,
|
|
3192
|
-
options.force === true
|
|
3504
|
+
options.force === true,
|
|
3505
|
+
options.host
|
|
3193
3506
|
);
|
|
3194
3507
|
});
|
|
3195
3508
|
const agent = program.command("agent").description("Agent-friendly theme and page tooling.");
|
|
@@ -3255,19 +3568,28 @@ function buildProgram() {
|
|
|
3255
3568
|
program.command("mcp").description("Run the Suda local MCP server over stdio.").action(async () => {
|
|
3256
3569
|
await startMcpServer();
|
|
3257
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
|
+
});
|
|
3258
3581
|
const authCmd = program.command("auth").description("Manage Suda authentication.");
|
|
3259
3582
|
authCmd.command("login").description("Authenticate Suda CLI with a SudaCloud workspace.").option(
|
|
3260
3583
|
"--host <host>",
|
|
3261
|
-
"The SudaCloud workspace host to authenticate against."
|
|
3262
|
-
"app.sudayun.cn"
|
|
3584
|
+
"The SudaCloud workspace host to authenticate against."
|
|
3263
3585
|
).action(async (options) => {
|
|
3264
3586
|
await login(options.host);
|
|
3265
3587
|
});
|
|
3266
|
-
authCmd.command("status").description("Check current authentication status.").action(async () => {
|
|
3267
|
-
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);
|
|
3268
3590
|
});
|
|
3269
|
-
authCmd.command("logout").description("
|
|
3270
|
-
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);
|
|
3271
3593
|
});
|
|
3272
3594
|
program.showHelpAfterError();
|
|
3273
3595
|
return program;
|