rkb-cli 0.3.0-beta.1 → 0.3.0-beta.2
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/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Requires Node.js 20 or later. Run `rkb --help` for usage.
|
|
4
4
|
|
|
5
|
-
Run `rkb login` to authorize the official Alibaba Cloud CLI OAuth application and obtain temporary STS credentials.
|
|
5
|
+
Run `rkb login` to authorize the official Alibaba Cloud CLI OAuth application and obtain temporary STS credentials. RKB reuses an installed `aliyun >= 3.3.0`, or automatically downloads a pinned, SHA-256-verified official CLI on first login. Automatic installation supports macOS/Linux x64 and arm64, and Windows x64, and caches the executable under `~/.rkb-external/tools/aliyun/`. First use requires access to `https://aliyuncli.alicdn.com`. For offline installation, set `RKB_EXTERNAL_ALIYUN_CLI` to a trusted official executable. No manual AK/SK is needed. Credentials are stored in `~/.rkb-external/aliyun/config.json` with owner-only file permissions. Use `--force` to authorize again. Run `rkb logout` to revoke the refresh token and remove this profile, or `logout --local` for local cleanup. POP APIs must enable AccessKey authentication and authorize the caller through RAM.
|
|
6
6
|
|
|
7
7
|
Configuration: `~/.rkb-external/config.yaml`; environment prefix: `RKB_EXTERNAL_ENV_`.
|
|
8
8
|
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { createReadStream } from "node:fs";
|
|
4
|
+
import { chmod, lstat, mkdir, mkdtemp, open, readFile, rename, rm, writeFile, } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { extract } from "tar";
|
|
7
|
+
import { unzipSync } from "fflate";
|
|
8
|
+
import { getConfigDir } from "../../config/index.js";
|
|
9
|
+
import { CapabilityError } from "../../core/result.js";
|
|
10
|
+
export const managedCliVersion = "3.5.0";
|
|
11
|
+
const downloadOrigin = "https://aliyuncli.alicdn.com";
|
|
12
|
+
const maxArchiveBytes = 128 * 1024 * 1024;
|
|
13
|
+
const maxBinaryBytes = 512 * 1024 * 1024;
|
|
14
|
+
// Official release asset digests, pinned with the CLI version:
|
|
15
|
+
// https://github.com/aliyun/aliyun-cli/releases/expanded_assets/v3.5.0
|
|
16
|
+
const artifacts = {
|
|
17
|
+
"darwin-x64": [
|
|
18
|
+
"macosx-3.5.0-amd64.tgz",
|
|
19
|
+
"26fa8c289963c07146f475d7df80e4cebf23c02ff7cdbe4f22045fe5f902be34",
|
|
20
|
+
],
|
|
21
|
+
"darwin-arm64": [
|
|
22
|
+
"macosx-3.5.0-arm64.tgz",
|
|
23
|
+
"6f1b0c380a456849adab800ee1396be58976020c6d9dfa3196dcfc41f496a518",
|
|
24
|
+
],
|
|
25
|
+
"linux-x64": [
|
|
26
|
+
"linux-3.5.0-amd64.tgz",
|
|
27
|
+
"f97640bd11002f26e19c4f0b4725a3695e37b43499268837f35dd938cbe043aa",
|
|
28
|
+
],
|
|
29
|
+
"linux-arm64": [
|
|
30
|
+
"linux-3.5.0-arm64.tgz",
|
|
31
|
+
"912250835e658894aa2ec04eed205292f18f7dc4a99b5ead00fa63d913769418",
|
|
32
|
+
],
|
|
33
|
+
"win32-x64": [
|
|
34
|
+
"windows-3.5.0-amd64.zip",
|
|
35
|
+
"9b1c23a90a5f9cd1c5ff2ccd7e64945272e659e1f66496c507db494f4a271895",
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
export function cliArtifact(platform, arch) {
|
|
39
|
+
const target = `${platform}-${arch}`;
|
|
40
|
+
const asset = artifacts[target];
|
|
41
|
+
if (!asset)
|
|
42
|
+
throw new CapabilityError("ALIYUN_CLI_UNSUPPORTED_PLATFORM", `Automatic Alibaba Cloud CLI installation is not supported on ${target}. Set RKB_EXTERNAL_ALIYUN_CLI to a compatible official CLI executable.`);
|
|
43
|
+
return {
|
|
44
|
+
target,
|
|
45
|
+
filename: `aliyun-cli-${asset[0]}`,
|
|
46
|
+
sha256: asset[1],
|
|
47
|
+
executable: platform === "win32" ? "aliyun.exe" : "aliyun",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export function supportsOAuth(version) {
|
|
51
|
+
const match = version?.trim().match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
52
|
+
return Boolean(match &&
|
|
53
|
+
(Number(match[1]) > 3 || (Number(match[1]) === 3 && Number(match[2]) >= 3)));
|
|
54
|
+
}
|
|
55
|
+
async function sha256(file) {
|
|
56
|
+
const hash = createHash("sha256");
|
|
57
|
+
for await (const chunk of createReadStream(file))
|
|
58
|
+
hash.update(chunk);
|
|
59
|
+
return hash.digest("hex");
|
|
60
|
+
}
|
|
61
|
+
async function cachedBinary(directory, asset) {
|
|
62
|
+
const binary = join(directory, asset.executable);
|
|
63
|
+
try {
|
|
64
|
+
if (!(await lstat(directory)).isDirectory() ||
|
|
65
|
+
!(await lstat(binary)).isFile())
|
|
66
|
+
return undefined;
|
|
67
|
+
const saved = JSON.parse(await readFile(join(directory, "checksum.json"), "utf8"));
|
|
68
|
+
if (saved.archive === asset.sha256 &&
|
|
69
|
+
saved.binary === (await sha256(binary)))
|
|
70
|
+
return binary;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Incomplete or damaged caches are replaced with a verified download.
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
async function download(asset, destination, signal, request) {
|
|
78
|
+
const response = await request(`${downloadOrigin}/${asset.filename}`, {
|
|
79
|
+
redirect: "error",
|
|
80
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(120000)]),
|
|
81
|
+
});
|
|
82
|
+
if (!response.ok || !response.body)
|
|
83
|
+
throw new CapabilityError("ALIYUN_CLI_DOWNLOAD_FAILED", `Official CLI download failed (HTTP ${response.status}). Check access to aliyuncli.alicdn.com and retry rkb login.`);
|
|
84
|
+
const reader = response.body.getReader();
|
|
85
|
+
const file = await open(destination, "wx", 0o600);
|
|
86
|
+
const hash = createHash("sha256");
|
|
87
|
+
let bytes = 0;
|
|
88
|
+
try {
|
|
89
|
+
while (true) {
|
|
90
|
+
signal.throwIfAborted();
|
|
91
|
+
const chunk = await reader.read();
|
|
92
|
+
if (chunk.done)
|
|
93
|
+
break;
|
|
94
|
+
bytes += chunk.value.byteLength;
|
|
95
|
+
if (bytes > maxArchiveBytes)
|
|
96
|
+
throw new Error("Archive too large");
|
|
97
|
+
hash.update(chunk.value);
|
|
98
|
+
await file.writeFile(chunk.value);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
await reader.cancel().catch(() => { });
|
|
103
|
+
await file.close();
|
|
104
|
+
}
|
|
105
|
+
if (hash.digest("hex") !== asset.sha256)
|
|
106
|
+
throw new CapabilityError("ALIYUN_CLI_CHECKSUM_MISMATCH", "The downloaded Alibaba Cloud CLI failed SHA-256 verification. Retry rkb login or set RKB_EXTERNAL_ALIYUN_CLI to a trusted official executable.");
|
|
107
|
+
}
|
|
108
|
+
export async function installManagedCli(asset, cacheRoot, options, request = fetch) {
|
|
109
|
+
const { signal } = options;
|
|
110
|
+
signal.throwIfAborted();
|
|
111
|
+
const parent = join(cacheRoot, managedCliVersion);
|
|
112
|
+
const destination = join(parent, asset.target);
|
|
113
|
+
const cached = await cachedBinary(destination, asset);
|
|
114
|
+
signal.throwIfAborted();
|
|
115
|
+
if (cached)
|
|
116
|
+
return cached;
|
|
117
|
+
let temporary;
|
|
118
|
+
try {
|
|
119
|
+
await mkdir(parent, { recursive: true, mode: 0o700 });
|
|
120
|
+
await rm(destination, { recursive: true, force: true });
|
|
121
|
+
temporary = await mkdtemp(join(parent, `.${asset.target}-`));
|
|
122
|
+
const archive = join(temporary, "download");
|
|
123
|
+
const binary = join(temporary, asset.executable);
|
|
124
|
+
options.onProgress?.(`Downloading Alibaba Cloud CLI ${managedCliVersion} for ${asset.target} (first use only)...`);
|
|
125
|
+
await download(asset, archive, signal, request);
|
|
126
|
+
signal.throwIfAborted();
|
|
127
|
+
if (asset.filename.endsWith(".zip")) {
|
|
128
|
+
const files = unzipSync(await readFile(archive), {
|
|
129
|
+
filter: (entry) => entry.name === asset.executable &&
|
|
130
|
+
entry.originalSize > 0 &&
|
|
131
|
+
entry.originalSize <= maxBinaryBytes,
|
|
132
|
+
});
|
|
133
|
+
if (!files[asset.executable])
|
|
134
|
+
throw new Error("Missing executable");
|
|
135
|
+
await writeFile(binary, files[asset.executable], {
|
|
136
|
+
flag: "wx",
|
|
137
|
+
mode: 0o700,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
await extract({
|
|
142
|
+
file: archive,
|
|
143
|
+
cwd: temporary,
|
|
144
|
+
strict: true,
|
|
145
|
+
filter: (path, entry) => (path === asset.executable || path === `./${asset.executable}`) &&
|
|
146
|
+
"type" in entry &&
|
|
147
|
+
entry.type === "File" &&
|
|
148
|
+
entry.size > 0 &&
|
|
149
|
+
entry.size <= maxBinaryBytes,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (!(await lstat(binary)).isFile())
|
|
153
|
+
throw new Error("Invalid executable");
|
|
154
|
+
await chmod(binary, 0o700);
|
|
155
|
+
await writeFile(join(temporary, "checksum.json"), JSON.stringify({ archive: asset.sha256, binary: await sha256(binary) }), { flag: "wx", mode: 0o600 });
|
|
156
|
+
await rm(archive);
|
|
157
|
+
signal.throwIfAborted();
|
|
158
|
+
try {
|
|
159
|
+
await rename(temporary, destination);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
// A simultaneous login may have completed the same atomic installation.
|
|
163
|
+
if (!(await cachedBinary(destination, asset)))
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
return join(destination, asset.executable);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
signal.throwIfAborted();
|
|
170
|
+
if (error instanceof CapabilityError)
|
|
171
|
+
throw error;
|
|
172
|
+
throw new CapabilityError("ALIYUN_CLI_INSTALL_FAILED", "Could not download or install the official Alibaba Cloud CLI. Check the network and external cache permissions, then retry rkb login. For offline use, set RKB_EXTERNAL_ALIYUN_CLI to a trusted executable.");
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
if (temporary)
|
|
176
|
+
await rm(temporary, { recursive: true, force: true });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function readVersion(executable, signal, env) {
|
|
180
|
+
return new Promise((resolve, reject) => {
|
|
181
|
+
execFile(executable, ["version"], { env, signal, timeout: 5000, maxBuffer: 65536, windowsHide: true }, (error, stdout) => {
|
|
182
|
+
if (signal.aborted)
|
|
183
|
+
reject(signal.reason);
|
|
184
|
+
else
|
|
185
|
+
resolve(error ? undefined : stdout.trim());
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
export async function resolveAliyunCli(options, probe = readVersion, install = installManagedCli) {
|
|
190
|
+
const { signal, env = process.env } = options;
|
|
191
|
+
signal.throwIfAborted();
|
|
192
|
+
const explicit = env.RKB_EXTERNAL_ALIYUN_CLI;
|
|
193
|
+
const version = await probe(explicit || "aliyun", signal, env);
|
|
194
|
+
if (supportsOAuth(version))
|
|
195
|
+
return explicit || "aliyun";
|
|
196
|
+
if (explicit)
|
|
197
|
+
throw new CapabilityError(version ? "ALIYUN_CLI_VERSION" : "ALIYUN_CLI_NOT_FOUND", "RKB_EXTERNAL_ALIYUN_CLI must point to a runnable official Alibaba Cloud CLI 3.3.0 or later.");
|
|
198
|
+
return install(cliArtifact(options.platform ?? process.platform, options.arch ?? process.arch), options.cacheRoot ?? join(getConfigDir(), "tools", "aliyun"), options);
|
|
199
|
+
}
|
|
@@ -6,6 +6,7 @@ import OpenApiClient, { Config, Params, OpenApiRequest, } from "@alicloud/openap
|
|
|
6
6
|
import { RuntimeOptions } from "@alicloud/tea-util";
|
|
7
7
|
import { getConfigDir } from "../../config/index.js";
|
|
8
8
|
import { CapabilityError } from "../../core/result.js";
|
|
9
|
+
import { resolveAliyunCli, supportsOAuth } from "./cli-binary.js";
|
|
9
10
|
const profileName = "rkb-external";
|
|
10
11
|
const oauthEndpoint = "https://oauth.aliyun.com/v1";
|
|
11
12
|
// Used only to refresh tokens issued by the official CLI's own login flow.
|
|
@@ -32,67 +33,71 @@ export function officialCliEnvironment(source) {
|
|
|
32
33
|
return Object.fromEntries(Object.entries(source).filter(([key]) => !/^(ALIBABA_CLOUD|ALIBABACLOUD|ALICLOUD)_(ACCESS_KEY|SECURITY_TOKEN|PROFILE|REGION|ENDPOINT|STS_|BEARER_|OIDC_|ROLE_|CREDENTIALS_URI|EXTERNAL_)/.test(key) &&
|
|
33
34
|
!/^(ACCESS_KEY_ID|ACCESS_KEY_SECRET|SECURITY_TOKEN|REGION_ID|REGION)$/.test(key)));
|
|
34
35
|
}
|
|
35
|
-
export const runOfficialCli = (args, options) =>
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
pending
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
36
|
+
export const runOfficialCli = async (args, options) => {
|
|
37
|
+
const env = officialCliEnvironment(process.env);
|
|
38
|
+
const executable = await resolveAliyunCli({ ...options, env });
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
options.signal.throwIfAborted();
|
|
41
|
+
const child = spawn(executable, args, {
|
|
42
|
+
shell: false,
|
|
43
|
+
env,
|
|
44
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
45
|
+
signal: options.signal,
|
|
46
|
+
killSignal: "SIGKILL",
|
|
47
|
+
});
|
|
48
|
+
let stdout = "", stderr = "", pending = "";
|
|
49
|
+
const seen = new Set();
|
|
50
|
+
const authorizationLines = (chunk, final = false) => {
|
|
51
|
+
pending += chunk;
|
|
52
|
+
const lines = pending.split(/\r?\n/);
|
|
53
|
+
pending = final ? "" : lines.pop() || "";
|
|
54
|
+
for (const line of lines) {
|
|
55
|
+
const url = line.match(/https:\/\/signin\.aliyun\.com\/oauth2\/v1\/auth\?[^\s]+/)?.[0];
|
|
56
|
+
if (url && !seen.has(url)) {
|
|
57
|
+
seen.add(url);
|
|
58
|
+
options.onAuthorization?.(url);
|
|
59
|
+
}
|
|
55
60
|
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
61
|
+
};
|
|
62
|
+
child.stdout.on("data", (chunk) => {
|
|
63
|
+
stdout += chunk.toString();
|
|
64
|
+
authorizationLines(chunk.toString());
|
|
65
|
+
if (stdout.length > 1024 * 1024)
|
|
66
|
+
child.kill("SIGKILL");
|
|
67
|
+
});
|
|
68
|
+
child.stderr.on("data", (chunk) => {
|
|
69
|
+
stderr += chunk.toString();
|
|
70
|
+
if (stderr.length > 1024 * 1024)
|
|
71
|
+
child.kill("SIGKILL");
|
|
72
|
+
});
|
|
73
|
+
child.stdin.on("error", () => {
|
|
74
|
+
/* process errors are handled below */
|
|
75
|
+
});
|
|
76
|
+
// Region and language already have defaults in the isolated profile.
|
|
77
|
+
child.stdin.end("\n\n");
|
|
78
|
+
child.on("error", (error) => reject(options.signal.aborted
|
|
79
|
+
? options.signal.reason
|
|
80
|
+
: new CapabilityError(error.code === "ENOENT"
|
|
81
|
+
? "ALIYUN_CLI_NOT_FOUND"
|
|
82
|
+
: "ALIYUN_CLI_FAILED", error.code === "ENOENT"
|
|
83
|
+
? "The selected Alibaba Cloud CLI is unavailable. Retry rkb login or check RKB_EXTERNAL_ALIYUN_CLI."
|
|
84
|
+
: "The official Alibaba Cloud CLI could not run.")));
|
|
85
|
+
child.on("close", (code) => {
|
|
86
|
+
authorizationLines("", true);
|
|
87
|
+
if (options.signal.aborted) {
|
|
88
|
+
reject(options.signal.reason);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (code !== 0) {
|
|
92
|
+
// CLI stderr may contain tokens and signed URLs; never relay it verbatim.
|
|
93
|
+
const requestId = stderr.match(/request[_ ]?id["\s:=]+([A-Fa-f0-9-]{16,64})/i)?.[1];
|
|
94
|
+
reject(new CapabilityError("ALIYUN_CLI_FAILED", `Official Alibaba Cloud login failed${requestId ? `; RequestId: ${requestId}` : ""}. Check official-cli application authorization and try rkb login --force.`, requestId));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
resolve(stdout);
|
|
98
|
+
});
|
|
94
99
|
});
|
|
95
|
-
}
|
|
100
|
+
};
|
|
96
101
|
async function callerIdentity(credentials) {
|
|
97
102
|
const client = new Client(new Config({ endpoint: "sts.aliyuncs.com", ...credentials }));
|
|
98
103
|
const response = await client.callApi(new Params({
|
|
@@ -250,11 +255,8 @@ export class OfficialCliSession {
|
|
|
250
255
|
const saved = await this.load();
|
|
251
256
|
const reused = !options.force && Boolean(saved?.profile.oauth_access_token);
|
|
252
257
|
if (!reused) {
|
|
253
|
-
const version = await this.run(["version"],
|
|
254
|
-
|
|
255
|
-
if (!match ||
|
|
256
|
-
Number(match[1]) < 3 ||
|
|
257
|
-
(Number(match[1]) === 3 && Number(match[2]) < 3))
|
|
258
|
+
const version = await this.run(["version"], options);
|
|
259
|
+
if (!supportsOAuth(version))
|
|
258
260
|
throw new CapabilityError("ALIYUN_CLI_VERSION", "Official Alibaba Cloud CLI 3.3.0 or later is required.");
|
|
259
261
|
if (!saved)
|
|
260
262
|
await this.save({
|
|
@@ -5,7 +5,7 @@ import { createAliyunLoginService } from "./factory.js";
|
|
|
5
5
|
export default class AliyunLogin extends Command {
|
|
6
6
|
static summary = "Sign in with your Alibaba Cloud identity";
|
|
7
7
|
static description = `Use the official Alibaba Cloud CLI OAuth login (PKCE) and obtain temporary STS credentials.
|
|
8
|
-
|
|
8
|
+
Uses an installed aliyun CLI 3.3.0 or later, or automatically downloads a verified official CLI on first login. No manual AccessKey configuration is needed.
|
|
9
9
|
The official-cli application must be authorized for your account. Credentials are stored in ~/.rkb-external/aliyun/config.json (owner access only).
|
|
10
10
|
External POP commands use AccessKey signatures and Alibaba Cloud RAM permissions.`;
|
|
11
11
|
static examples = [
|
|
@@ -39,6 +39,7 @@ External POP commands use AccessKey signatures and Alibaba Cloud RAM permissions
|
|
|
39
39
|
const result = await createAliyunLoginService().login({
|
|
40
40
|
force: flags.force,
|
|
41
41
|
signal: controller.signal,
|
|
42
|
+
onProgress: (message) => this.logToStderr(message),
|
|
42
43
|
onAuthorization: (url) => {
|
|
43
44
|
this.logToStderr("Complete Alibaba Cloud sign-in on this computer. If needed, open this URL manually:");
|
|
44
45
|
// Deliberately bypass file logging; never persist authorization URLs or codes.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rkb-cli",
|
|
3
|
-
"version": "0.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.2",
|
|
4
4
|
"description": "RaaS Knowledge Buddy command-line interface",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
"@alicloud/tea-util": "^1.4.11",
|
|
21
21
|
"@oclif/core": "^4.3.0",
|
|
22
22
|
"chalk": "^4.1.2",
|
|
23
|
+
"fflate": "0.8.3",
|
|
23
24
|
"open": "^11.0.0",
|
|
25
|
+
"tar": "^7.5.10",
|
|
24
26
|
"yaml": "^2.9.0"
|
|
25
27
|
},
|
|
26
28
|
"oclif": {
|