@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 +350 -72
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/theme/README.md +17 -0
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
|
|
@@ -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
|
|
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:
|
|
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
|
|
2903
|
-
|
|
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("
|
|
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;
|