rkb-cli 0.3.0-beta.1

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.
@@ -0,0 +1,120 @@
1
+ import OpenApiClient, { Config, OpenApiRequest, Params, } from "@alicloud/openapi-client";
2
+ import { RuntimeOptions } from "@alicloud/tea-util";
3
+ import { CapabilityError } from "../../core/result.js";
4
+ import { getConfigValue } from "../../config/index.js";
5
+ import { runtimeProfile } from "../../runtime/profile.js";
6
+ import { readSandboxStsCredentials, } from "./sandbox-sts.js";
7
+ import { OfficialCliSession } from "../aliyun/official-cli.js";
8
+ // The SDK's CommonJS default export is wrapped once when loaded by native ESM.
9
+ const Client = (typeof OpenApiClient === "function"
10
+ ? OpenApiClient
11
+ : OpenApiClient.default);
12
+ export function record(value) {
13
+ if (!value || typeof value !== "object" || Array.isArray(value))
14
+ throw new CapabilityError("POP_INVALID_RESPONSE", "POP returned an invalid response object.");
15
+ return value;
16
+ }
17
+ // Java DTO fixtures use lowerCamelCase; the POP gateway normally uses PascalCase.
18
+ export function field(value, name) {
19
+ const object = record(value);
20
+ return object[name] ?? object[name[0].toUpperCase() + name.slice(1)];
21
+ }
22
+ export function requiredString(value, name) {
23
+ const result = field(value, name);
24
+ if (typeof result !== "string" || !result.trim())
25
+ throw new CapabilityError("POP_INVALID_RESPONSE", `POP response is missing ${name}.`);
26
+ return result;
27
+ }
28
+ function requestId(value) {
29
+ if (!value || typeof value !== "object" || Array.isArray(value))
30
+ return undefined;
31
+ const raw = field(value, "requestId");
32
+ return typeof raw === "string" ? raw : undefined;
33
+ }
34
+ export class PopTransport {
35
+ deployment;
36
+ timeout;
37
+ credentialProvider;
38
+ execute;
39
+ constructor(deployment, timeout = 30000, execute, credentialProvider = async () => readSandboxStsCredentials() ?? new OfficialCliSession().get()) {
40
+ this.deployment = deployment;
41
+ this.timeout = timeout;
42
+ this.credentialProvider = credentialProvider;
43
+ this.execute = execute;
44
+ }
45
+ async call(action, fields = {}) {
46
+ const deployment = this.deployment;
47
+ if (!deployment?.endpoint || !deployment.apiVersion)
48
+ throw new CapabilityError("POP_NOT_CONFIGURED", "This external build has no POP endpoint/API Version. Configure editions/external.json and rebuild.");
49
+ if (!/^[a-zA-Z0-9.-]+(?::[0-9]+)?$/.test(deployment.endpoint))
50
+ throw new CapabilityError("POP_INVALID_ENDPOINT", "POP endpoint must be a hostname without a URL path.");
51
+ if (!Number.isFinite(this.timeout) || this.timeout <= 0)
52
+ throw new CapabilityError("POP_INVALID_TIMEOUT", "request.timeout must be a positive number.");
53
+ const params = new Params({
54
+ action,
55
+ version: deployment.apiVersion,
56
+ protocol: "HTTPS",
57
+ pathname: "/",
58
+ method: "POST",
59
+ authType: "AK",
60
+ style: "RPC",
61
+ reqBodyType: deployment.requestBodyType,
62
+ bodyType: "json",
63
+ });
64
+ const body = Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
65
+ const credentials = await this.credentialProvider();
66
+ const request = new OpenApiRequest({ body });
67
+ const runtime = new RuntimeOptions({
68
+ readTimeout: this.timeout,
69
+ connectTimeout: this.timeout,
70
+ autoretry: false,
71
+ maxAttempts: 1,
72
+ });
73
+ let response;
74
+ try {
75
+ // Build with the currently valid STS credentials on every call, including pagination.
76
+ // Reusing a client initialized with old credentials would break automatic renewal.
77
+ const execute = this.execute ??
78
+ new Client(new Config({
79
+ endpoint: deployment.endpoint,
80
+ accessKeyId: credentials.accessKeyId,
81
+ accessKeySecret: credentials.accessKeySecret,
82
+ securityToken: credentials.securityToken,
83
+ }));
84
+ response =
85
+ typeof execute === "function"
86
+ ? await execute(params, request, runtime)
87
+ : await execute.callApi(params, request, runtime);
88
+ }
89
+ catch (error) {
90
+ // SDK messages can include signed request URLs. Expose only stable error metadata.
91
+ const object = error && typeof error === "object"
92
+ ? error
93
+ : {};
94
+ const code = typeof object.code === "string" ? object.code : "POP_REQUEST_FAILED";
95
+ const id = requestId(object.data) ?? requestId(object);
96
+ throw new CapabilityError(code, `POP ${action} failed (${code})${id ? `; RequestId: ${id}` : ""}. Check the deployment, network and external login.`, id);
97
+ }
98
+ const envelope = record(response.body);
99
+ const success = field(envelope, "success");
100
+ const code = field(envelope, "code");
101
+ const status = field(envelope, "httpStatusCode");
102
+ if (success === false ||
103
+ (code !== undefined && String(code) !== "200") ||
104
+ (typeof status === "number" && status >= 400)) {
105
+ const id = requestId(envelope);
106
+ const detail = field(envelope, "message");
107
+ const errorCode = code === undefined ? "POP_REQUEST_FAILED" : String(code);
108
+ throw new CapabilityError(errorCode, `${typeof detail === "string" ? detail : "POP request failed"}${id ? `; RequestId: ${id}` : ""}`, id);
109
+ }
110
+ if (success !== true && String(code) !== "200")
111
+ throw new CapabilityError("POP_INVALID_RESPONSE", "POP response has no success status.");
112
+ const data = field(envelope, "data");
113
+ if (data === undefined || data === null)
114
+ throw new CapabilityError("POP_INVALID_RESPONSE", "POP response has no data.");
115
+ return data;
116
+ }
117
+ }
118
+ export function createTransport() {
119
+ return new PopTransport(runtimeProfile.pop, Number(getConfigValue("request.timeout") ?? 30000));
120
+ }
@@ -0,0 +1,19 @@
1
+ import './runtime/bootstrap.js';
2
+ import Command0 from "./commands/doc/index.js";
3
+ import Command1 from "./commands/doc/publish.js";
4
+ import Command2 from "./features/auth/aliyun/command.js";
5
+ import Command3 from "./features/auth/aliyun/logout-command.js";
6
+ import Command4 from "./commands/space/index.js";
7
+ import Command5 from "./commands/space/list.js";
8
+ import Command6 from "./commands/space/use.js";
9
+ import Command7 from "./commands/whoami.js";
10
+ export default {
11
+ "doc": Command0,
12
+ "doc:publish": Command1,
13
+ "login": Command2,
14
+ "logout": Command3,
15
+ "space": Command4,
16
+ "space:list": Command5,
17
+ "space:use": Command6,
18
+ "whoami": Command7
19
+ };
@@ -0,0 +1,2 @@
1
+ import {Command, Help} from '@oclif/core';
2
+ export default class Topic extends Command { static summary = "Manage doc; available commands are listed below"; async run() { await this.parse(Topic); await new Help(this.config).showHelp(["doc"]); }}
@@ -0,0 +1,31 @@
1
+ import { Command } from "@oclif/core";
2
+ import open from "open";
3
+ import { baseFlags } from "../../lib/command-flags.js";
4
+ import { logger } from "../../lib/logger.js";
5
+ import { getService } from "../../runtime/container.js";
6
+ export default class DocPublish extends Command {
7
+ static summary = "Start a documentation publish pipeline";
8
+ static examples = ["<%= config.bin %> doc publish"];
9
+ static flags = { ...baseFlags };
10
+ async run() {
11
+ const { flags } = await this.parse(DocPublish);
12
+ const result = await getService("docPublish").startPipeline();
13
+ if (!result.success || !result.data) {
14
+ if (flags.json)
15
+ this.log(JSON.stringify(result, null, 2));
16
+ else
17
+ logger.error(result.error ?? "Failed to start publish pipeline");
18
+ this.exit(1);
19
+ }
20
+ if (flags.json) {
21
+ this.log(JSON.stringify(result, null, 2));
22
+ return;
23
+ }
24
+ logger.success("Publish pipeline started!");
25
+ this.log(`Pipeline ID: ${result.data.pipelineId}`);
26
+ if (result.data.pipelineUrl) {
27
+ this.log(`Pipeline URL: ${result.data.pipelineUrl}`);
28
+ await open(result.data.pipelineUrl);
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,2 @@
1
+ import {Command, Help} from '@oclif/core';
2
+ export default class Topic extends Command { static summary = "Manage space; available commands are listed below"; async run() { await this.parse(Topic); await new Help(this.config).showHelp(["space"]); }}
@@ -0,0 +1,40 @@
1
+ import { runtimeProfile } from "../../runtime/profile.js";
2
+ import { Command } from "@oclif/core";
3
+ import { baseFlags } from "../../lib/command-flags.js";
4
+ import { logger } from "../../lib/logger.js";
5
+ import { getService } from "../../runtime/container.js";
6
+ export default class SpaceList extends Command {
7
+ static summary = runtimeProfile.edition === "internal"
8
+ ? "List spaces accessible to the current API key"
9
+ : "List spaces accessible to the current credentials";
10
+ static examples = [
11
+ "<%= config.bin %> space list",
12
+ "<%= config.bin %> space list --json",
13
+ ];
14
+ static flags = { ...baseFlags };
15
+ async run() {
16
+ const { flags } = await this.parse(SpaceList);
17
+ const result = await getService("spaceRead").list();
18
+ if (!result.success || !result.data) {
19
+ if (flags.json)
20
+ this.log(JSON.stringify(result, null, 2));
21
+ else
22
+ logger.error(result.error ?? "Failed to list spaces");
23
+ this.exit(1);
24
+ }
25
+ if (flags.json) {
26
+ this.log(JSON.stringify(result.data, null, 2));
27
+ return;
28
+ }
29
+ if (result.data.spaces.length === 0) {
30
+ logger.info("No accessible spaces found");
31
+ return;
32
+ }
33
+ const codeWidth = Math.max(4, ...result.data.spaces.map((space) => space.code.length));
34
+ this.log(` ${"CODE".padEnd(codeWidth)} NAME`);
35
+ for (const space of result.data.spaces) {
36
+ const current = space.code === result.data.currentSpaceCode ? "*" : " ";
37
+ this.log(`${current} ${space.code.padEnd(codeWidth)} ${space.name}`);
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,29 @@
1
+ import { Args, Command } from "@oclif/core";
2
+ import { baseFlags } from "../../lib/command-flags.js";
3
+ import { logger } from "../../lib/logger.js";
4
+ import { getService } from "../../runtime/container.js";
5
+ export default class SpaceUse extends Command {
6
+ static summary = "Switch the current rkb space";
7
+ static examples = ["<%= config.bin %> space use docs"];
8
+ static args = {
9
+ spaceCode: Args.string({ description: "Space code", required: true }),
10
+ };
11
+ static flags = { ...baseFlags };
12
+ async run() {
13
+ const { args, flags } = await this.parse(SpaceUse);
14
+ const result = await getService("spaceRead").use(args.spaceCode);
15
+ if (flags.json) {
16
+ this.log(JSON.stringify(result, null, 2));
17
+ }
18
+ else if (result.success) {
19
+ logger.success(result.message ?? `Switched to space: ${args.spaceCode}`);
20
+ if (flags.verbose && result.data)
21
+ logger.info(`Name: ${result.data.space.name}`);
22
+ }
23
+ else {
24
+ logger.error(result.error ?? "Failed to switch space");
25
+ }
26
+ if (!result.success)
27
+ this.exit(1);
28
+ }
29
+ }
@@ -0,0 +1,34 @@
1
+ import { Command } from "@oclif/core";
2
+ import { baseFlags } from "../lib/command-flags.js";
3
+ import { getService } from "../runtime/container.js";
4
+ export default class Whoami extends Command {
5
+ static summary = "Show current logged in user";
6
+ static description = "Validate the current credentials and show the logged-in user.";
7
+ static flags = {
8
+ ...baseFlags,
9
+ };
10
+ async run() {
11
+ const { flags } = await this.parse(Whoami);
12
+ const result = await getService("identity").whoami();
13
+ if (!result.success) {
14
+ if (flags.json) {
15
+ this.log(JSON.stringify({ success: false, error: result.error, code: result.code }, null, 2));
16
+ }
17
+ else {
18
+ this.logToStderr(result.error);
19
+ }
20
+ this.exit(1);
21
+ }
22
+ if (flags.json) {
23
+ this.log(JSON.stringify({ success: true, user: result.data }, null, 2));
24
+ return;
25
+ }
26
+ this.log(result.data.name);
27
+ if (flags.verbose) {
28
+ if (result.data.email)
29
+ this.logToStderr(`Email: ${result.data.email}`);
30
+ if (result.data.empId)
31
+ this.logToStderr(`Employee ID: ${result.data.empId}`);
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,157 @@
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { parse, stringify } from "yaml";
5
+ import { runtimeProfile } from "../runtime/profile.js";
6
+ export function getConfigDir() {
7
+ return (process.env[runtimeProfile.configDirEnv] ||
8
+ join(homedir(), runtimeProfile.configDirName));
9
+ }
10
+ export function getConfigFile() {
11
+ return join(getConfigDir(), "config.yaml");
12
+ }
13
+ export function getSchemaItems() {
14
+ return runtimeProfile.schema.map((item) => ({
15
+ ...item,
16
+ enum: item.enum ? [...item.enum] : undefined,
17
+ }));
18
+ }
19
+ export function configEnvKeyFor(key) {
20
+ return `${runtimeProfile.envPrefix}${key.toUpperCase().replace(/\./g, "_")}`;
21
+ }
22
+ export function isConfigFromEnv(key) {
23
+ return process.env[configEnvKeyFor(key)] !== undefined;
24
+ }
25
+ export function ensureConfigDir() {
26
+ const configDir = getConfigDir();
27
+ mkdirSync(configDir, { recursive: true, mode: 0o700 });
28
+ try {
29
+ chmodSync(configDir, 0o700);
30
+ }
31
+ catch {
32
+ // Some filesystems do not support POSIX permissions.
33
+ }
34
+ }
35
+ export function readConfigRaw() {
36
+ const configFile = getConfigFile();
37
+ if (!existsSync(configFile))
38
+ return {};
39
+ try {
40
+ const value = parse(readFileSync(configFile, "utf8"));
41
+ return isRecord(value) ? value : {};
42
+ }
43
+ catch (error) {
44
+ throw new Error(`Failed to read config file ${configFile}: ${errorMessage(error)}`);
45
+ }
46
+ }
47
+ export function readConfig() {
48
+ const config = {};
49
+ for (const item of runtimeProfile.schema) {
50
+ setNestedValue(config, item.key, getConfigValue(item.key));
51
+ }
52
+ return config;
53
+ }
54
+ export function getConfigValue(key) {
55
+ const schemaItem = runtimeProfile.schema.find((item) => item.key === key);
56
+ const envValue = process.env[configEnvKeyFor(key)];
57
+ if (envValue !== undefined) {
58
+ return coerceValue(key, envValue, schemaItem?.type ?? "string");
59
+ }
60
+ const fileValue = getNestedValue(readConfigRaw(), key);
61
+ if (fileValue !== undefined)
62
+ return fileValue;
63
+ return schemaItem?.default;
64
+ }
65
+ export function setConfigValue(key, value) {
66
+ const config = readConfigRaw();
67
+ setNestedValue(config, key, value);
68
+ writeConfig(config);
69
+ }
70
+ export function deleteConfigValue(key) {
71
+ const config = readConfigRaw();
72
+ const deleted = deleteNestedValue(config, key);
73
+ if (deleted)
74
+ writeConfig(config);
75
+ return deleted;
76
+ }
77
+ export function writeConfig(config) {
78
+ ensureConfigDir();
79
+ const configFile = getConfigFile();
80
+ const temporaryFile = `${configFile}.${process.pid}.tmp`;
81
+ writeFileSync(temporaryFile, stringify(config), {
82
+ encoding: "utf8",
83
+ mode: 0o600,
84
+ });
85
+ renameSync(temporaryFile, configFile);
86
+ try {
87
+ chmodSync(configFile, 0o600);
88
+ }
89
+ catch {
90
+ // Some filesystems do not support POSIX permissions.
91
+ }
92
+ }
93
+ export function flattenConfig(object, prefix = "") {
94
+ const result = {};
95
+ for (const [key, value] of Object.entries(object)) {
96
+ const path = prefix ? `${prefix}.${key}` : key;
97
+ if (isRecord(value)) {
98
+ Object.assign(result, flattenConfig(value, path));
99
+ }
100
+ else {
101
+ result[path] = value;
102
+ }
103
+ }
104
+ return result;
105
+ }
106
+ export function setNestedValue(object, key, value) {
107
+ const segments = key.split(".");
108
+ let current = object;
109
+ for (const segment of segments.slice(0, -1)) {
110
+ if (!isRecord(current[segment]))
111
+ current[segment] = {};
112
+ current = current[segment];
113
+ }
114
+ current[segments.at(-1)] = value;
115
+ }
116
+ function coerceValue(key, value, type) {
117
+ if (type === "number") {
118
+ const parsed = Number(value);
119
+ if (!Number.isFinite(parsed)) {
120
+ throw new Error(`${configEnvKeyFor(key)} must be a number`);
121
+ }
122
+ return parsed;
123
+ }
124
+ if (type === "boolean") {
125
+ if (["true", "1"].includes(value.toLowerCase()))
126
+ return true;
127
+ if (["false", "0"].includes(value.toLowerCase()))
128
+ return false;
129
+ throw new Error(`${configEnvKeyFor(key)} must be true, false, 1, or 0`);
130
+ }
131
+ return value;
132
+ }
133
+ function getNestedValue(object, key) {
134
+ return key.split(".").reduce((value, segment) => {
135
+ return isRecord(value) ? value[segment] : undefined;
136
+ }, object);
137
+ }
138
+ function deleteNestedValue(object, key) {
139
+ const segments = key.split(".");
140
+ let current = object;
141
+ for (const segment of segments.slice(0, -1)) {
142
+ const next = current[segment];
143
+ if (!isRecord(next))
144
+ return false;
145
+ current = next;
146
+ }
147
+ const leaf = segments.at(-1);
148
+ if (!Object.prototype.hasOwnProperty.call(current, leaf))
149
+ return false;
150
+ return delete current[leaf];
151
+ }
152
+ function isRecord(value) {
153
+ return typeof value === "object" && value !== null && !Array.isArray(value);
154
+ }
155
+ function errorMessage(error) {
156
+ return error instanceof Error ? error.message : String(error);
157
+ }
@@ -0,0 +1,12 @@
1
+ import { configEnvKeyFor, getConfigValue, isConfigFromEnv, setConfigValue, } from "../config/index.js";
2
+ import { runtimeProfile } from "../runtime/profile.js";
3
+ export function resourceContext() {
4
+ return {
5
+ bin: runtimeProfile.bin,
6
+ credentialLabel: runtimeProfile.edition === "internal" ? "API key" : "credentials",
7
+ getSpace: () => String(getConfigValue(runtimeProfile.spaceKey) ?? "").trim(),
8
+ setSpace: (code) => setConfigValue(runtimeProfile.spaceKey, code),
9
+ spaceEnvKey: configEnvKeyFor(runtimeProfile.spaceKey),
10
+ isSpaceFromEnv: () => isConfigFromEnv(runtimeProfile.spaceKey),
11
+ };
12
+ }
@@ -0,0 +1,17 @@
1
+ export class CapabilityError extends Error {
2
+ code;
3
+ requestId;
4
+ constructor(code, message, requestId) {
5
+ super(message);
6
+ this.code = code;
7
+ this.requestId = requestId;
8
+ this.name = "CapabilityError";
9
+ }
10
+ }
11
+ export function failure(prefix, error) {
12
+ return {
13
+ success: false,
14
+ error: `${prefix}: ${error instanceof Error ? error.message : String(error)}`,
15
+ ...(error instanceof CapabilityError ? { code: error.code } : {}),
16
+ };
17
+ }
@@ -0,0 +1,78 @@
1
+ import { Command, Flags } from "@oclif/core";
2
+ import { baseFlags } from "../../../lib/command-flags.js";
3
+ import { CapabilityError } from "../../../core/result.js";
4
+ import { createAliyunLoginService } from "./factory.js";
5
+ export default class AliyunLogin extends Command {
6
+ static summary = "Sign in with your Alibaba Cloud identity";
7
+ static description = `Use the official Alibaba Cloud CLI OAuth login (PKCE) and obtain temporary STS credentials.
8
+ Requires aliyun CLI 3.3.0 or later. No manual AccessKey configuration is needed.
9
+ The official-cli application must be authorized for your account. Credentials are stored in ~/.rkb-external/aliyun/config.json (owner access only).
10
+ External POP commands use AccessKey signatures and Alibaba Cloud RAM permissions.`;
11
+ static examples = [
12
+ "<%= config.bin %> login",
13
+ "<%= config.bin %> login --force",
14
+ "<%= config.bin %> login --json",
15
+ ];
16
+ static flags = {
17
+ ...baseFlags,
18
+ force: Flags.boolean({
19
+ description: "Authorize again instead of reusing the saved login",
20
+ default: false,
21
+ }),
22
+ timeout: Flags.integer({
23
+ description: "Login timeout in seconds (1-600)",
24
+ default: 180,
25
+ }),
26
+ };
27
+ async run() {
28
+ const { flags } = await this.parse(AliyunLogin);
29
+ const controller = new AbortController();
30
+ const cancel = () => controller.abort(new CapabilityError("OIDC_CANCELLED", "Login was cancelled."));
31
+ let timer;
32
+ let failure;
33
+ try {
34
+ if (flags.timeout < 1 || flags.timeout > 600)
35
+ throw new CapabilityError("OIDC_INVALID_TIMEOUT", "Login timeout must be between 1 and 600 seconds.");
36
+ timer = setTimeout(() => controller.abort(new CapabilityError("OIDC_TIMEOUT", "Login timed out. Run rkb login to try again.")), flags.timeout * 1000);
37
+ process.once("SIGINT", cancel);
38
+ process.once("SIGTERM", cancel);
39
+ const result = await createAliyunLoginService().login({
40
+ force: flags.force,
41
+ signal: controller.signal,
42
+ onAuthorization: (url) => {
43
+ this.logToStderr("Complete Alibaba Cloud sign-in on this computer. If needed, open this URL manually:");
44
+ // Deliberately bypass file logging; never persist authorization URLs or codes.
45
+ this.logToStderr(url);
46
+ },
47
+ });
48
+ if (flags.json)
49
+ this.log(JSON.stringify({ success: true, provider: "aliyun", ...result }, null, 2));
50
+ else {
51
+ // JSON escaping prevents identity display names from injecting terminal control sequences.
52
+ this.log(`Alibaba Cloud identity verified: ${JSON.stringify(result.identity.name)}`);
53
+ this.log(`Account ID: ${JSON.stringify(result.identity.accountId)}; User ID: ${JSON.stringify(result.identity.userId)}`);
54
+ this.log("Official OAuth login saved. External POP commands use temporary STS credentials; API access depends on RAM permissions.");
55
+ if (flags.verbose)
56
+ this.log(`STS credential expiry: ${result.expiresAt}; Saved login reused: ${result.reused}`);
57
+ }
58
+ }
59
+ catch (error) {
60
+ failure =
61
+ error instanceof CapabilityError
62
+ ? error
63
+ : new CapabilityError("OIDC_LOGIN_FAILED", "Alibaba Cloud login failed. Try again; no credentials are included in this error.");
64
+ }
65
+ finally {
66
+ clearTimeout(timer);
67
+ process.removeListener("SIGINT", cancel);
68
+ process.removeListener("SIGTERM", cancel);
69
+ }
70
+ if (failure) {
71
+ if (flags.json)
72
+ this.log(JSON.stringify({ success: false, code: failure.code, error: failure.message }, null, 2));
73
+ else
74
+ this.logToStderr(`${failure.code}: ${failure.message}`);
75
+ this.exit(1);
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,3 @@
1
+ import { OfficialCliSession } from "../../../adapters/aliyun/official-cli.js";
2
+ export const createAliyunLoginService = () => new OfficialCliSession();
3
+ export const createAliyunLogoutService = () => new OfficialCliSession();
@@ -0,0 +1,69 @@
1
+ import { Command, Flags } from "@oclif/core";
2
+ import { baseFlags } from "../../../lib/command-flags.js";
3
+ import { CapabilityError } from "../../../core/result.js";
4
+ import { createAliyunLogoutService } from "./factory.js";
5
+ export default class AliyunLogout extends Command {
6
+ static summary = "Sign out of RKB's Alibaba Cloud identity session";
7
+ static description = `Revoke the saved Alibaba Cloud refresh token, then remove RKB's isolated official CLI credential file.
8
+ If revocation fails, the saved login is retained for retry. Use --local to remove only local credentials, including a malformed record, without contacting Alibaba Cloud.
9
+ This does not sign out the browser's Alibaba Cloud account or change other aliyun profiles. To change accounts, switch the account in your browser before running login again.`;
10
+ static examples = [
11
+ "<%= config.bin %> logout",
12
+ "<%= config.bin %> logout --json",
13
+ "<%= config.bin %> logout --local",
14
+ ];
15
+ static flags = {
16
+ ...baseFlags,
17
+ local: Flags.boolean({
18
+ description: "Remove only the local login without revoking Alibaba Cloud tokens",
19
+ default: false,
20
+ }),
21
+ };
22
+ async run() {
23
+ const { flags } = await this.parse(AliyunLogout);
24
+ const controller = new AbortController();
25
+ const cancel = () => controller.abort(new CapabilityError("OIDC_CANCELLED", "Logout was cancelled. Run logout again to complete cleanup."));
26
+ const timer = setTimeout(() => controller.abort(new CapabilityError("OIDC_TIMEOUT", "Logout timed out. Run logout again to complete cleanup.")), 30000);
27
+ process.once("SIGINT", cancel);
28
+ process.once("SIGTERM", cancel);
29
+ let failure;
30
+ try {
31
+ const result = await createAliyunLogoutService().logout({
32
+ local: flags.local,
33
+ signal: controller.signal,
34
+ });
35
+ if (flags.json)
36
+ this.log(JSON.stringify({ success: true, provider: "aliyun", ...result }, null, 2));
37
+ else {
38
+ this.log(result.cleared
39
+ ? "RKB's saved Alibaba Cloud login was removed."
40
+ : "No saved RKB login remains on this computer.");
41
+ if (result.revocation === "confirmed")
42
+ this.log("Alibaba Cloud refresh token revocation confirmed.");
43
+ else if (result.revocation === "skipped")
44
+ this.log("Local cleanup only; remote token revocation was skipped.");
45
+ else if (result.revocation === "no_refresh_token")
46
+ this.log("No refresh token was saved; only local credentials were removed.");
47
+ this.log("Your browser's Alibaba Cloud sign-in remains active. Switch accounts there before logging in as another user.");
48
+ }
49
+ }
50
+ catch (error) {
51
+ failure =
52
+ error instanceof CapabilityError
53
+ ? error
54
+ : new CapabilityError("OIDC_LOGOUT_FAILED", "Logout could not be completed. Retry logout; no credentials are included in this error.");
55
+ }
56
+ finally {
57
+ clearTimeout(timer);
58
+ process.removeListener("SIGINT", cancel);
59
+ process.removeListener("SIGTERM", cancel);
60
+ }
61
+ if (failure) {
62
+ if (flags.json)
63
+ this.log(JSON.stringify({ success: false, code: failure.code, error: failure.message }, null, 2));
64
+ else
65
+ this.logToStderr(`${failure.code}: ${failure.message}`);
66
+ this.exit(1);
67
+ }
68
+ }
69
+ }