@synopackageland/cli 0.3.1 → 0.3.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/dist/attestation/app-package.js +8 -1
- package/dist/attestation/platform-service.d.ts +70 -0
- package/dist/attestation/platform-service.js +250 -0
- package/dist/bin.js +0 -0
- package/dist/build-spk/generator.js +200 -10
- package/dist/build-spk/resource-worker.d.ts +14 -0
- package/dist/build-spk/resource-worker.js +140 -0
- package/dist/cli.js +69 -10
- package/dist/compose/analyze.d.ts +42 -1
- package/dist/compose/analyze.js +482 -18
- package/dist/contract/load.d.ts +1 -0
- package/dist/contract/load.js +1 -1
- package/dist/contract/posix-all-shares.d.ts +16 -0
- package/dist/contract/posix-all-shares.js +63 -0
- package/dist/contract/schema-validate.js +26 -1
- package/dist/contract/validate.js +57 -0
- package/dist/contract/web-launch.d.ts +6 -0
- package/dist/contract/web-launch.js +25 -0
- package/dist/deploy/deploy.d.ts +1 -0
- package/dist/deploy/deploy.js +94 -2
- package/dist/deploy/homes-values.d.ts +19 -0
- package/dist/deploy/homes-values.js +29 -0
- package/dist/deploy/param-values.d.ts +21 -0
- package/dist/deploy/param-values.js +51 -0
- package/dist/deploy/spk-version.d.ts +13 -0
- package/dist/deploy/spk-version.js +80 -27
- package/dist/doctor.d.ts +22 -0
- package/dist/doctor.js +594 -0
- package/dist/error-collector.d.ts +2 -0
- package/dist/error-collector.js +46 -0
- package/dist/error-registry.d.ts +7 -0
- package/dist/error-registry.js +837 -0
- package/dist/host/broker-upgrade-preflight.d.ts +61 -0
- package/dist/host/broker-upgrade-preflight.js +156 -0
- package/dist/host/catalog-install.d.ts +20 -0
- package/dist/host/catalog-install.js +143 -17
- package/dist/host/credentials.js +35 -5
- package/dist/host/dsm-client.d.ts +12 -0
- package/dist/host/dsm-client.js +69 -1
- package/dist/host/dsm-compound.d.ts +37 -0
- package/dist/host/dsm-compound.js +120 -0
- package/dist/host/fake-nas.d.ts +1 -0
- package/dist/host/fake-nas.js +3 -0
- package/dist/host/types.d.ts +6 -0
- package/dist/publish/curated-release.js +8 -4
- package/dist/publish/platform-service-release.d.ts +60 -0
- package/dist/publish/platform-service-release.js +333 -0
- package/dist/publish/preflight.js +24 -2
- package/dist/release-bin.js +5 -1
- package/dist/templates/hello-files/compose.yaml +2 -0
- package/dist/templates/hello-web/compose.yaml +14 -3
- package/dist/templates/hello-web/synology-app.yaml +1 -1
- package/dist/templates/hello-web-page/compose.yaml +14 -3
- package/dist/templates/hello-web-page/synology-app.yaml +1 -1
- package/dist/test-host.d.ts +2 -0
- package/dist/test-host.js +15 -2
- package/dist/types.d.ts +1 -1
- package/dist/validate.js +14 -3
- package/package.json +2 -2
- package/schema/doctor.schema.json +38 -0
- package/schema/synology-app.schema.json +1 -1
- package/skills/reference/create.md +19 -0
- package/skills/reference/diagnose.md +35 -2
- package/skills/reference/files.md +10 -0
- package/templates/hello-files/compose.yaml +2 -0
- package/templates/hello-web/compose.yaml +14 -3
- package/templates/hello-web/synology-app.yaml +1 -1
- package/templates/hello-web-page/compose.yaml +14 -3
- package/templates/hello-web-page/synology-app.yaml +1 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export declare const BROKER_UPGRADE_TARGET_HOST = "10.17.83.200";
|
|
2
|
+
export type DsmUpgradeResponseKind = "error-4552" | "success" | "consent" | "unknown";
|
|
3
|
+
export interface ConsentDialogEvidence {
|
|
4
|
+
consentDialog: {
|
|
5
|
+
dependers: string[];
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export interface BrokerUpgradeDependerBaseline {
|
|
9
|
+
packageIdentity: string;
|
|
10
|
+
version: string;
|
|
11
|
+
status: string;
|
|
12
|
+
running: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface BrokerUpgradeDepender extends BrokerUpgradeDependerBaseline {
|
|
15
|
+
healthCheckOk: boolean;
|
|
16
|
+
a23Classified: boolean;
|
|
17
|
+
dataSnapshotOk: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface BrokerUpgradePreflightInput {
|
|
20
|
+
targetHost: string;
|
|
21
|
+
broker: {
|
|
22
|
+
version: string;
|
|
23
|
+
status: string;
|
|
24
|
+
running: boolean;
|
|
25
|
+
};
|
|
26
|
+
dependers: BrokerUpgradeDepender[];
|
|
27
|
+
previousSpk: {
|
|
28
|
+
available: boolean;
|
|
29
|
+
sha256?: string;
|
|
30
|
+
};
|
|
31
|
+
canonicalizeGateSatisfied: boolean;
|
|
32
|
+
catalogEntry: {
|
|
33
|
+
version: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export type PreflightDecision = {
|
|
37
|
+
ok: true;
|
|
38
|
+
} | {
|
|
39
|
+
ok: false;
|
|
40
|
+
codes: string[];
|
|
41
|
+
};
|
|
42
|
+
export interface Evaluate4552BranchInput {
|
|
43
|
+
response: unknown;
|
|
44
|
+
dependers: BrokerUpgradeDependerBaseline[];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Classify only already-parsed DSM evidence. Strings are deliberately never
|
|
48
|
+
* parsed or searched: a textual mention of 4552 is not a DSM error response.
|
|
49
|
+
*/
|
|
50
|
+
export declare function classifyDsmUpgradeResponse(raw: unknown): DsmUpgradeResponseKind;
|
|
51
|
+
/**
|
|
52
|
+
* Evaluate every local prerequisite without performing an upgrade or
|
|
53
|
+
* proposing a stop/start action. A missing or malformed prerequisite is a
|
|
54
|
+
* failure, even when another part of the input looks healthy.
|
|
55
|
+
*/
|
|
56
|
+
export declare function evaluateBrokerUpgradePreflight(input: BrokerUpgradePreflightInput): PreflightDecision;
|
|
57
|
+
/**
|
|
58
|
+
* The 4552 path is entered only for a numeric DSM error and a non-empty,
|
|
59
|
+
* fully identified running baseline inventory.
|
|
60
|
+
*/
|
|
61
|
+
export declare function evaluate4552Branch(input: Evaluate4552BranchInput): PreflightDecision;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { comparePackageVersions } from "@synopackageland/package-source/index-manifest";
|
|
2
|
+
export const BROKER_UPGRADE_TARGET_HOST = "10.17.83.200";
|
|
3
|
+
/**
|
|
4
|
+
* Classify only already-parsed DSM evidence. Strings are deliberately never
|
|
5
|
+
* parsed or searched: a textual mention of 4552 is not a DSM error response.
|
|
6
|
+
*/
|
|
7
|
+
export function classifyDsmUpgradeResponse(raw) {
|
|
8
|
+
if (!isRecord(raw)) {
|
|
9
|
+
return "unknown";
|
|
10
|
+
}
|
|
11
|
+
if (isRecord(raw.error) && raw.error.code === 4552) {
|
|
12
|
+
return "error-4552";
|
|
13
|
+
}
|
|
14
|
+
const hasError = Object.prototype.hasOwnProperty.call(raw, "error") &&
|
|
15
|
+
raw.error !== null &&
|
|
16
|
+
raw.error !== undefined;
|
|
17
|
+
if (raw.success === true && !hasError) {
|
|
18
|
+
return "success";
|
|
19
|
+
}
|
|
20
|
+
if (isConsentDialogEvidence(raw)) {
|
|
21
|
+
return "consent";
|
|
22
|
+
}
|
|
23
|
+
return "unknown";
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Evaluate every local prerequisite without performing an upgrade or
|
|
27
|
+
* proposing a stop/start action. A missing or malformed prerequisite is a
|
|
28
|
+
* failure, even when another part of the input looks healthy.
|
|
29
|
+
*/
|
|
30
|
+
export function evaluateBrokerUpgradePreflight(input) {
|
|
31
|
+
const candidate = isRecord(input) ? input : {};
|
|
32
|
+
const codes = [];
|
|
33
|
+
const addCode = (code) => {
|
|
34
|
+
if (!codes.includes(code)) {
|
|
35
|
+
codes.push(code);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
if (candidate.targetHost !== BROKER_UPGRADE_TARGET_HOST) {
|
|
39
|
+
addCode("PREFLIGHT_WRONG_TARGET");
|
|
40
|
+
}
|
|
41
|
+
const broker = candidate.broker;
|
|
42
|
+
if (!isRecord(broker)) {
|
|
43
|
+
addCode("PREFLIGHT_BROKER_NOT_RUNNING");
|
|
44
|
+
addCode("PREFLIGHT_BROKER_VERSION_INVALID");
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
if (broker.running !== true || broker.status !== "running") {
|
|
48
|
+
addCode("PREFLIGHT_BROKER_NOT_RUNNING");
|
|
49
|
+
}
|
|
50
|
+
if (!isNonEmptyString(broker.version)) {
|
|
51
|
+
addCode("PREFLIGHT_BROKER_VERSION_INVALID");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const dependers = candidate.dependers;
|
|
55
|
+
if (!Array.isArray(dependers)) {
|
|
56
|
+
addCode("PREFLIGHT_DEPENDER_INVENTORY_INVALID");
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
for (const depender of dependers) {
|
|
60
|
+
if (!isRecord(depender)) {
|
|
61
|
+
addCode("PREFLIGHT_DEPENDER_INVENTORY_INVALID");
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (!isNonEmptyString(depender.packageIdentity) || !isNonEmptyString(depender.version)) {
|
|
65
|
+
addCode("PREFLIGHT_DEPENDER_INVENTORY_INVALID");
|
|
66
|
+
}
|
|
67
|
+
if (depender.running !== true) {
|
|
68
|
+
addCode("PREFLIGHT_DEPENDER_NOT_RUNNING");
|
|
69
|
+
}
|
|
70
|
+
if (depender.status !== "running") {
|
|
71
|
+
addCode("PREFLIGHT_DEPENDER_UNSAFE_STATUS");
|
|
72
|
+
}
|
|
73
|
+
if (depender.healthCheckOk !== true) {
|
|
74
|
+
addCode("PREFLIGHT_DEPENDER_HEALTHCHECK_FAILED");
|
|
75
|
+
}
|
|
76
|
+
if (depender.a23Classified !== true) {
|
|
77
|
+
addCode("PREFLIGHT_DEPENDER_A23_UNCLASSIFIED");
|
|
78
|
+
}
|
|
79
|
+
if (depender.dataSnapshotOk !== true) {
|
|
80
|
+
addCode("PREFLIGHT_DEPENDER_DATA_SNAPSHOT_MISSING");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const previousSpk = candidate.previousSpk;
|
|
85
|
+
if (!isRecord(previousSpk) || previousSpk.available !== true) {
|
|
86
|
+
addCode("PREFLIGHT_PREVIOUS_SPK_UNAVAILABLE");
|
|
87
|
+
}
|
|
88
|
+
else if (!isSha256(previousSpk.sha256)) {
|
|
89
|
+
addCode("PREFLIGHT_PREVIOUS_SPK_DIGEST_INVALID");
|
|
90
|
+
}
|
|
91
|
+
if (candidate.canonicalizeGateSatisfied !== true) {
|
|
92
|
+
addCode("PREFLIGHT_CANONICALIZE_GATE_UNSATISFIED");
|
|
93
|
+
}
|
|
94
|
+
const catalogEntry = candidate.catalogEntry;
|
|
95
|
+
if (!isRecord(catalogEntry) || !isNonEmptyString(catalogEntry.version)) {
|
|
96
|
+
addCode("PREFLIGHT_CATALOG_VERSION_INVALID");
|
|
97
|
+
}
|
|
98
|
+
else if (!isRecord(broker) || !isNonEmptyString(broker.version)) {
|
|
99
|
+
// The broker version gate is already reported above; without both version
|
|
100
|
+
// strings there is no safe comparison to perform.
|
|
101
|
+
addCode("PREFLIGHT_VERSION_COMPARISON_INVALID");
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const comparison = comparePackageVersions(catalogEntry.version, broker.version);
|
|
105
|
+
if (!Number.isFinite(comparison)) {
|
|
106
|
+
addCode("PREFLIGHT_VERSION_COMPARISON_INVALID");
|
|
107
|
+
}
|
|
108
|
+
else if (comparison <= 0) {
|
|
109
|
+
addCode("PREFLIGHT_CATALOG_VERSION_NOT_NEWER");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return decisionFromCodes(codes);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The 4552 path is entered only for a numeric DSM error and a non-empty,
|
|
116
|
+
* fully identified running baseline inventory.
|
|
117
|
+
*/
|
|
118
|
+
export function evaluate4552Branch(input) {
|
|
119
|
+
const candidate = isRecord(input) ? input : {};
|
|
120
|
+
const codes = [];
|
|
121
|
+
if (classifyDsmUpgradeResponse(candidate.response) !== "error-4552") {
|
|
122
|
+
codes.push("PREFLIGHT_RESPONSE_NOT_4552");
|
|
123
|
+
}
|
|
124
|
+
const dependers = candidate.dependers;
|
|
125
|
+
if (!Array.isArray(dependers) ||
|
|
126
|
+
dependers.length === 0 ||
|
|
127
|
+
dependers.some((depender) => !isRunningBaseline(depender))) {
|
|
128
|
+
codes.push("PREFLIGHT_DEPENDER_INVENTORY_INCOMPLETE");
|
|
129
|
+
}
|
|
130
|
+
return decisionFromCodes(codes);
|
|
131
|
+
}
|
|
132
|
+
function decisionFromCodes(codes) {
|
|
133
|
+
return codes.length === 0 ? { ok: true } : { ok: false, codes };
|
|
134
|
+
}
|
|
135
|
+
function isRunningBaseline(value) {
|
|
136
|
+
return (isRecord(value) &&
|
|
137
|
+
isNonEmptyString(value.packageIdentity) &&
|
|
138
|
+
isNonEmptyString(value.version) &&
|
|
139
|
+
value.status === "running" &&
|
|
140
|
+
value.running === true);
|
|
141
|
+
}
|
|
142
|
+
function isConsentDialogEvidence(value) {
|
|
143
|
+
const dialog = value.consentDialog;
|
|
144
|
+
return (isRecord(dialog) &&
|
|
145
|
+
Array.isArray(dialog.dependers) &&
|
|
146
|
+
dialog.dependers.every((depender) => typeof depender === "string"));
|
|
147
|
+
}
|
|
148
|
+
function isRecord(value) {
|
|
149
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
150
|
+
}
|
|
151
|
+
function isNonEmptyString(value) {
|
|
152
|
+
return typeof value === "string" && value.length > 0;
|
|
153
|
+
}
|
|
154
|
+
function isSha256(value) {
|
|
155
|
+
return typeof value === "string" && /^[0-9a-fA-F]{64}$/.test(value);
|
|
156
|
+
}
|
|
@@ -23,11 +23,23 @@ export interface InstalledPackageState {
|
|
|
23
23
|
installed: boolean;
|
|
24
24
|
version?: string;
|
|
25
25
|
status?: string;
|
|
26
|
+
error?: {
|
|
27
|
+
code?: number;
|
|
28
|
+
description?: string;
|
|
29
|
+
workerMsg?: unknown[];
|
|
30
|
+
};
|
|
31
|
+
brokenBy?: string;
|
|
32
|
+
stage?: string;
|
|
33
|
+
lastStage?: string;
|
|
34
|
+
statusCode?: number;
|
|
35
|
+
statusDescription?: string;
|
|
36
|
+
statusOrigin?: string;
|
|
26
37
|
}
|
|
27
38
|
export interface UploadedCatalogPackage {
|
|
28
39
|
id: string;
|
|
29
40
|
taskId: string;
|
|
30
41
|
}
|
|
42
|
+
export declare function packageStateFromRecord(value: unknown): InstalledPackageState;
|
|
31
43
|
export declare function packageStateFromListPayload(payload: unknown, packageId: string): InstalledPackageState | undefined;
|
|
32
44
|
export declare function fetchInstalledPackageState(dsmOrigin: string, cookieHeader: string, packageId: string): Promise<InstalledPackageState | undefined>;
|
|
33
45
|
export declare function waitForInstalledVersion(readState: () => InstalledPackageState | undefined | Promise<InstalledPackageState | undefined>, options: {
|
|
@@ -36,6 +48,14 @@ export declare function waitForInstalledVersion(readState: () => InstalledPackag
|
|
|
36
48
|
deadlineMs?: number;
|
|
37
49
|
pollMs?: number;
|
|
38
50
|
}): Promise<void>;
|
|
51
|
+
export interface UninstallPackageOptions {
|
|
52
|
+
dsmOrigin: string;
|
|
53
|
+
cookieHeader: string;
|
|
54
|
+
packageId: string;
|
|
55
|
+
deadlineMs?: number;
|
|
56
|
+
pollMs?: number;
|
|
57
|
+
}
|
|
58
|
+
export declare function uninstallPackageFromDsm(options: UninstallPackageOptions): Promise<void>;
|
|
39
59
|
export declare function catalogInstallationRequests(entry: Pick<CatalogPackageEntry, "package" | "version" | "size" | "beta" | "link" | "md5" | "qinst" | "deppkgs" | "breakpkgs" | "replacepkgs">, installed: boolean, sid: string, volumePath?: string): {
|
|
40
60
|
check: Record<string, string>;
|
|
41
61
|
install: Record<string, string>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import { dsmFetch } from "./dsm-http.js";
|
|
4
|
+
import { assertDsmCompoundResponse } from "./dsm-compound.js";
|
|
4
5
|
function normalizeDeppkgs(deppkgs) {
|
|
5
6
|
if (typeof deppkgs === "string") {
|
|
6
7
|
const match = /^([^><=]+)(>=|>|<=|<|==)(.+)$/.exec(deppkgs.trim());
|
|
@@ -27,7 +28,9 @@ async function dsmWebApi(dsmOrigin, cookieHeader, params, options = {}) {
|
|
|
27
28
|
});
|
|
28
29
|
const body = (await response.json());
|
|
29
30
|
const errorCode = body.error?.code;
|
|
30
|
-
if (!body.success &&
|
|
31
|
+
if (!body.success &&
|
|
32
|
+
!options.allowFailure &&
|
|
33
|
+
!(errorCode && options.allowErrorCodes?.includes(errorCode))) {
|
|
31
34
|
throw new Error(`DSM ${params.api}/${params.method} failed: ${JSON.stringify(body.error ?? body)}`);
|
|
32
35
|
}
|
|
33
36
|
return body;
|
|
@@ -92,29 +95,90 @@ function readInstalledPackageState(packageId, profileName) {
|
|
|
92
95
|
}
|
|
93
96
|
try {
|
|
94
97
|
const parsed = JSON.parse(result.stdout);
|
|
95
|
-
|
|
96
|
-
return { installed: false, version: parsed.data?.version, status: parsed.data?.status };
|
|
97
|
-
}
|
|
98
|
-
return { installed: true, version: parsed.data.version, status: parsed.data.status };
|
|
98
|
+
return packageStateFromRecord(parsed.data);
|
|
99
99
|
}
|
|
100
100
|
catch {
|
|
101
101
|
return undefined;
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
+
function record(value) {
|
|
105
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
106
|
+
? value
|
|
107
|
+
: {};
|
|
108
|
+
}
|
|
109
|
+
function stringValue(...values) {
|
|
110
|
+
return values.find((value) => typeof value === "string" && value.length > 0);
|
|
111
|
+
}
|
|
112
|
+
function numberValue(...values) {
|
|
113
|
+
return values.find((value) => typeof value === "number" && Number.isFinite(value));
|
|
114
|
+
}
|
|
115
|
+
function packageError(value) {
|
|
116
|
+
const source = record(value);
|
|
117
|
+
const code = numberValue(source.code);
|
|
118
|
+
const description = stringValue(source.description);
|
|
119
|
+
const workerMsg = Array.isArray(source.worker_msg)
|
|
120
|
+
? source.worker_msg
|
|
121
|
+
: Array.isArray(source.workerMsg)
|
|
122
|
+
? source.workerMsg
|
|
123
|
+
: undefined;
|
|
124
|
+
if (code === undefined && description === undefined && workerMsg === undefined) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
const error = {};
|
|
128
|
+
if (code !== undefined)
|
|
129
|
+
error.code = code;
|
|
130
|
+
if (description !== undefined)
|
|
131
|
+
error.description = description;
|
|
132
|
+
if (workerMsg !== undefined)
|
|
133
|
+
error.workerMsg = workerMsg;
|
|
134
|
+
return error;
|
|
135
|
+
}
|
|
136
|
+
export function packageStateFromRecord(value) {
|
|
137
|
+
const match = record(value);
|
|
138
|
+
const additional = record(match.additional);
|
|
139
|
+
const installedInfo = record(additional.installed_info ?? match.installedInfo);
|
|
140
|
+
const installedPath = stringValue(installedInfo.path, match.installedPath);
|
|
141
|
+
const version = stringValue(match.version);
|
|
142
|
+
const status = stringValue(additional.status, match.rawStatus, match.status);
|
|
143
|
+
const error = packageError(additional.error ?? match.error);
|
|
144
|
+
const state = {
|
|
145
|
+
installed: Boolean(installedPath) || match.installed === true,
|
|
146
|
+
};
|
|
147
|
+
if (version !== undefined)
|
|
148
|
+
state.version = version;
|
|
149
|
+
if (status !== undefined)
|
|
150
|
+
state.status = status;
|
|
151
|
+
if (error !== undefined)
|
|
152
|
+
state.error = error;
|
|
153
|
+
const brokenBy = stringValue(additional.broken_by, additional.brokenBy, match.broken_by, match.brokenBy);
|
|
154
|
+
if (brokenBy !== undefined)
|
|
155
|
+
state.brokenBy = brokenBy;
|
|
156
|
+
const stage = stringValue(additional.stage, match.stage);
|
|
157
|
+
if (stage !== undefined)
|
|
158
|
+
state.stage = stage;
|
|
159
|
+
const lastStage = stringValue(additional.last_stage, additional.lastStage, match.last_stage, match.lastStage);
|
|
160
|
+
if (lastStage !== undefined)
|
|
161
|
+
state.lastStage = lastStage;
|
|
162
|
+
const statusCode = numberValue(additional.status_code, additional.statusCode, match.status_code, match.statusCode, match.rawStatusCode);
|
|
163
|
+
if (statusCode !== undefined)
|
|
164
|
+
state.statusCode = statusCode;
|
|
165
|
+
const statusDescription = stringValue(additional.status_description, additional.statusDescription, match.status_description, match.statusDescription, match.rawStatusDescription);
|
|
166
|
+
if (statusDescription !== undefined)
|
|
167
|
+
state.statusDescription = statusDescription;
|
|
168
|
+
const statusOrigin = stringValue(additional.status_origin, additional.statusOrigin, match.status_origin, match.statusOrigin, match.rawStatusOrigin);
|
|
169
|
+
if (statusOrigin !== undefined)
|
|
170
|
+
state.statusOrigin = statusOrigin;
|
|
171
|
+
return state;
|
|
172
|
+
}
|
|
104
173
|
export function packageStateFromListPayload(payload, packageId) {
|
|
105
|
-
const
|
|
106
|
-
const data =
|
|
174
|
+
const root = payload && typeof payload === "object" ? payload : undefined;
|
|
175
|
+
const data = root?.data && typeof root.data === "object" ? root.data : root;
|
|
107
176
|
const packages = Array.isArray(data?.packages) ? data.packages : [];
|
|
108
177
|
const match = packages.find((item) => item && typeof item === "object" && item.id === packageId);
|
|
109
178
|
if (!match) {
|
|
110
179
|
return { installed: false };
|
|
111
180
|
}
|
|
112
|
-
|
|
113
|
-
const installedPath = additional?.installed_info?.path;
|
|
114
|
-
const installed = Boolean(installedPath) || match.installed === true;
|
|
115
|
-
const status = typeof additional?.status === "string" ? additional.status : undefined;
|
|
116
|
-
const version = typeof match.version === "string" ? match.version : undefined;
|
|
117
|
-
return { installed, version, status };
|
|
181
|
+
return packageStateFromRecord(match);
|
|
118
182
|
}
|
|
119
183
|
export async function fetchInstalledPackageState(dsmOrigin, cookieHeader, packageId) {
|
|
120
184
|
try {
|
|
@@ -160,6 +224,66 @@ export async function waitForInstalledVersion(readState, options) {
|
|
|
160
224
|
? `Timed out waiting for ${options.packageId} to reach version ${options.expectedVersion}`
|
|
161
225
|
: `Timed out waiting for ${options.packageId} to install from catalog`);
|
|
162
226
|
}
|
|
227
|
+
function packageStatusIsStopped(state) {
|
|
228
|
+
const status = state?.status?.trim().toLowerCase();
|
|
229
|
+
return state?.installed === true && (status === "stop" || status === "stopped" || status === "manually-stopped");
|
|
230
|
+
}
|
|
231
|
+
async function waitForPackageStopped(readState, options) {
|
|
232
|
+
const deadline = Date.now() + (options.deadlineMs ?? 5 * 60 * 1000);
|
|
233
|
+
const pollMs = options.pollMs ?? 1000;
|
|
234
|
+
while (Date.now() < deadline) {
|
|
235
|
+
const state = await readState();
|
|
236
|
+
if (packageStatusIsStopped(state)) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
240
|
+
}
|
|
241
|
+
throw new Error(`Timed out waiting for ${options.packageId} to stop before uninstall`);
|
|
242
|
+
}
|
|
243
|
+
async function waitForPackageUninstalled(readState, options) {
|
|
244
|
+
const deadline = Date.now() + (options.deadlineMs ?? 5 * 60 * 1000);
|
|
245
|
+
const pollMs = options.pollMs ?? 1000;
|
|
246
|
+
while (Date.now() < deadline) {
|
|
247
|
+
const state = await readState();
|
|
248
|
+
if (state?.installed === false) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
252
|
+
}
|
|
253
|
+
throw new Error(`Timed out waiting for ${options.packageId} to be uninstalled`);
|
|
254
|
+
}
|
|
255
|
+
function uninstallFailure(packageId, error) {
|
|
256
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
257
|
+
const failure = new Error(`Failed to uninstall ${packageId}: ${detail}`);
|
|
258
|
+
failure.code = "CATALOG_INSTALL_FAILED";
|
|
259
|
+
return failure;
|
|
260
|
+
}
|
|
261
|
+
export async function uninstallPackageFromDsm(options) {
|
|
262
|
+
try {
|
|
263
|
+
const sid = extractSid(options.cookieHeader);
|
|
264
|
+
const readState = () => fetchInstalledPackageState(options.dsmOrigin, options.cookieHeader, options.packageId);
|
|
265
|
+
await dsmWebApi(options.dsmOrigin, options.cookieHeader, {
|
|
266
|
+
api: "SYNO.Core.Package.Control",
|
|
267
|
+
version: "1",
|
|
268
|
+
method: "stop",
|
|
269
|
+
id: jsonApiParam(options.packageId),
|
|
270
|
+
_sid: sid,
|
|
271
|
+
});
|
|
272
|
+
await waitForPackageStopped(readState, options);
|
|
273
|
+
await dsmWebApi(options.dsmOrigin, options.cookieHeader, {
|
|
274
|
+
api: "SYNO.Core.Package.Uninstallation",
|
|
275
|
+
version: "1",
|
|
276
|
+
method: "uninstall",
|
|
277
|
+
id: jsonApiParam(options.packageId),
|
|
278
|
+
extra_values: jsonApiParam("{}"),
|
|
279
|
+
_sid: sid,
|
|
280
|
+
});
|
|
281
|
+
await waitForPackageUninstalled(readState, options);
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
throw uninstallFailure(options.packageId, error);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
163
287
|
export function catalogInstallationRequests(entry, installed, sid, volumePath) {
|
|
164
288
|
const install = {
|
|
165
289
|
api: "SYNO.Core.Package.Installation",
|
|
@@ -265,7 +389,7 @@ export async function installPackageFromLocalSpk(options) {
|
|
|
265
389
|
const installed = dsmState?.installed === true || isPackageInstalled(options.packageId, options.profileName);
|
|
266
390
|
const uploaded = await uploadLocalSpk(options, sid);
|
|
267
391
|
const compound = JSON.stringify(catalogCompoundSteps(uploaded, { version: options.version, size: readFileSync(options.spkPath).byteLength }, { blupgrade: installed, extraValues: options.extraValues }));
|
|
268
|
-
await dsmWebApi(options.dsmOrigin, options.cookieHeader, {
|
|
392
|
+
const compoundResponse = await dsmWebApi(options.dsmOrigin, options.cookieHeader, {
|
|
269
393
|
api: "SYNO.Entry.Request",
|
|
270
394
|
version: "1",
|
|
271
395
|
method: "request",
|
|
@@ -273,7 +397,8 @@ export async function installPackageFromLocalSpk(options) {
|
|
|
273
397
|
mode: jsonApiParam("sequential"),
|
|
274
398
|
compound,
|
|
275
399
|
_sid: sid,
|
|
276
|
-
});
|
|
400
|
+
}, { allowFailure: true });
|
|
401
|
+
assertDsmCompoundResponse(compoundResponse);
|
|
277
402
|
await waitForInstalledVersion(async () => (await fetchInstalledPackageState(options.dsmOrigin, options.cookieHeader, options.packageId)) ??
|
|
278
403
|
readInstalledPackageState(options.packageId, options.profileName), { packageId: options.packageId, expectedVersion: options.version });
|
|
279
404
|
}
|
|
@@ -303,7 +428,7 @@ async function uploadCatalogSpk(options, entry, sid) {
|
|
|
303
428
|
async function installCatalogViaUpload(options, entry, sid, installed) {
|
|
304
429
|
const uploaded = await uploadCatalogSpk(options, entry, sid);
|
|
305
430
|
const compound = JSON.stringify(catalogCompoundSteps(uploaded, entry, { blupgrade: installed, extraValues: options.extraValues }));
|
|
306
|
-
await dsmWebApi(options.dsmOrigin, options.cookieHeader, {
|
|
431
|
+
const compoundResponse = await dsmWebApi(options.dsmOrigin, options.cookieHeader, {
|
|
307
432
|
api: "SYNO.Entry.Request",
|
|
308
433
|
version: "1",
|
|
309
434
|
method: "request",
|
|
@@ -311,7 +436,8 @@ async function installCatalogViaUpload(options, entry, sid, installed) {
|
|
|
311
436
|
mode: jsonApiParam("sequential"),
|
|
312
437
|
compound,
|
|
313
438
|
_sid: sid,
|
|
314
|
-
});
|
|
439
|
+
}, { allowFailure: true });
|
|
440
|
+
assertDsmCompoundResponse(compoundResponse);
|
|
315
441
|
}
|
|
316
442
|
export async function installPackageFromCatalog(options) {
|
|
317
443
|
const entry = options.expectedVersion
|
package/dist/host/credentials.js
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
function findProfile(settings, input) {
|
|
5
|
+
if (!input) {
|
|
6
|
+
return settings.hosts?.[settings.defaultHost ?? "default"];
|
|
7
|
+
}
|
|
8
|
+
// 1. Direct match by profile name
|
|
9
|
+
if (settings.hosts?.[input]) {
|
|
10
|
+
return settings.hosts[input];
|
|
11
|
+
}
|
|
12
|
+
// 2. Match by URL hostname or exact URL
|
|
13
|
+
for (const profile of Object.values(settings.hosts ?? {})) {
|
|
14
|
+
if (profile?.url) {
|
|
15
|
+
try {
|
|
16
|
+
const u = new URL(profile.url);
|
|
17
|
+
if (u.hostname === input ||
|
|
18
|
+
profile.url === input ||
|
|
19
|
+
profile.url.replace(/\/$/, "") === input.replace(/\/$/, "")) {
|
|
20
|
+
return profile;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// ignore invalid URL
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
4
30
|
export function resolveHostCredentials(hostName) {
|
|
5
31
|
const settingsPath = join(homedir(), ".synoagentcli", "setting.json");
|
|
6
32
|
let settings;
|
|
@@ -8,10 +34,9 @@ export function resolveHostCredentials(hostName) {
|
|
|
8
34
|
settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
9
35
|
}
|
|
10
36
|
catch {
|
|
11
|
-
return fromEnv();
|
|
37
|
+
return fromEnv(hostName);
|
|
12
38
|
}
|
|
13
|
-
const
|
|
14
|
-
const profile = settings.hosts?.[selected] ?? settings.hosts?.[settings.defaultHost ?? "default"];
|
|
39
|
+
const profile = findProfile(settings, hostName);
|
|
15
40
|
const username = profile?.username ?? profile?.auth?.username;
|
|
16
41
|
const password = profile?.password ?? profile?.auth?.password;
|
|
17
42
|
if (profile?.url && username && password) {
|
|
@@ -24,10 +49,15 @@ export function resolveHostCredentials(hostName) {
|
|
|
24
49
|
if (profile?.url) {
|
|
25
50
|
return fromEnv(profile.url);
|
|
26
51
|
}
|
|
27
|
-
return fromEnv();
|
|
52
|
+
return fromEnv(hostName);
|
|
28
53
|
}
|
|
29
54
|
function fromEnv(urlOverride) {
|
|
30
|
-
|
|
55
|
+
let dsmOrigin = process.env.DSM_URL;
|
|
56
|
+
if (urlOverride) {
|
|
57
|
+
dsmOrigin = urlOverride.startsWith("http://") || urlOverride.startsWith("https://")
|
|
58
|
+
? urlOverride
|
|
59
|
+
: `http://${urlOverride}:5000`;
|
|
60
|
+
}
|
|
31
61
|
const password = process.env.DSM_PASSWORD;
|
|
32
62
|
const username = process.env.DSM_USERNAME ?? "admin";
|
|
33
63
|
if (!dsmOrigin || !password) {
|
|
@@ -4,6 +4,17 @@ export interface CreateNasHostOptions {
|
|
|
4
4
|
credentials?: HostCredentials;
|
|
5
5
|
}
|
|
6
6
|
export declare function createNasHost(options?: CreateNasHostOptions): NasHost | null;
|
|
7
|
+
declare function synoagentcli(args: string[], profileName?: string): {
|
|
8
|
+
status: number | null;
|
|
9
|
+
stdout: string;
|
|
10
|
+
stderr: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* DSM's User Home read reports the selected volume, not the final share path.
|
|
14
|
+
* Keep the projection strict: an unexpected response must not become a host
|
|
15
|
+
* bind to an arbitrary path.
|
|
16
|
+
*/
|
|
17
|
+
export declare function homesPathFromDsmUserHomeRead(result: Pick<ReturnType<typeof synoagentcli>, "status" | "stdout">): string | undefined;
|
|
7
18
|
/**
|
|
8
19
|
* DSM reports the CPU vendor, not the ISA. Every x86_64 platform Synology
|
|
9
20
|
* ships is Intel or AMD; every aarch64 one is Realtek, Marvell, Annapurna or
|
|
@@ -11,3 +22,4 @@ export declare function createNasHost(options?: CreateNasHostOptions): NasHost |
|
|
|
11
22
|
* `--arch` rather than guessing wrong and building an unusable SPK.
|
|
12
23
|
*/
|
|
13
24
|
export declare function archFamilyForCpuVendor(vendor: string | undefined): "x86_64" | "aarch64" | undefined;
|
|
25
|
+
export {};
|
package/dist/host/dsm-client.js
CHANGED
|
@@ -6,7 +6,7 @@ import { IDENTITY_CGI_PATH } from "./broker-paths.js";
|
|
|
6
6
|
import { packageSourceOrigin } from "../deploy/catalog-publish.js";
|
|
7
7
|
import { lookupSharePathWithSynoagentcli } from "./share-lookup.js";
|
|
8
8
|
import { dsmFetch } from "./dsm-http.js";
|
|
9
|
-
import { installPackageFromCatalog, installPackageFromLocalSpk, fetchInstalledPackageState } from "./catalog-install.js";
|
|
9
|
+
import { installPackageFromCatalog, installPackageFromLocalSpk, uninstallPackageFromDsm, fetchInstalledPackageState, packageStateFromRecord, } from "./catalog-install.js";
|
|
10
10
|
import { resolveHostCredentials } from "./credentials.js";
|
|
11
11
|
export function createNasHost(options = {}) {
|
|
12
12
|
const credentials = options.credentials ?? resolveHostCredentials(options.host);
|
|
@@ -27,6 +27,34 @@ function synoagentcli(args, profileName) {
|
|
|
27
27
|
stderr: result.stderr ?? "",
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* DSM's User Home read reports the selected volume, not the final share path.
|
|
32
|
+
* Keep the projection strict: an unexpected response must not become a host
|
|
33
|
+
* bind to an arbitrary path.
|
|
34
|
+
*/
|
|
35
|
+
export function homesPathFromDsmUserHomeRead(result) {
|
|
36
|
+
if (result.status !== 0) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
let payload;
|
|
40
|
+
try {
|
|
41
|
+
payload = JSON.parse(result.stdout);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
if (payload.success !== true || payload.data?.enable !== true) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
if (typeof payload.data.location !== "string") {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const volume = payload.data.location.trim().replace(/\/+$/, "");
|
|
53
|
+
if (!/^\/volume[1-9][0-9]*$/.test(volume)) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
return `${volume}/homes`;
|
|
57
|
+
}
|
|
30
58
|
function createDsmNasHost(credentials, profileName) {
|
|
31
59
|
const dsmOrigin = credentials.dsmOrigin.replace(/\/$/, "");
|
|
32
60
|
const brokerBaseUrl = `${dsmOrigin}${IDENTITY_CGI_PATH}`;
|
|
@@ -172,6 +200,19 @@ function createDsmNasHost(credentials, profileName) {
|
|
|
172
200
|
projectEnvCache.delete(input.packageId);
|
|
173
201
|
}
|
|
174
202
|
},
|
|
203
|
+
async uninstallPackage(packageIdentity) {
|
|
204
|
+
try {
|
|
205
|
+
await ensureLogin();
|
|
206
|
+
await uninstallPackageFromDsm({
|
|
207
|
+
dsmOrigin,
|
|
208
|
+
cookieHeader: cookieHeader ?? "",
|
|
209
|
+
packageId: packageIdentity,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
projectEnvCache.delete(packageIdentity);
|
|
214
|
+
}
|
|
215
|
+
},
|
|
175
216
|
async getArchFamily() {
|
|
176
217
|
await ensureLogin();
|
|
177
218
|
const response = await dsmFetch(`${dsmOrigin}/webapi/entry.cgi?api=SYNO.Core.System&version=1&method=info`, { headers: { cookie: cookieHeader ?? "" } });
|
|
@@ -184,6 +225,15 @@ function createDsmNasHost(credentials, profileName) {
|
|
|
184
225
|
async resolveSharePath(logicalName) {
|
|
185
226
|
return lookupSharePathWithSynoagentcli(logicalName, profileName);
|
|
186
227
|
},
|
|
228
|
+
async resolveHomesPath() {
|
|
229
|
+
try {
|
|
230
|
+
const result = synoagentcli(["dsm", "control-panel", "user-group", "advanced", "get", "--section", "home"], profileName);
|
|
231
|
+
return homesPathFromDsmUserHomeRead(result);
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
},
|
|
187
237
|
async getInstalledProjectEnv(packageIdentity) {
|
|
188
238
|
const cached = projectEnvCache.get(packageIdentity);
|
|
189
239
|
if (cached) {
|
|
@@ -222,6 +272,24 @@ function createDsmNasHost(credentials, profileName) {
|
|
|
222
272
|
throw error;
|
|
223
273
|
}
|
|
224
274
|
},
|
|
275
|
+
async getInstalledPackageState(packageIdentity) {
|
|
276
|
+
await ensureLogin();
|
|
277
|
+
const viaDsm = await fetchInstalledPackageState(dsmOrigin, cookieHeader ?? "", packageIdentity);
|
|
278
|
+
if (viaDsm) {
|
|
279
|
+
return viaDsm;
|
|
280
|
+
}
|
|
281
|
+
const result = synoagentcli(["dsm", "package-center", "get", packageIdentity], profileName);
|
|
282
|
+
if (result.status !== 0) {
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
const parsed = JSON.parse(result.stdout);
|
|
287
|
+
return packageStateFromRecord(parsed.data);
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
},
|
|
225
293
|
async getInstalledPackageVersion(packageIdentity) {
|
|
226
294
|
await ensureLogin();
|
|
227
295
|
const viaDsm = await fetchInstalledPackageState(dsmOrigin, cookieHeader ?? "", packageIdentity);
|