@synopackageland/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/dist/attestation/app-package.d.ts +86 -0
- package/dist/attestation/app-package.js +199 -0
- package/dist/attestation/install-on-host.d.ts +14 -0
- package/dist/attestation/install-on-host.js +52 -0
- package/dist/attestation/keyring.d.ts +21 -0
- package/dist/attestation/keyring.js +168 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +14 -0
- package/dist/build-spk/dsm-arch.d.ts +4 -0
- package/dist/build-spk/dsm-arch.js +32 -0
- package/dist/build-spk/dsm-icons.d.ts +2 -0
- package/dist/build-spk/dsm-icons.js +67 -0
- package/dist/build-spk/generator.d.ts +43 -0
- package/dist/build-spk/generator.js +653 -0
- package/dist/build-spk/native-payload.d.ts +22 -0
- package/dist/build-spk/native-payload.js +654 -0
- package/dist/build-spk/reproducible-archive.d.ts +8 -0
- package/dist/build-spk/reproducible-archive.js +34 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +345 -0
- package/dist/compose/analyze.d.ts +25 -0
- package/dist/compose/analyze.js +346 -0
- package/dist/compose/env.d.ts +7 -0
- package/dist/compose/env.js +36 -0
- package/dist/compose/privilege.d.ts +14 -0
- package/dist/compose/privilege.js +102 -0
- package/dist/compose/runner.d.ts +32 -0
- package/dist/compose/runner.js +84 -0
- package/dist/contract/load.d.ts +25 -0
- package/dist/contract/load.js +87 -0
- package/dist/contract/schema-validate.d.ts +3 -0
- package/dist/contract/schema-validate.js +50 -0
- package/dist/contract/validate.d.ts +3 -0
- package/dist/contract/validate.js +80 -0
- package/dist/contract/web-launch.d.ts +19 -0
- package/dist/contract/web-launch.js +94 -0
- package/dist/deploy/catalog-publish.d.ts +3 -0
- package/dist/deploy/catalog-publish.js +7 -0
- package/dist/deploy/deploy.d.ts +22 -0
- package/dist/deploy/deploy.js +136 -0
- package/dist/deploy/identity-shared.d.ts +3 -0
- package/dist/deploy/identity-shared.js +14 -0
- package/dist/deploy/identity.d.ts +7 -0
- package/dist/deploy/identity.js +20 -0
- package/dist/deploy/package-source.d.ts +7 -0
- package/dist/deploy/package-source.js +17 -0
- package/dist/deploy/run-as-values.d.ts +18 -0
- package/dist/deploy/run-as-values.js +48 -0
- package/dist/deploy/share-values.d.ts +20 -0
- package/dist/deploy/share-values.js +60 -0
- package/dist/deploy/spk-info.d.ts +12 -0
- package/dist/deploy/spk-info.js +41 -0
- package/dist/deploy/spk-version.d.ts +4 -0
- package/dist/deploy/spk-version.js +64 -0
- package/dist/dev.d.ts +13 -0
- package/dist/dev.js +31 -0
- package/dist/discovery.d.ts +8 -0
- package/dist/discovery.js +22 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +18 -0
- package/dist/host/broker-paths.d.ts +1 -0
- package/dist/host/broker-paths.js +1 -0
- package/dist/host/catalog-install.d.ts +59 -0
- package/dist/host/catalog-install.js +326 -0
- package/dist/host/credentials.d.ts +2 -0
- package/dist/host/credentials.js +37 -0
- package/dist/host/digests.d.ts +5 -0
- package/dist/host/digests.js +55 -0
- package/dist/host/dsm-client.d.ts +6 -0
- package/dist/host/dsm-client.js +290 -0
- package/dist/host/dsm-http.d.ts +1 -0
- package/dist/host/dsm-http.js +87 -0
- package/dist/host/fake-nas.d.ts +16 -0
- package/dist/host/fake-nas.js +116 -0
- package/dist/host/open-url.d.ts +5 -0
- package/dist/host/open-url.js +13 -0
- package/dist/host/share-lookup.d.ts +3 -0
- package/dist/host/share-lookup.js +144 -0
- package/dist/host/types.d.ts +67 -0
- package/dist/host/types.js +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +14 -0
- package/dist/info.d.ts +17 -0
- package/dist/info.js +34 -0
- package/dist/init.d.ts +4 -0
- package/dist/init.js +31 -0
- package/dist/inspect.d.ts +13 -0
- package/dist/inspect.js +102 -0
- package/dist/load-env.d.ts +1 -0
- package/dist/load-env.js +31 -0
- package/dist/log.d.ts +13 -0
- package/dist/log.js +20 -0
- package/dist/mock-broker/bootstrap.d.ts +2 -0
- package/dist/mock-broker/bootstrap.js +8 -0
- package/dist/mock-broker/files.d.ts +60 -0
- package/dist/mock-broker/files.js +408 -0
- package/dist/mock-broker/oidc.d.ts +31 -0
- package/dist/mock-broker/oidc.js +161 -0
- package/dist/mock-broker/path.d.ts +9 -0
- package/dist/mock-broker/path.js +50 -0
- package/dist/mock-broker/server.d.ts +19 -0
- package/dist/mock-broker/server.js +350 -0
- package/dist/publish/build-ledger.d.ts +22 -0
- package/dist/publish/build-ledger.js +26 -0
- package/dist/publish/community-path.d.ts +6 -0
- package/dist/publish/community-path.js +41 -0
- package/dist/publish/curated-release.d.ts +78 -0
- package/dist/publish/curated-release.js +693 -0
- package/dist/publish/file-build-ledger.d.ts +18 -0
- package/dist/publish/file-build-ledger.js +268 -0
- package/dist/publish/github-pr.d.ts +25 -0
- package/dist/publish/github-pr.js +85 -0
- package/dist/publish/github-repo.d.ts +8 -0
- package/dist/publish/github-repo.js +21 -0
- package/dist/publish/publish.d.ts +23 -0
- package/dist/publish/publish.js +128 -0
- package/dist/publish/sync-definition.d.ts +4 -0
- package/dist/publish/sync-definition.js +48 -0
- package/dist/skill.d.ts +10 -0
- package/dist/skill.js +50 -0
- package/dist/templates/compose-import/compose.yaml +11 -0
- package/dist/templates/compose-import/synology-app.yaml +15 -0
- package/dist/templates/hello-files/com.synology.hello.files.dev.spk +0 -0
- package/dist/templates/hello-files/compose.yaml +8 -0
- package/dist/templates/hello-files/synology-app.yaml +13 -0
- package/dist/templates/hello-web/compose.yaml +7 -0
- package/dist/templates/hello-web/html/app.js +23 -0
- package/dist/templates/hello-web/html/index.html +15 -0
- package/dist/templates/hello-web/html/sdk.js +68 -0
- package/dist/templates/hello-web/synology-app.yaml +13 -0
- package/dist/templates/hello-web-page/com.synology.hello.web.page.dev.spk +0 -0
- package/dist/templates/hello-web-page/compose.yaml +7 -0
- package/dist/templates/hello-web-page/html/app.js +23 -0
- package/dist/templates/hello-web-page/html/index.html +15 -0
- package/dist/templates/hello-web-page/html/sdk.js +68 -0
- package/dist/templates/hello-web-page/synology-app.yaml +13 -0
- package/dist/test-command.d.ts +13 -0
- package/dist/test-command.js +26 -0
- package/dist/test-host.d.ts +13 -0
- package/dist/test-host.js +24 -0
- package/dist/types.d.ts +59 -0
- package/dist/types.js +1 -0
- package/dist/validate.d.ts +8 -0
- package/dist/validate.js +103 -0
- package/package.json +38 -0
- package/schema/synology-app.schema.json +137 -0
- package/skills/SKILL.md +51 -0
- package/skills/reference/create.md +34 -0
- package/skills/reference/deploy.md +41 -0
- package/skills/reference/diagnose.md +42 -0
- package/skills/reference/files.md +37 -0
- package/skills/reference/identity.md +31 -0
- package/skills/reference/wrap.md +35 -0
- package/templates/compose-import/compose.yaml +11 -0
- package/templates/compose-import/synology-app.yaml +15 -0
- package/templates/hello-files/com.synology.hello.files.dev.spk +0 -0
- package/templates/hello-files/compose.yaml +8 -0
- package/templates/hello-files/synology-app.yaml +13 -0
- package/templates/hello-web/compose.yaml +7 -0
- package/templates/hello-web/html/app.js +23 -0
- package/templates/hello-web/html/index.html +15 -0
- package/templates/hello-web/html/sdk.js +68 -0
- package/templates/hello-web/synology-app.yaml +13 -0
- package/templates/hello-web-page/com.synology.hello.web.page.dev.spk +0 -0
- package/templates/hello-web-page/compose.yaml +7 -0
- package/templates/hello-web-page/html/app.js +23 -0
- package/templates/hello-web-page/html/index.html +15 -0
- package/templates/hello-web-page/html/sdk.js +68 -0
- package/templates/hello-web-page/synology-app.yaml +13 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { dsmFetch } from "./dsm-http.js";
|
|
4
|
+
function normalizeDeppkgs(deppkgs) {
|
|
5
|
+
if (typeof deppkgs === "string") {
|
|
6
|
+
const match = /^([^><=]+)(>=|>|<=|<|==)(.+)$/.exec(deppkgs.trim());
|
|
7
|
+
if (match) {
|
|
8
|
+
return { [match[1]]: { [match[2]]: match[3] } };
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return deppkgs;
|
|
12
|
+
}
|
|
13
|
+
function jsonApiParam(value) {
|
|
14
|
+
return JSON.stringify(value ?? null);
|
|
15
|
+
}
|
|
16
|
+
function extractSid(cookieHeader) {
|
|
17
|
+
const match = /(?:^|;\s*)id=([^;]+)/.exec(cookieHeader);
|
|
18
|
+
if (!match?.[1]) {
|
|
19
|
+
throw new Error("DSM session cookie missing id=");
|
|
20
|
+
}
|
|
21
|
+
return match[1];
|
|
22
|
+
}
|
|
23
|
+
async function dsmWebApi(dsmOrigin, cookieHeader, params, options = {}) {
|
|
24
|
+
const query = new URLSearchParams(params);
|
|
25
|
+
const response = await dsmFetch(`${dsmOrigin}/webapi/entry.cgi?${query.toString()}`, {
|
|
26
|
+
headers: { cookie: cookieHeader },
|
|
27
|
+
});
|
|
28
|
+
const body = (await response.json());
|
|
29
|
+
const errorCode = body.error?.code;
|
|
30
|
+
if (!body.success && !(errorCode && options.allowErrorCodes?.includes(errorCode))) {
|
|
31
|
+
throw new Error(`DSM ${params.api}/${params.method} failed: ${JSON.stringify(body.error ?? body)}`);
|
|
32
|
+
}
|
|
33
|
+
return body;
|
|
34
|
+
}
|
|
35
|
+
async function fetchCatalogEntry(options) {
|
|
36
|
+
const sid = extractSid(options.cookieHeader);
|
|
37
|
+
const list = await dsmWebApi(options.dsmOrigin, options.cookieHeader, {
|
|
38
|
+
api: "SYNO.Core.Package.Server",
|
|
39
|
+
version: "2",
|
|
40
|
+
method: "list",
|
|
41
|
+
blforcereload: "true",
|
|
42
|
+
blloadothers: "true",
|
|
43
|
+
_sid: sid,
|
|
44
|
+
});
|
|
45
|
+
const data = list.data;
|
|
46
|
+
const entries = [...(data.packages ?? []), ...(data.beta_packages ?? [])];
|
|
47
|
+
const entry = entries.find((item) => item.package === options.packageId);
|
|
48
|
+
if (!entry) {
|
|
49
|
+
throw new Error(`Package ${options.packageId} not found in Package Source catalog`);
|
|
50
|
+
}
|
|
51
|
+
return entry;
|
|
52
|
+
}
|
|
53
|
+
async function fetchCatalogEntryFromOrigin(origin, packageId) {
|
|
54
|
+
const catalogUrl = `${origin.replace(/\/$/, "")}/?arch=noarch&build=69057&major=7`;
|
|
55
|
+
try {
|
|
56
|
+
const response = await dsmFetch(catalogUrl);
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const data = (await response.json());
|
|
61
|
+
const entries = [...(data.packages ?? []), ...(data.beta_packages ?? [])];
|
|
62
|
+
return entries.find((item) => item.package === packageId);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function waitForCatalogEntry(options, expectedVersion) {
|
|
69
|
+
const deadline = Date.now() + 5 * 60 * 1000;
|
|
70
|
+
let lastVersion;
|
|
71
|
+
while (Date.now() < deadline) {
|
|
72
|
+
const originEntry = options.packageSourceOrigin
|
|
73
|
+
? await fetchCatalogEntryFromOrigin(options.packageSourceOrigin, options.packageId)
|
|
74
|
+
: undefined;
|
|
75
|
+
const entry = originEntry ?? await fetchCatalogEntry(options);
|
|
76
|
+
lastVersion = entry.version;
|
|
77
|
+
if (entry.version === expectedVersion) {
|
|
78
|
+
return entry;
|
|
79
|
+
}
|
|
80
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
81
|
+
}
|
|
82
|
+
throw new Error(`Package ${options.packageId} catalog shows ${lastVersion ?? "unknown"}; expected ${expectedVersion}`);
|
|
83
|
+
}
|
|
84
|
+
function readInstalledPackageState(packageId, profileName) {
|
|
85
|
+
const args = ["dsm", "package-center", "get", packageId];
|
|
86
|
+
if (profileName) {
|
|
87
|
+
args.push("--host", profileName);
|
|
88
|
+
}
|
|
89
|
+
const result = spawnSync("synoagentcli", args, { encoding: "utf8" });
|
|
90
|
+
if (result.status !== 0) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const parsed = JSON.parse(result.stdout);
|
|
95
|
+
if (!parsed.data?.installed) {
|
|
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 };
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export function packageStateFromListPayload(payload, packageId) {
|
|
105
|
+
const record = payload && typeof payload === "object" ? payload : undefined;
|
|
106
|
+
const data = record?.data && typeof record.data === "object" ? record.data : record;
|
|
107
|
+
const packages = Array.isArray(data?.packages) ? data.packages : [];
|
|
108
|
+
const match = packages.find((item) => item && typeof item === "object" && item.id === packageId);
|
|
109
|
+
if (!match) {
|
|
110
|
+
return { installed: false };
|
|
111
|
+
}
|
|
112
|
+
const additional = match.additional;
|
|
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 };
|
|
118
|
+
}
|
|
119
|
+
export async function fetchInstalledPackageState(dsmOrigin, cookieHeader, packageId) {
|
|
120
|
+
try {
|
|
121
|
+
const body = await dsmWebApi(dsmOrigin, cookieHeader, {
|
|
122
|
+
api: "SYNO.Core.Package",
|
|
123
|
+
version: "2",
|
|
124
|
+
method: "list",
|
|
125
|
+
additional: JSON.stringify(["status", "installed_info"]),
|
|
126
|
+
ignore_hidden: "false",
|
|
127
|
+
});
|
|
128
|
+
return packageStateFromListPayload(body, packageId);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function isPackageInstalled(packageId, profileName) {
|
|
135
|
+
const state = readInstalledPackageState(packageId, profileName);
|
|
136
|
+
return state?.installed === true;
|
|
137
|
+
}
|
|
138
|
+
function installStatusIsBroken(status) {
|
|
139
|
+
if (!status) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
return /(broken|error|fail|installing|150)/i.test(status);
|
|
143
|
+
}
|
|
144
|
+
export async function waitForInstalledVersion(readState, options) {
|
|
145
|
+
const deadline = Date.now() + (options.deadlineMs ?? 15 * 60 * 1000);
|
|
146
|
+
const pollMs = options.pollMs ?? 5000;
|
|
147
|
+
while (Date.now() < deadline) {
|
|
148
|
+
const state = await readState();
|
|
149
|
+
if (state?.installed && installStatusIsBroken(state.status)) {
|
|
150
|
+
throw new Error(`Package ${options.packageId} install did not complete (Package Center status ${state.status}). Check synopkg postinst / webservice acquire.`);
|
|
151
|
+
}
|
|
152
|
+
if (state?.installed) {
|
|
153
|
+
if (!options.expectedVersion || state.version === options.expectedVersion) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
158
|
+
}
|
|
159
|
+
throw new Error(options.expectedVersion
|
|
160
|
+
? `Timed out waiting for ${options.packageId} to reach version ${options.expectedVersion}`
|
|
161
|
+
: `Timed out waiting for ${options.packageId} to install from catalog`);
|
|
162
|
+
}
|
|
163
|
+
export function catalogInstallationRequests(entry, installed, sid, volumePath) {
|
|
164
|
+
const install = {
|
|
165
|
+
api: "SYNO.Core.Package.Installation",
|
|
166
|
+
version: "1",
|
|
167
|
+
method: "install",
|
|
168
|
+
name: jsonApiParam(entry.package),
|
|
169
|
+
is_syno: "false",
|
|
170
|
+
beta: String(Boolean(entry.beta)),
|
|
171
|
+
url: jsonApiParam(entry.link),
|
|
172
|
+
checksum: jsonApiParam(entry.md5),
|
|
173
|
+
filesize: String(entry.size),
|
|
174
|
+
type: "0",
|
|
175
|
+
blqinst: String(Boolean(entry.qinst)),
|
|
176
|
+
operation: jsonApiParam(installed ? "upgrade" : "install"),
|
|
177
|
+
_sid: sid,
|
|
178
|
+
};
|
|
179
|
+
if (volumePath) {
|
|
180
|
+
install.volume_path = jsonApiParam(volumePath);
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
check: {
|
|
184
|
+
api: "SYNO.Core.Package.Installation",
|
|
185
|
+
version: "2",
|
|
186
|
+
method: "check",
|
|
187
|
+
depsers: jsonApiParam(""),
|
|
188
|
+
deppkgs: jsonApiParam(normalizeDeppkgs(entry.deppkgs)),
|
|
189
|
+
conflictpkgs: jsonApiParam(null),
|
|
190
|
+
breakpkgs: jsonApiParam(entry.breakpkgs),
|
|
191
|
+
replacepkgs: jsonApiParam(entry.replacepkgs),
|
|
192
|
+
ver: jsonApiParam(entry.version),
|
|
193
|
+
size: String(entry.size),
|
|
194
|
+
id: jsonApiParam(entry.package),
|
|
195
|
+
blupgrade: installed ? "true" : "false",
|
|
196
|
+
install_type: jsonApiParam(""),
|
|
197
|
+
install_on_cold_storage: jsonApiParam(""),
|
|
198
|
+
blCheckDep: "true",
|
|
199
|
+
_sid: sid,
|
|
200
|
+
},
|
|
201
|
+
install,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
export function catalogCompoundSteps(uploaded, entry, options) {
|
|
205
|
+
return [
|
|
206
|
+
{
|
|
207
|
+
api: "SYNO.Core.Package.Installation",
|
|
208
|
+
method: "check",
|
|
209
|
+
version: 2,
|
|
210
|
+
id: uploaded.id,
|
|
211
|
+
ver: entry.version,
|
|
212
|
+
size: entry.size,
|
|
213
|
+
install_type: "",
|
|
214
|
+
install_on_cold_storage: false,
|
|
215
|
+
breakpkgs: null,
|
|
216
|
+
blCheckDep: true,
|
|
217
|
+
replacepkgs: null,
|
|
218
|
+
blupgrade: options.blupgrade,
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
api: "SYNO.Core.Package.Installation",
|
|
222
|
+
method: "install",
|
|
223
|
+
version: 1,
|
|
224
|
+
extra_values: JSON.stringify(options.extraValues ?? {}),
|
|
225
|
+
type: 0,
|
|
226
|
+
check_codesign: false,
|
|
227
|
+
force: false,
|
|
228
|
+
installrunpackage: true,
|
|
229
|
+
task_id: uploaded.taskId,
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
api: "SYNO.Core.Package",
|
|
233
|
+
method: "get",
|
|
234
|
+
version: 1,
|
|
235
|
+
id: uploaded.id,
|
|
236
|
+
additional: ["status", "dsm_apps"],
|
|
237
|
+
},
|
|
238
|
+
];
|
|
239
|
+
}
|
|
240
|
+
export function catalogUpgradeCompoundSteps(uploaded, entry, extraValues) {
|
|
241
|
+
return catalogCompoundSteps(uploaded, entry, { blupgrade: true, extraValues });
|
|
242
|
+
}
|
|
243
|
+
async function uploadLocalSpk(options, sid) {
|
|
244
|
+
const bytes = readFileSync(options.spkPath);
|
|
245
|
+
const form = new FormData();
|
|
246
|
+
form.append("api", "SYNO.Core.Package.Installation");
|
|
247
|
+
form.append("method", "upload");
|
|
248
|
+
form.append("version", "1");
|
|
249
|
+
form.append("_sid", sid);
|
|
250
|
+
form.append("file", new Blob([bytes]), `${options.packageId}.spk`);
|
|
251
|
+
const uploadResponse = await dsmFetch(`${options.dsmOrigin}/webapi/entry.cgi`, {
|
|
252
|
+
method: "POST",
|
|
253
|
+
headers: { cookie: options.cookieHeader },
|
|
254
|
+
body: form,
|
|
255
|
+
});
|
|
256
|
+
const body = (await uploadResponse.json());
|
|
257
|
+
if (!body.success || !body.data?.id || !body.data.task_id) {
|
|
258
|
+
throw new Error(`DSM SYNO.Core.Package.Installation/upload failed: ${JSON.stringify(body.error ?? body)}`);
|
|
259
|
+
}
|
|
260
|
+
return { id: body.data.id, taskId: body.data.task_id };
|
|
261
|
+
}
|
|
262
|
+
export async function installPackageFromLocalSpk(options) {
|
|
263
|
+
const sid = extractSid(options.cookieHeader);
|
|
264
|
+
const dsmState = await fetchInstalledPackageState(options.dsmOrigin, options.cookieHeader, options.packageId);
|
|
265
|
+
const installed = dsmState?.installed === true || isPackageInstalled(options.packageId, options.profileName);
|
|
266
|
+
const uploaded = await uploadLocalSpk(options, sid);
|
|
267
|
+
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, {
|
|
269
|
+
api: "SYNO.Entry.Request",
|
|
270
|
+
version: "1",
|
|
271
|
+
method: "request",
|
|
272
|
+
stop_when_error: "true",
|
|
273
|
+
mode: jsonApiParam("sequential"),
|
|
274
|
+
compound,
|
|
275
|
+
_sid: sid,
|
|
276
|
+
});
|
|
277
|
+
await waitForInstalledVersion(async () => (await fetchInstalledPackageState(options.dsmOrigin, options.cookieHeader, options.packageId)) ??
|
|
278
|
+
readInstalledPackageState(options.packageId, options.profileName), { packageId: options.packageId, expectedVersion: options.version });
|
|
279
|
+
}
|
|
280
|
+
async function uploadCatalogSpk(options, entry, sid) {
|
|
281
|
+
const response = await dsmFetch(entry.link);
|
|
282
|
+
if (!response.ok) {
|
|
283
|
+
throw new Error(`Failed to download catalog SPK for ${entry.package}: HTTP ${response.status}`);
|
|
284
|
+
}
|
|
285
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
286
|
+
const form = new FormData();
|
|
287
|
+
form.append("api", "SYNO.Core.Package.Installation");
|
|
288
|
+
form.append("method", "upload");
|
|
289
|
+
form.append("version", "1");
|
|
290
|
+
form.append("_sid", sid);
|
|
291
|
+
form.append("file", new Blob([bytes]), `${entry.package}.spk`);
|
|
292
|
+
const uploadResponse = await dsmFetch(`${options.dsmOrigin}/webapi/entry.cgi`, {
|
|
293
|
+
method: "POST",
|
|
294
|
+
headers: { cookie: options.cookieHeader },
|
|
295
|
+
body: form,
|
|
296
|
+
});
|
|
297
|
+
const body = (await uploadResponse.json());
|
|
298
|
+
if (!body.success || !body.data?.id || !body.data.task_id) {
|
|
299
|
+
throw new Error(`DSM SYNO.Core.Package.Installation/upload failed: ${JSON.stringify(body.error ?? body)}`);
|
|
300
|
+
}
|
|
301
|
+
return { id: body.data.id, taskId: body.data.task_id };
|
|
302
|
+
}
|
|
303
|
+
async function installCatalogViaUpload(options, entry, sid, installed) {
|
|
304
|
+
const uploaded = await uploadCatalogSpk(options, entry, sid);
|
|
305
|
+
const compound = JSON.stringify(catalogCompoundSteps(uploaded, entry, { blupgrade: installed, extraValues: options.extraValues }));
|
|
306
|
+
await dsmWebApi(options.dsmOrigin, options.cookieHeader, {
|
|
307
|
+
api: "SYNO.Entry.Request",
|
|
308
|
+
version: "1",
|
|
309
|
+
method: "request",
|
|
310
|
+
stop_when_error: "true",
|
|
311
|
+
mode: jsonApiParam("sequential"),
|
|
312
|
+
compound,
|
|
313
|
+
_sid: sid,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
export async function installPackageFromCatalog(options) {
|
|
317
|
+
const entry = options.expectedVersion
|
|
318
|
+
? await waitForCatalogEntry(options, options.expectedVersion)
|
|
319
|
+
: await fetchCatalogEntry(options);
|
|
320
|
+
const sid = extractSid(options.cookieHeader);
|
|
321
|
+
const dsmState = await fetchInstalledPackageState(options.dsmOrigin, options.cookieHeader, options.packageId);
|
|
322
|
+
const installed = dsmState?.installed === true || isPackageInstalled(options.packageId, options.profileName);
|
|
323
|
+
await installCatalogViaUpload(options, entry, sid, installed);
|
|
324
|
+
await waitForInstalledVersion(async () => (await fetchInstalledPackageState(options.dsmOrigin, options.cookieHeader, options.packageId)) ??
|
|
325
|
+
readInstalledPackageState(options.packageId, options.profileName), { packageId: options.packageId, expectedVersion: entry.version });
|
|
326
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
export function resolveHostCredentials(hostName) {
|
|
5
|
+
const settingsPath = join(homedir(), ".synoagentcli", "setting.json");
|
|
6
|
+
let settings;
|
|
7
|
+
try {
|
|
8
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return fromEnv();
|
|
12
|
+
}
|
|
13
|
+
const selected = hostName ?? settings.defaultHost ?? "default";
|
|
14
|
+
const profile = settings.hosts?.[selected] ?? settings.hosts?.[settings.defaultHost ?? "default"];
|
|
15
|
+
const username = profile?.username ?? profile?.auth?.username;
|
|
16
|
+
const password = profile?.password ?? profile?.auth?.password;
|
|
17
|
+
if (profile?.url && username && password) {
|
|
18
|
+
return {
|
|
19
|
+
dsmOrigin: profile.url,
|
|
20
|
+
username,
|
|
21
|
+
password,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (profile?.url) {
|
|
25
|
+
return fromEnv(profile.url);
|
|
26
|
+
}
|
|
27
|
+
return fromEnv();
|
|
28
|
+
}
|
|
29
|
+
function fromEnv(urlOverride) {
|
|
30
|
+
const dsmOrigin = urlOverride ?? process.env.DSM_URL;
|
|
31
|
+
const password = process.env.DSM_PASSWORD;
|
|
32
|
+
const username = process.env.DSM_USERNAME ?? "admin";
|
|
33
|
+
if (!dsmOrigin || !password) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return { dsmOrigin, username, password };
|
|
37
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ProjectRoot } from "../discovery.js";
|
|
2
|
+
export declare function digestString(value: string): string;
|
|
3
|
+
export declare function digestDirectory(root: string): string;
|
|
4
|
+
export declare function digestProjectContract(project: ProjectRoot): string;
|
|
5
|
+
export declare function digestProjectBundle(project: ProjectRoot): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
export function digestString(value) {
|
|
6
|
+
return createHash("sha256").update(value).digest("hex");
|
|
7
|
+
}
|
|
8
|
+
export function digestDirectory(root) {
|
|
9
|
+
const hash = createHash("sha256");
|
|
10
|
+
for (const relative of walkFiles(root)) {
|
|
11
|
+
hash.update(relative);
|
|
12
|
+
hash.update("\0");
|
|
13
|
+
hash.update(readFileSync(join(root, relative)));
|
|
14
|
+
hash.update("\0");
|
|
15
|
+
}
|
|
16
|
+
return hash.digest("hex");
|
|
17
|
+
}
|
|
18
|
+
export function digestProjectContract(project) {
|
|
19
|
+
return digestString(readFileSync(project.contractPath, "utf8"));
|
|
20
|
+
}
|
|
21
|
+
export function digestProjectBundle(project) {
|
|
22
|
+
const staging = mkdtempSync(join(tmpdir(), "syno-bundle-digest-"));
|
|
23
|
+
try {
|
|
24
|
+
const packageDir = join(staging, "package");
|
|
25
|
+
mkdirSync(packageDir, { recursive: true });
|
|
26
|
+
cpSync(project.composePath, join(packageDir, "compose.yaml"));
|
|
27
|
+
cpSync(project.contractPath, join(packageDir, "synology-app.yaml"));
|
|
28
|
+
const htmlDir = join(project.root, "html");
|
|
29
|
+
if (existsSync(htmlDir)) {
|
|
30
|
+
cpSync(htmlDir, join(packageDir, "html"), { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
return digestDirectory(packageDir);
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
rmSync(staging, { recursive: true, force: true });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function walkFiles(root, prefix = "") {
|
|
39
|
+
const files = [];
|
|
40
|
+
for (const entry of readdirSync(root).sort(comparePosixPaths)) {
|
|
41
|
+
const relative = prefix ? `${prefix}/${entry}` : entry;
|
|
42
|
+
const absolute = join(root, entry);
|
|
43
|
+
const stat = statSync(absolute);
|
|
44
|
+
if (stat.isDirectory()) {
|
|
45
|
+
files.push(...walkFiles(absolute, relative));
|
|
46
|
+
}
|
|
47
|
+
else if (stat.isFile()) {
|
|
48
|
+
files.push(relative);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return files;
|
|
52
|
+
}
|
|
53
|
+
function comparePosixPaths(left, right) {
|
|
54
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
55
|
+
}
|