@synopackageland/cli 0.9.1 → 0.9.3
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/CHANGELOG.md +54 -0
- package/dist/cli.js +4 -3
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +5 -0
- package/dist/images/pin.js +0 -1
- package/dist/init.js +0 -1
- package/dist/runtime/executor-digests.json +33 -0
- package/dist/standalone/inspect-spk.js +3 -3
- package/dist/standalone/root-window.js +27 -12
- package/dist/standalone/spk.js +2 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.3 (2026-10-12)
|
|
4
|
+
|
|
5
|
+
Builds with SDK 0.9.4 and templates 0.9.3: `init` writes SDK v0.9.4 and
|
|
6
|
+
`@synopackageland/sdk` 0.9.4. The executor digest table records SDK 0.9.4,
|
|
7
|
+
whose executor asks DSM's native `SLIBAppPrivUserHas` for App Privileges
|
|
8
|
+
instead of the `SYNO.Core.AppPriv.App` preview. An App rebuilt with SDK 0.9.4
|
|
9
|
+
gets the new executor; the policy digest stays the same, so an installed App
|
|
10
|
+
needs no new approval.
|
|
11
|
+
|
|
12
|
+
繁中摘要:使用 SDK 0.9.4 與 templates 0.9.3。executor 改以 DSM 原生
|
|
13
|
+
`SLIBAppPrivUserHas` 判斷應用程式權限,digest 表加入 0.9.4;policy digest 不變,
|
|
14
|
+
重新建置後已安裝的 App 不需重新核准。限定 IP 的應用程式權限規則改為依瀏覽器位址
|
|
15
|
+
判斷;自行組 `auth` 的呼叫端必須帶 `remote_addr`,否則 executor 回 `APP_DENIED`。
|
|
16
|
+
|
|
17
|
+
- Behavior change: an Application Privileges rule limited to IP addresses
|
|
18
|
+
applies to the browser's address; with SDK 0.9.3 or earlier the executor
|
|
19
|
+
refused every request under such a rule. Work no browser asked for is
|
|
20
|
+
checked as coming from 127.0.0.1. The generated CGI sends the address
|
|
21
|
+
through `api.DSMSession`; code that builds the executor `auth` itself must
|
|
22
|
+
add `remote_addr` or the executor refuses with `APP_DENIED`.
|
|
23
|
+
- The fake-DSM executor flow tests host the 0.9.4 executor's embedded bridge
|
|
24
|
+
and check the online grant against the browser's address and the offline
|
|
25
|
+
write against 127.0.0.1.
|
|
26
|
+
|
|
27
|
+
## 0.9.2 (2026-10-12)
|
|
28
|
+
|
|
29
|
+
Builds with SDK 0.9.3 and templates 0.9.2: `init` writes SDK v0.9.3 and
|
|
30
|
+
`@synopackageland/sdk` 0.9.3. The executor digest table records SDK 0.9.2 and
|
|
31
|
+
0.9.3 (the same digests; 0.9.3 changes only translations). An App rebuilt
|
|
32
|
+
with SDK 0.9.2 or 0.9.3 gets the new executor; the policy digest stays the
|
|
33
|
+
same, so an installed App needs no new approval.
|
|
34
|
+
|
|
35
|
+
繁中摘要:`check` 與 `build` 的錯誤訊息每個錯誤碼只出現一次;`build --json` 的 SDK
|
|
36
|
+
下載失敗附上 GOPRIVATE 提示;有 executor 並宣告 `install.sharedFolder` 的 App 通過
|
|
37
|
+
`inspect` 的 postinst 檢查。重新安裝後的核准只需輸入一次密碼:共用資料夾讀回等 40 s,
|
|
38
|
+
沒到達 executor 的選擇任務在同一次核准內再跑一次,啟動後的健康檢查等 30 s。
|
|
39
|
+
|
|
40
|
+
- `check` and `build` name each error code once. 0.9.1 printed the first code
|
|
41
|
+
twice, for example
|
|
42
|
+
`[STANDALONE_PORTAL_REQUIRES_EXECUTOR] [STANDALONE_PORTAL_REQUIRES_EXECUTOR] …`.
|
|
43
|
+
- A `--json` error without its own hint takes the registry suggestion of its
|
|
44
|
+
code. `build --json` now answers `SDK_DOWNLOAD_FAILED` with the GOPRIVATE and
|
|
45
|
+
git-credential step instead of the generic hint.
|
|
46
|
+
- `inspect` accepts an App with `capabilities.rootOps` or `delegate` that
|
|
47
|
+
declares `install.sharedFolder`. Its `postinst` and `postupgrade` start the
|
|
48
|
+
executor before the shared-folder script, and 0.9.1 reported them as not
|
|
49
|
+
matching.
|
|
50
|
+
- Re-approval after a reinstall takes one password submission for both the
|
|
51
|
+
executor and an install-selected share. The DSM window now reads the share
|
|
52
|
+
selection back for 40 s instead of 20 s, runs a selection task that missed
|
|
53
|
+
the restarting executor once more without a new confirmation, and waits
|
|
54
|
+
30 s instead of 10 s for a started executor before withdrawing the tasks an
|
|
55
|
+
approval created.
|
|
56
|
+
|
|
3
57
|
## 0.9.1 (2026-10-12)
|
|
4
58
|
|
|
5
59
|
Fixes from the 0.9.0 acceptance on DSM (t-0294, t-0290, t-0292).
|
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,8 @@ import { basename, resolve, dirname, join } from "node:path";
|
|
|
5
5
|
import { findProjectRoot } from "./discovery.js";
|
|
6
6
|
import { loadContract } from "./contract/load.js";
|
|
7
7
|
import { contractIconPath } from "./contract/icon.js";
|
|
8
|
-
import { formatErrors } from "./errors.js";
|
|
8
|
+
import { codedMessage, formatErrors } from "./errors.js";
|
|
9
|
+
import { getErrorCode } from "./error-registry.js";
|
|
9
10
|
import { formatInspectReport, inspectProject } from "./inspect.js";
|
|
10
11
|
import { formatPinReport, pinProjectImages } from "./images/pin.js";
|
|
11
12
|
import { initFromTemplate, isTemplateName, listTemplates } from "./init.js";
|
|
@@ -33,7 +34,7 @@ import { checkJobs, commitOutputs, forEachArch } from "./standalone/arch-jobs.js
|
|
|
33
34
|
import { checkReproducible } from "./reproducible.js";
|
|
34
35
|
import { inspectAnySpk } from "./spk/inspect.js";
|
|
35
36
|
function failure(code, message, hint = "修正設定後重試;使用 --help 查看指令用法。") {
|
|
36
|
-
return Object.assign(new Error(
|
|
37
|
+
return Object.assign(new Error(codedMessage(code, message)), { code, hint });
|
|
37
38
|
}
|
|
38
39
|
function requireProject(cwd, project) {
|
|
39
40
|
const found = findProjectRoot(project ? resolve(cwd, project) : cwd);
|
|
@@ -531,7 +532,7 @@ export async function runCli(argv) {
|
|
|
531
532
|
ok: false,
|
|
532
533
|
code,
|
|
533
534
|
message,
|
|
534
|
-
hint: detail.hint ?? "修正設定後重試;使用 --help 查看指令用法。",
|
|
535
|
+
hint: detail.hint ?? getErrorCode(code)?.suggestion ?? "修正設定後重試;使用 --help 查看指令用法。",
|
|
535
536
|
...(detail.report ? { data: detail.report } : {}),
|
|
536
537
|
}));
|
|
537
538
|
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -3,4 +3,6 @@ export declare function makeError(code: string, span: SourceSpan, message: strin
|
|
|
3
3
|
export declare function contractSpan(file: string, line: number, column: number, path?: string): SourceSpan;
|
|
4
4
|
export declare function composeSpan(file: string, line: number, column: number, path?: string): SourceSpan;
|
|
5
5
|
export declare function formatError(error: ValidationError): string;
|
|
6
|
+
/** The message of an error thrown with a code: the code in brackets, then the message; the code only once. */
|
|
7
|
+
export declare function codedMessage(code: string, message: string): string;
|
|
6
8
|
export declare function formatErrors(errors: ValidationError[]): string;
|
package/dist/errors.js
CHANGED
|
@@ -13,6 +13,11 @@ export function formatError(error) {
|
|
|
13
13
|
: `${error.span.file}:${error.span.line}:${error.span.column}`;
|
|
14
14
|
return `[${error.code}] ${location}\n${error.message}\n建議:${error.suggestion}`;
|
|
15
15
|
}
|
|
16
|
+
/** The message of an error thrown with a code: the code in brackets, then the message; the code only once. */
|
|
17
|
+
export function codedMessage(code, message) {
|
|
18
|
+
const prefix = `[${code}] `;
|
|
19
|
+
return message.startsWith(prefix) ? message : prefix + message;
|
|
20
|
+
}
|
|
16
21
|
export function formatErrors(errors) {
|
|
17
22
|
return errors.map(formatError).join("\n\n");
|
|
18
23
|
}
|
package/dist/images/pin.js
CHANGED
|
@@ -9,7 +9,6 @@ import { isScalar, parseDocument } from "yaml";
|
|
|
9
9
|
* (Docker Hub and ghcr both issue an anonymous Bearer token for public repos).
|
|
10
10
|
*/
|
|
11
11
|
const DIGEST_VALUE = /^sha256:[0-9a-f]{64}$/i;
|
|
12
|
-
const DIGEST_AT_END = /@sha256:([0-9a-f]{64})$/i;
|
|
13
12
|
/** A registry returns the index (manifest list) digest when we ask for it. */
|
|
14
13
|
const MANIFEST_ACCEPT = [
|
|
15
14
|
"application/vnd.oci.image.index.v1+json",
|
package/dist/init.js
CHANGED
|
@@ -257,7 +257,6 @@ function substitute(path, substitutions) {
|
|
|
257
257
|
writeFileSync(path, text);
|
|
258
258
|
}
|
|
259
259
|
function substitutionsFor(template, targetDir, options) {
|
|
260
|
-
const ui = resolvedUi(template, options);
|
|
261
260
|
const caps = capabilityNames(options.with ?? []);
|
|
262
261
|
if (options.ui !== undefined && template !== "native-go")
|
|
263
262
|
throw new Error("[INIT_OPTIONS_UNSUPPORTED] --ui 只適用於 native-go。");
|
|
@@ -20,5 +20,38 @@
|
|
|
20
20
|
"armv7": "b65a14f0fc77a1d3221c7dc6781eb66059aa200956b094d58a62bd1cad10f116"
|
|
21
21
|
},
|
|
22
22
|
"goToolchain": "go1.26.9"
|
|
23
|
+
},
|
|
24
|
+
"0.9.2": {
|
|
25
|
+
"x86_64": "89c26f18ef05b601963341da6cce43f588bdf2a74039feaf3dfcffdd1295207f",
|
|
26
|
+
"aarch64": "6616953ac1721da64a134898ed1bec93d8e5a81fc044f8bc822751df1a871bdd",
|
|
27
|
+
"armv7": "d9c62b601454b8806a6eeef18a503385b508a8a3b947afa9b433a092979cc6da",
|
|
28
|
+
"lifecycle": {
|
|
29
|
+
"x86_64": "819459580135e016184ad80c258350d57194d6134e8aa4cf1caf5237085b1004",
|
|
30
|
+
"aarch64": "5abcfc21403e84a0af15d972bcb819ac6fedfd9aaf5898ddc8c4bb06130f9f59",
|
|
31
|
+
"armv7": "b65a14f0fc77a1d3221c7dc6781eb66059aa200956b094d58a62bd1cad10f116"
|
|
32
|
+
},
|
|
33
|
+
"goToolchain": "go1.26.9"
|
|
34
|
+
},
|
|
35
|
+
"0.9.3": {
|
|
36
|
+
"x86_64": "89c26f18ef05b601963341da6cce43f588bdf2a74039feaf3dfcffdd1295207f",
|
|
37
|
+
"aarch64": "6616953ac1721da64a134898ed1bec93d8e5a81fc044f8bc822751df1a871bdd",
|
|
38
|
+
"armv7": "d9c62b601454b8806a6eeef18a503385b508a8a3b947afa9b433a092979cc6da",
|
|
39
|
+
"lifecycle": {
|
|
40
|
+
"x86_64": "819459580135e016184ad80c258350d57194d6134e8aa4cf1caf5237085b1004",
|
|
41
|
+
"aarch64": "5abcfc21403e84a0af15d972bcb819ac6fedfd9aaf5898ddc8c4bb06130f9f59",
|
|
42
|
+
"armv7": "b65a14f0fc77a1d3221c7dc6781eb66059aa200956b094d58a62bd1cad10f116"
|
|
43
|
+
},
|
|
44
|
+
"goToolchain": "go1.26.9"
|
|
45
|
+
},
|
|
46
|
+
"0.9.4": {
|
|
47
|
+
"x86_64": "fc3c206b6a8e30b7470cedabf6676c027960311469685eab2fe544fe6cc671ac",
|
|
48
|
+
"aarch64": "aa1a8a1b88758ad47ca69d30e9649a24a93b94b5b5fc181ce782035c2c77c497",
|
|
49
|
+
"armv7": "0c39cb98b67735fede6bbbe91471695e26707f377cbc50dd860a1d9a7fe06fcf",
|
|
50
|
+
"lifecycle": {
|
|
51
|
+
"x86_64": "819459580135e016184ad80c258350d57194d6134e8aa4cf1caf5237085b1004",
|
|
52
|
+
"aarch64": "5abcfc21403e84a0af15d972bcb819ac6fedfd9aaf5898ddc8c4bb06130f9f59",
|
|
53
|
+
"armv7": "b65a14f0fc77a1d3221c7dc6781eb66059aa200956b094d58a62bd1cad10f116"
|
|
54
|
+
},
|
|
55
|
+
"goToolchain": "go1.26.9"
|
|
23
56
|
}
|
|
24
57
|
}
|
|
@@ -118,7 +118,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
118
118
|
if (metadata.entry)
|
|
119
119
|
await checkEntryLauncher(dir, info.package, metadata.entry, findings);
|
|
120
120
|
if (standalone)
|
|
121
|
-
checkSharedFolder(dir, findings, info.package, sharedFolder, executor?.policy.scope === 'install-selected', portal);
|
|
121
|
+
checkSharedFolder(dir, findings, info.package, sharedFolder, executor?.policy.scope === 'install-selected', portal, hasExecutor);
|
|
122
122
|
const icon = inspectPackageIcons(dir, findings);
|
|
123
123
|
const cgiEntry = payload.find((entry) => entry.path === CGI_PAYLOAD_PATH);
|
|
124
124
|
if (standalone && !cgiEntry) {
|
|
@@ -381,7 +381,7 @@ function checkBackground(dir, info, payload, findings, executor, daemon) {
|
|
|
381
381
|
* declaration (without one, any present file must be the default native
|
|
382
382
|
* install wizard), and no other install/upgrade wizard file may exist.
|
|
383
383
|
*/
|
|
384
|
-
function checkSharedFolder(dir, findings, pkg, folder, selected = false, portal) {
|
|
384
|
+
function checkSharedFolder(dir, findings, pkg, folder, selected = false, portal, executor = false) {
|
|
385
385
|
const wizardDir = join(dir, "WIZARD_UIFILES");
|
|
386
386
|
const present = (existsSync(wizardDir) ? readdirSync(wizardDir) : []).filter(name => /^(install|upgrade)_uifile(_|\.|$)/.test(name)).sort();
|
|
387
387
|
const postinst = readOptional(join(dir, "scripts/postinst"));
|
|
@@ -407,7 +407,7 @@ function checkSharedFolder(dir, findings, pkg, folder, selected = false, portal)
|
|
|
407
407
|
if (!folder)
|
|
408
408
|
return;
|
|
409
409
|
for (const hook of ["postinst", "postupgrade"]) {
|
|
410
|
-
if (readOptional(join(dir, "scripts", hook)) !== renderStandaloneScript(hook, { package: pkg, sharedFolder: folder, ...(selected ? { capabilities: { delegate: { scope: "install-selected" } } } : {}), ...portalManifest(portal) })) {
|
|
410
|
+
if (readOptional(join(dir, "scripts", hook)) !== renderStandaloneScript(hook, { package: pkg, sharedFolder: folder, ...(selected ? { capabilities: { delegate: { scope: "install-selected" } } } : executor ? { capabilities: { rootOps: true } } : {}), ...portalManifest(portal) })) {
|
|
411
411
|
findings.push(`scripts/${hook} 與 install.sharedFolder 宣告的固定腳本不符`);
|
|
412
412
|
}
|
|
413
413
|
}
|
|
@@ -155,22 +155,34 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
|
|
|
155
155
|
if (current.ref) throw fail("executor_selection_cleanup_conflict");
|
|
156
156
|
});
|
|
157
157
|
}
|
|
158
|
+
// The task waits up to 35 s for the executor, whose account, AppPriv and
|
|
159
|
+
// share lookups took 16-20 s on a measured NAS: read back for 40 s,
|
|
160
|
+
// backing off from 500 ms to 2 s like health.
|
|
161
|
+
function readSelection(budget, delay) {
|
|
162
|
+
delay = Math.min(delay || 500, 2000);
|
|
163
|
+
verified.state = "ready";
|
|
164
|
+
return health(verified, 0).then(function (current) {
|
|
165
|
+
if (current.state === "ready" && current.executor.selection && current.executor.selection.scope === selectedScope && Number.isSafeInteger(current.executor.selection.generation) && current.executor.selection.generation > 0) return current;
|
|
166
|
+
if (budget >= delay && options.wait) return options.wait(delay).then(function () { return readSelection(budget - delay, delay * 2); });
|
|
167
|
+
throw fail("executor_selection_unconfirmed");
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
// Right after a reinstall the boot task restarts the executor, so a run can
|
|
171
|
+
// miss it. Past the readback window that run has ended: run the pinned task
|
|
172
|
+
// once more. Its expected generation makes it stale if the first one landed.
|
|
173
|
+
function runAndRead(ref, runs) {
|
|
174
|
+
return send(taskAPI, "run", 2, {tasks: [ref]}).then(function () { return readSelection(40000); }).catch(function (e) {
|
|
175
|
+
if (runs > 1 && e && e.code === "executor_selection_unconfirmed") return runAndRead(ref, runs - 1);
|
|
176
|
+
throw e;
|
|
177
|
+
});
|
|
178
|
+
}
|
|
158
179
|
return find(task).then(function (current) {
|
|
159
180
|
if (current.state === "blocked" || current.state === "unsupported") throw fail("executor_selection_conflict");
|
|
160
181
|
var removed = current.ref ? remove(task, current.ref) : Promise.resolve();
|
|
161
182
|
return removed.then(function () { return create(false, definition(task), confirmation); });
|
|
162
183
|
}).then(function () { return find(task); }).then(function (current) {
|
|
163
184
|
if (current.state !== "ready") throw fail("executor_selection_readback_mismatch");
|
|
164
|
-
return
|
|
165
|
-
}).then(function () {
|
|
166
|
-
function readSelection(attempts) {
|
|
167
|
-
return health(verified, 0).then(function (current) {
|
|
168
|
-
if (current.state === "ready" && current.executor.selection && current.executor.selection.scope === selectedScope && Number.isSafeInteger(current.executor.selection.generation) && current.executor.selection.generation > 0) return current;
|
|
169
|
-
if (attempts > 1 && options.wait) return options.wait(500).then(function () { verified.state = "ready"; return readSelection(attempts - 1); });
|
|
170
|
-
throw fail("executor_selection_unconfirmed");
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
return readSelection(40);
|
|
185
|
+
return runAndRead(current.ref, 2);
|
|
174
186
|
}).then(function (result) { return removeExact().then(function () { return result; }); }, function (error) {
|
|
175
187
|
return removeExact().then(function () { throw error; });
|
|
176
188
|
});
|
|
@@ -188,6 +200,9 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
|
|
|
188
200
|
verified.executor = status; return verified;
|
|
189
201
|
}, function () { return null; });
|
|
190
202
|
}
|
|
203
|
+
// A started executor answered only after its own DSM lookups, which took
|
|
204
|
+
// 16-20 s on a measured NAS; giving up withdraws a fresh approval.
|
|
205
|
+
var startBudget = 30000;
|
|
191
206
|
// budget: how long to keep asking after a start (ms). The wait doubles from
|
|
192
207
|
// 500 ms to 2 s so an open window does not poll the executor twice a second.
|
|
193
208
|
function health(verified, budget, delay) {
|
|
@@ -211,7 +226,7 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
|
|
|
211
226
|
return send(eventAPI, "run", 1, verified.tasks.boot.ref);
|
|
212
227
|
}).then(function () {
|
|
213
228
|
// run can succeed for a nonexistent task. Read both definitions again.
|
|
214
|
-
return read().then(function (after) { return health(after,
|
|
229
|
+
return read().then(function (after) { return health(after, startBudget); });
|
|
215
230
|
}).catch(function (e) { verified.state = "blocked"; verified.reason = reason(e); return verified; });
|
|
216
231
|
}
|
|
217
232
|
// The window panel and the app's own settings share this access; each
|
|
@@ -230,7 +245,7 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
|
|
|
230
245
|
if (verified.state !== "ready") throw fail("executor_not_ready");
|
|
231
246
|
return options.call("executor.control", {action: action}).then(function () {
|
|
232
247
|
return send(taskAPI, "run", 2, {tasks: [verified.tasks.control.ref]});
|
|
233
|
-
}).then(read).then(function (after) { return action === "start" ? health(after,
|
|
248
|
+
}).then(read).then(function (after) { return action === "start" ? health(after, startBudget) : after; });
|
|
234
249
|
}).then(publish);
|
|
235
250
|
},
|
|
236
251
|
subscribe: function (listener) { listeners.push(listener); }
|
package/dist/standalone/spk.js
CHANGED
|
@@ -12,6 +12,7 @@ import { writeWizardUiFiles } from "../build-spk/wizard-i18n.js";
|
|
|
12
12
|
import { renderDelegateScopeSteps } from '../build-spk/delegate-scope.js';
|
|
13
13
|
import { createReproducibleTar } from "../build-spk/reproducible-archive.js";
|
|
14
14
|
import { renderStandaloneScript } from "./lifecycle.js";
|
|
15
|
+
import { codedMessage } from "../errors.js";
|
|
15
16
|
import { DSM_LANGS, LOCALES } from "./i18n.js";
|
|
16
17
|
import { appTitle, CGI_PAYLOAD_PATH, renderUiConfig, renderWindowScript, windowScriptName } from "./dsm-window.js";
|
|
17
18
|
import { writeUiTexts } from "../build-spk/ui-texts.js";
|
|
@@ -40,7 +41,7 @@ export const LIFECYCLE_SCRIPTS = [
|
|
|
40
41
|
export class StandaloneBuildError extends Error {
|
|
41
42
|
code;
|
|
42
43
|
constructor(code, message) {
|
|
43
|
-
super(
|
|
44
|
+
super(codedMessage(code, message));
|
|
44
45
|
this.code = code;
|
|
45
46
|
}
|
|
46
47
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synopackageland/cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"description": "Syno Package Land Developer Kit CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"prepublishOnly": "npm run build"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@synopackageland/templates": "0.9.
|
|
27
|
+
"@synopackageland/templates": "0.9.3",
|
|
28
28
|
"ajv": "^8.17.1",
|
|
29
29
|
"commander": "^13.1.0",
|
|
30
30
|
"tar": "^7.4.3",
|