@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,653 @@
|
|
|
1
|
+
import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import * as tar from "tar";
|
|
6
|
+
import { loadContract, collectInstallParameters } from "../contract/load.js";
|
|
7
|
+
import { renderDsmWindowConfig, resolvedWebLaunch, trustedAppEntryPath, } from "../contract/web-launch.js";
|
|
8
|
+
import { loadCompose } from "../compose/analyze.js";
|
|
9
|
+
import { scanComposePrivilege } from "../compose/privilege.js";
|
|
10
|
+
import { validateProject } from "../validate.js";
|
|
11
|
+
import { writeDsmWindowIcons, writePackageCenterIcons } from "./dsm-icons.js";
|
|
12
|
+
import { infoArchValue } from "./dsm-arch.js";
|
|
13
|
+
import { createReproducibleTar } from "./reproducible-archive.js";
|
|
14
|
+
const FILE_MODE = 0o644;
|
|
15
|
+
const SCRIPT_MODE = 0o755;
|
|
16
|
+
const INSTALL_UIFILE = "install_uifile";
|
|
17
|
+
const UPGRADE_UIFILE = "upgrade_uifile";
|
|
18
|
+
const UNINSTALL_UIFILE = "uninstall_uifile";
|
|
19
|
+
export const EMBEDDED_ATTESTATION_RELATIVE_PATH = ".synopkgland/attestation.jwt";
|
|
20
|
+
const SCRIPT_NAMES = [
|
|
21
|
+
"preinst",
|
|
22
|
+
"postinst",
|
|
23
|
+
"preuninst",
|
|
24
|
+
"postuninst",
|
|
25
|
+
"preupgrade",
|
|
26
|
+
"postupgrade",
|
|
27
|
+
"start-stop-status",
|
|
28
|
+
];
|
|
29
|
+
export async function buildSpk(options) {
|
|
30
|
+
if (options.embeddedAttestationToken !== undefined && options.embeddedAttestationToken.length === 0) {
|
|
31
|
+
return { ok: false, errors: ["EMBEDDED_ATTESTATION_TOKEN_EMPTY"] };
|
|
32
|
+
}
|
|
33
|
+
const validation = validateProject(options.project);
|
|
34
|
+
if (!validation.ok) {
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
errors: validation.errors.map((e) => e.code),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const parsed = loadContract(options.project.contractPath);
|
|
41
|
+
const contract = { ...parsed.contract };
|
|
42
|
+
if (options.packageIdentity) {
|
|
43
|
+
contract.package = options.packageIdentity;
|
|
44
|
+
}
|
|
45
|
+
if (options.displayname) {
|
|
46
|
+
contract.displayname = options.displayname;
|
|
47
|
+
}
|
|
48
|
+
if (options.spkVersion) {
|
|
49
|
+
contract.version = options.spkVersion;
|
|
50
|
+
}
|
|
51
|
+
const staging = mkdtempSync(join(tmpdir(), "syno-spk-"));
|
|
52
|
+
try {
|
|
53
|
+
writeFileSync(join(staging, "INFO"), renderInfo(contract, options.archFamily), "utf8");
|
|
54
|
+
writePackageCenterIcons(staging);
|
|
55
|
+
mkdirSync(join(staging, "conf"), { recursive: true });
|
|
56
|
+
writeFileSync(join(staging, "conf", "privilege"), renderPrivilege(), "utf8");
|
|
57
|
+
writeFileSync(join(staging, "privilege"), renderPrivilege(), "utf8");
|
|
58
|
+
mkdirSync(join(staging, "WIZARD_UIFILES"), { recursive: true });
|
|
59
|
+
const parsedCompose = loadCompose(options.project.composePath);
|
|
60
|
+
writeFileSync(join(staging, "WIZARD_UIFILES", INSTALL_UIFILE), JSON.stringify(renderInstallWizard(contract, parsedCompose?.compose), null, 2), "utf8");
|
|
61
|
+
writeFileSync(join(staging, "WIZARD_UIFILES", UPGRADE_UIFILE), JSON.stringify(renderUpgradeWizard(contract, parsedCompose?.compose), null, 2), "utf8");
|
|
62
|
+
writeFileSync(join(staging, "WIZARD_UIFILES", UNINSTALL_UIFILE), JSON.stringify(renderUninstallWizard(), null, 2), "utf8");
|
|
63
|
+
mkdirSync(join(staging, "scripts"), { recursive: true });
|
|
64
|
+
for (const name of SCRIPT_NAMES) {
|
|
65
|
+
const scriptPath = join(staging, "scripts", name);
|
|
66
|
+
writeFileSync(scriptPath, renderManagedScript(name, contract), "utf8");
|
|
67
|
+
chmodSync(scriptPath, SCRIPT_MODE);
|
|
68
|
+
}
|
|
69
|
+
mkdirSync(join(staging, "package"), { recursive: true });
|
|
70
|
+
copyFileSync(options.project.composePath, join(staging, "package", "compose.yaml"));
|
|
71
|
+
copyFileSync(options.project.contractPath, join(staging, "package", "synology-app.yaml"));
|
|
72
|
+
const htmlDir = join(options.project.root, "html");
|
|
73
|
+
if (existsSync(htmlDir)) {
|
|
74
|
+
cpSync(htmlDir, join(staging, "package", "html"), { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
if (options.embeddedAttestationToken !== undefined) {
|
|
77
|
+
const attestationPath = join(staging, "package", EMBEDDED_ATTESTATION_RELATIVE_PATH);
|
|
78
|
+
mkdirSync(join(staging, "package", ".synopkgland"), { recursive: true });
|
|
79
|
+
writeFileSync(attestationPath, options.embeddedAttestationToken, "utf8");
|
|
80
|
+
chmodSync(attestationPath, FILE_MODE);
|
|
81
|
+
}
|
|
82
|
+
if (contract.adminport !== undefined && resolvedWebLaunch(contract) === "dsm-window") {
|
|
83
|
+
const uiDir = join(staging, "package", "ui");
|
|
84
|
+
mkdirSync(uiDir, { recursive: true });
|
|
85
|
+
writeFileSync(join(uiDir, "config"), renderDsmWindowConfig(contract), "utf8");
|
|
86
|
+
writeDsmWindowIcons(uiDir);
|
|
87
|
+
}
|
|
88
|
+
chmodSync(join(staging, "INFO"), FILE_MODE);
|
|
89
|
+
chmodSync(join(staging, "privilege"), FILE_MODE);
|
|
90
|
+
chmodSync(join(staging, "WIZARD_UIFILES", INSTALL_UIFILE), FILE_MODE);
|
|
91
|
+
chmodSync(join(staging, "WIZARD_UIFILES", UPGRADE_UIFILE), FILE_MODE);
|
|
92
|
+
chmodSync(join(staging, "WIZARD_UIFILES", UNINSTALL_UIFILE), FILE_MODE);
|
|
93
|
+
chmodSync(join(staging, "package", "compose.yaml"), FILE_MODE);
|
|
94
|
+
chmodSync(join(staging, "package", "synology-app.yaml"), FILE_MODE);
|
|
95
|
+
if (contract.adminport !== undefined && resolvedWebLaunch(contract) === "dsm-window") {
|
|
96
|
+
chmodSync(join(staging, "package", "ui", "config"), FILE_MODE);
|
|
97
|
+
}
|
|
98
|
+
const outputPath = options.outputPath ?? join(options.project.root, `${String(contract.package)}.spk`);
|
|
99
|
+
await createDeterministicSpk(staging, outputPath);
|
|
100
|
+
return { ok: true, outputPath, errors: [] };
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
rmSync(staging, { recursive: true, force: true });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function quoteInfo(value) {
|
|
107
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
108
|
+
}
|
|
109
|
+
function spkVersion(version) {
|
|
110
|
+
return /-\d+$/.test(version) ? version : `${version}-0001`;
|
|
111
|
+
}
|
|
112
|
+
function renderInfo(contract, archFamily) {
|
|
113
|
+
const lines = [];
|
|
114
|
+
const add = (key, value) => {
|
|
115
|
+
lines.push(`${key}=${quoteInfo(String(value))}`);
|
|
116
|
+
};
|
|
117
|
+
add("package", String(contract.package));
|
|
118
|
+
add("version", spkVersion(String(contract.version)));
|
|
119
|
+
add("displayname", String(contract.displayname));
|
|
120
|
+
add("description", String(contract.description));
|
|
121
|
+
add("maintainer", String(contract.maintainer));
|
|
122
|
+
add("arch", archFamily === undefined ? "noarch" : infoArchValue(archFamily));
|
|
123
|
+
add("os_min_ver", String(contract.os_min_ver));
|
|
124
|
+
// synopkg's Container Manager dep gate reports "not ready" even when CM is
|
|
125
|
+
// running. Depend on Broker only; Broker's adapter.probe() checks CM at
|
|
126
|
+
// App install time (CM_CAPABILITY_MISSING).
|
|
127
|
+
add("install_dep_packages", "SynoPkgLandService>=0.1.0-0016");
|
|
128
|
+
add("startable", "yes");
|
|
129
|
+
const launch = resolvedWebLaunch(contract);
|
|
130
|
+
if (contract.adminprotocol) {
|
|
131
|
+
add("adminprotocol", String(contract.adminprotocol));
|
|
132
|
+
}
|
|
133
|
+
if (launch === "new-page" || launch === "dsm-window") {
|
|
134
|
+
// Package Center Open uses INFO.adminurl as a same-host path. Do not emit
|
|
135
|
+
// adminport — that would open the Compose published port (ADR-0053/0062).
|
|
136
|
+
add("adminurl", trustedAppEntryPath(contract));
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
if (contract.adminport !== undefined) {
|
|
140
|
+
add("adminport", Number(contract.adminport));
|
|
141
|
+
// DSM checkport=yes treats INFO.adminport as exclusive. Compose also binds
|
|
142
|
+
// that host port, so a matching adminport yields synopkg 283 — including
|
|
143
|
+
// when the same App's container already holds the port. ADR-0053: the
|
|
144
|
+
// published port is the Web Station upstream, not Package Center Open.
|
|
145
|
+
add("checkport", "no");
|
|
146
|
+
}
|
|
147
|
+
if (contract.adminurl) {
|
|
148
|
+
add("adminurl", String(contract.adminurl));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (launch === "dsm-window") {
|
|
152
|
+
add("dsmuidir", "ui");
|
|
153
|
+
add("dsmappname", String(contract.package));
|
|
154
|
+
}
|
|
155
|
+
return `${lines.join("\n")}\n`;
|
|
156
|
+
}
|
|
157
|
+
function renderPrivilege() {
|
|
158
|
+
return `${JSON.stringify({
|
|
159
|
+
defaults: {
|
|
160
|
+
"run-as": "package",
|
|
161
|
+
},
|
|
162
|
+
}, null, 2)}\n`;
|
|
163
|
+
}
|
|
164
|
+
const SHARED_FOLDER_MOUNT_CONSENT = "此 App 會用你在這台 NAS 選的資料夾;整顆應用都看得到。Shared Folder Mount 是整個 App Instance 的 container access:能打開這個 App 的使用者,容器裡的程式都看得到該 Share,即使該使用者在 File Station 看不到。這不是逐使用者 DSM ACL。要依登入者權限讀寫 NAS 檔案,必須另外核准廣域 files.* 並走 File Gateway。";
|
|
165
|
+
const DSM_USER_RUN_AS_CONSENT = "寫進 Share 的新檔在 File Station 顯示為你選的那位 DSM 使用者。點開此 App 的人共用這個磁碟身分,不是逐登入 ACL。";
|
|
166
|
+
const DSM_USER_UPGRADE_CONSENT = "升級時更換 Container Run-as 不會 chown Share 上的舊檔;只有升級後寫入的新檔才會使用新的磁碟身分。";
|
|
167
|
+
const SHARED_FOLDER_RW_UPGRADE_CONSENT = "此版本把 Share 掛載改為讀寫;容器可讀取並寫入你選的 Share,不再是唯讀。";
|
|
168
|
+
const APP_OWN_SPACE_CONSENT = "此 App 只用 App 自己的空間。";
|
|
169
|
+
const OIDC_COMPAT_CONSENT = "此 App 可用你的 DSM 帳號登入;如果這台 NAS 尚未啟用相容模式,App 會使用自己的登入。登入不等於 File Station ACL。";
|
|
170
|
+
function hasOidcCompat(contract) {
|
|
171
|
+
const integration = contract.integration;
|
|
172
|
+
if (!integration || typeof integration !== "object") {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
const identity = integration.identity;
|
|
176
|
+
if (!identity || typeof identity !== "object") {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
const oidcCompat = identity.oidc_compat;
|
|
180
|
+
return Boolean(oidcCompat && typeof oidcCompat === "object");
|
|
181
|
+
}
|
|
182
|
+
function renderFilesCapability(contract) {
|
|
183
|
+
const permissions = contract.permissions;
|
|
184
|
+
const files = permissions?.files;
|
|
185
|
+
if (!files) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
const parts = [];
|
|
189
|
+
if (files.read) {
|
|
190
|
+
parts.push("讀取檔案");
|
|
191
|
+
}
|
|
192
|
+
if (files.write) {
|
|
193
|
+
parts.push("寫入檔案");
|
|
194
|
+
}
|
|
195
|
+
if (files.delete) {
|
|
196
|
+
parts.push("刪除檔案");
|
|
197
|
+
}
|
|
198
|
+
if (parts.length === 0) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
return `此 App 需要以下檔案能力:${parts.join("、")}。這是每位登入者的 DSM ACL,經 File Gateway。`;
|
|
202
|
+
}
|
|
203
|
+
function volumeSources(compose) {
|
|
204
|
+
return volumeMounts(compose).map((mount) => mount.source);
|
|
205
|
+
}
|
|
206
|
+
function volumeMounts(compose) {
|
|
207
|
+
const mounts = [];
|
|
208
|
+
for (const service of Object.values(compose.services ?? {})) {
|
|
209
|
+
const volumes = service.volumes;
|
|
210
|
+
if (!volumes) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const entries = Array.isArray(volumes) ? volumes : [volumes];
|
|
214
|
+
for (const entry of entries) {
|
|
215
|
+
if (typeof entry === "string") {
|
|
216
|
+
const parts = entry.split(":");
|
|
217
|
+
const source = parts[0]?.trim() ?? "";
|
|
218
|
+
if (source) {
|
|
219
|
+
const modes = parts
|
|
220
|
+
.slice(2)
|
|
221
|
+
.flatMap((part) => part.split(","))
|
|
222
|
+
.map((part) => part.trim());
|
|
223
|
+
mounts.push({ source, readOnly: modes.includes("ro") });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
else if (entry && typeof entry === "object" && "source" in entry) {
|
|
227
|
+
const record = entry;
|
|
228
|
+
const source = String(record.source ?? "").trim();
|
|
229
|
+
if (source) {
|
|
230
|
+
mounts.push({ source, readOnly: record.read_only === true });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return mounts;
|
|
236
|
+
}
|
|
237
|
+
function composeUsesOnlyAppSpace(compose) {
|
|
238
|
+
const sources = volumeSources(compose);
|
|
239
|
+
if (sources.length === 0) {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
return sources.every((source) => source.startsWith("${SYNOPKG_PKGVAR}") || source.startsWith("${SYNO_APP_SECRETS_DIR}"));
|
|
243
|
+
}
|
|
244
|
+
const PRIVILEGE_MAX_WARNING = "此 App 的容器有高權限,接近這台 NAS 的 root、幾乎整台機器。是否安裝由你這位管理員決定。";
|
|
245
|
+
function renderInstallWizard(contract, compose) {
|
|
246
|
+
const items = [];
|
|
247
|
+
const params = collectInstallParameters(contract);
|
|
248
|
+
const hasSharedFolder = params.some((item) => item.param.type === "shared_folder");
|
|
249
|
+
const hasDsmUser = params.some((item) => item.param.type === "dsm_user");
|
|
250
|
+
const capabilityText = renderFilesCapability(contract);
|
|
251
|
+
if (compose && scanComposePrivilege(compose).length > 0) {
|
|
252
|
+
items.push({
|
|
253
|
+
desc: PRIVILEGE_MAX_WARNING,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
if (hasOidcCompat(contract)) {
|
|
257
|
+
items.push({
|
|
258
|
+
desc: OIDC_COMPAT_CONSENT,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
if (hasSharedFolder) {
|
|
262
|
+
items.push({
|
|
263
|
+
desc: SHARED_FOLDER_MOUNT_CONSENT,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
if (hasDsmUser) {
|
|
267
|
+
items.push({
|
|
268
|
+
desc: DSM_USER_RUN_AS_CONSENT,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
if (capabilityText) {
|
|
272
|
+
items.push({
|
|
273
|
+
desc: capabilityText,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
if (!hasSharedFolder && !capabilityText && compose && composeUsesOnlyAppSpace(compose)) {
|
|
277
|
+
items.push({
|
|
278
|
+
desc: APP_OWN_SPACE_CONSENT,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
for (const { name, param } of params) {
|
|
282
|
+
if (param.type === "secret" && param.generate === true) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (param.type === "string") {
|
|
286
|
+
items.push({
|
|
287
|
+
type: "textfield",
|
|
288
|
+
desc: `Install Parameter: ${name}`,
|
|
289
|
+
subitems: [{
|
|
290
|
+
key: name,
|
|
291
|
+
desc: name,
|
|
292
|
+
defaultValue: param.default ?? "",
|
|
293
|
+
validator: { allowBlank: false },
|
|
294
|
+
}],
|
|
295
|
+
});
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (param.type === "boolean") {
|
|
299
|
+
items.push({
|
|
300
|
+
type: "multiselect",
|
|
301
|
+
desc: `Install Parameter: ${name}`,
|
|
302
|
+
subitems: [{ key: name, desc: name, defaultValue: param.default ?? false }],
|
|
303
|
+
});
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (param.type === "secret") {
|
|
307
|
+
items.push({
|
|
308
|
+
type: "password",
|
|
309
|
+
desc: `Install Parameter: ${name}`,
|
|
310
|
+
subitems: [{ key: name, desc: name, validator: { allowBlank: false } }],
|
|
311
|
+
});
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (param.type === "shared_folder") {
|
|
315
|
+
items.push({
|
|
316
|
+
type: "combobox",
|
|
317
|
+
desc: `Shared Folder: ${name}`,
|
|
318
|
+
subitems: [{
|
|
319
|
+
key: name,
|
|
320
|
+
desc: name,
|
|
321
|
+
displayField: "name",
|
|
322
|
+
valueField: "name",
|
|
323
|
+
editable: false,
|
|
324
|
+
forceSelection: true,
|
|
325
|
+
autoSelect: false,
|
|
326
|
+
mode: "remote",
|
|
327
|
+
api_store: {
|
|
328
|
+
api: "SYNO.Core.Share",
|
|
329
|
+
method: "list",
|
|
330
|
+
version: 1,
|
|
331
|
+
baseParams: { limit: -1, offset: 0 },
|
|
332
|
+
root: "shares",
|
|
333
|
+
idProperty: "name",
|
|
334
|
+
fields: ["name"],
|
|
335
|
+
},
|
|
336
|
+
validator: { allowBlank: false },
|
|
337
|
+
}],
|
|
338
|
+
});
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (param.type === "dsm_user") {
|
|
342
|
+
items.push({
|
|
343
|
+
type: "combobox",
|
|
344
|
+
desc: `Container Run-as: ${name}`,
|
|
345
|
+
subitems: [{
|
|
346
|
+
key: name,
|
|
347
|
+
desc: name,
|
|
348
|
+
displayField: "name",
|
|
349
|
+
valueField: "name",
|
|
350
|
+
editable: false,
|
|
351
|
+
forceSelection: true,
|
|
352
|
+
autoSelect: false,
|
|
353
|
+
mode: "remote",
|
|
354
|
+
api_store: {
|
|
355
|
+
api: "SYNO.Core.User",
|
|
356
|
+
method: "list",
|
|
357
|
+
version: 1,
|
|
358
|
+
baseParams: { limit: -1, offset: 0 },
|
|
359
|
+
root: "users",
|
|
360
|
+
idProperty: "name",
|
|
361
|
+
fields: ["name"],
|
|
362
|
+
},
|
|
363
|
+
validator: { allowBlank: false },
|
|
364
|
+
}],
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return [{
|
|
369
|
+
step_title: "安裝設定",
|
|
370
|
+
items,
|
|
371
|
+
}];
|
|
372
|
+
}
|
|
373
|
+
function renderUpgradeWizard(contract, compose) {
|
|
374
|
+
const installWizard = renderInstallWizard(contract, compose);
|
|
375
|
+
const params = collectInstallParameters(contract);
|
|
376
|
+
const sharedFolderNames = params
|
|
377
|
+
.filter((item) => item.param.type === "shared_folder")
|
|
378
|
+
.map((item) => item.name);
|
|
379
|
+
const hasSharedFolder = sharedFolderNames.length > 0;
|
|
380
|
+
const hasDsmUser = params.some((item) => item.param.type === "dsm_user");
|
|
381
|
+
const hasReadWriteSharedFolder = hasSharedFolder &&
|
|
382
|
+
compose !== undefined &&
|
|
383
|
+
volumeMounts(compose).some((mount) => !mount.readOnly &&
|
|
384
|
+
sharedFolderNames.some((name) => mount.source === `\${${name}}` || mount.source.startsWith(`\${${name}}/`)));
|
|
385
|
+
const upgradeConsent = [];
|
|
386
|
+
if (hasReadWriteSharedFolder) {
|
|
387
|
+
upgradeConsent.push({
|
|
388
|
+
desc: SHARED_FOLDER_RW_UPGRADE_CONSENT,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
if (hasDsmUser && hasSharedFolder) {
|
|
392
|
+
upgradeConsent.push({
|
|
393
|
+
desc: DSM_USER_UPGRADE_CONSENT,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
if (upgradeConsent.length === 0) {
|
|
397
|
+
return [{
|
|
398
|
+
...installWizard[0],
|
|
399
|
+
step_title: "升級設定",
|
|
400
|
+
}];
|
|
401
|
+
}
|
|
402
|
+
const firstControl = installWizard[0].items.findIndex((item) => item.subitems !== undefined);
|
|
403
|
+
const insertAt = firstControl === -1 ? installWizard[0].items.length : firstControl;
|
|
404
|
+
const items = [...installWizard[0].items];
|
|
405
|
+
items.splice(insertAt, 0, ...upgradeConsent);
|
|
406
|
+
return [{
|
|
407
|
+
...installWizard[0],
|
|
408
|
+
step_title: "升級設定",
|
|
409
|
+
items,
|
|
410
|
+
}];
|
|
411
|
+
}
|
|
412
|
+
function renderUninstallWizard() {
|
|
413
|
+
return [{
|
|
414
|
+
step_title: "解除安裝",
|
|
415
|
+
items: [
|
|
416
|
+
{
|
|
417
|
+
desc: "勾選 Delete App Data 會永久刪除 App data 與 secrets,但不刪 Shared Folder 或 named volume。平台沒有檢查 Hyper Backup,也未驗證可復原備份。",
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
type: "multiselect",
|
|
421
|
+
desc: "Delete App Data",
|
|
422
|
+
subitems: [
|
|
423
|
+
{
|
|
424
|
+
key: "delete_app_data",
|
|
425
|
+
desc: "Delete App Data",
|
|
426
|
+
defaultValue: false,
|
|
427
|
+
},
|
|
428
|
+
],
|
|
429
|
+
},
|
|
430
|
+
],
|
|
431
|
+
}];
|
|
432
|
+
}
|
|
433
|
+
function renderManagedScript(name, contract) {
|
|
434
|
+
if (name === "start-stop-status") {
|
|
435
|
+
return `#!/bin/sh
|
|
436
|
+
INSTALLER="\${BROKER_INSTALLER:-/var/packages/SynoPkgLandService/target/bin/synopkgland-installer}"
|
|
437
|
+
SOCKET="\${BROKER_SOCKET:-/var/packages/SynoPkgLandService/var/installer.sock}"
|
|
438
|
+
case "$1" in
|
|
439
|
+
start) exit 0 ;;
|
|
440
|
+
stop) exit 0 ;;
|
|
441
|
+
status)
|
|
442
|
+
if [ -x "$INSTALLER" ] && [ -S "$SOCKET" ]; then
|
|
443
|
+
"$INSTALLER" --socket "$SOCKET" --phase status --attempt-id "status-$$" \\
|
|
444
|
+
--package-identity "${contract.package}" --package-version "\${SYNOPKG_PKGVER}" \\
|
|
445
|
+
--package-path "\${SYNOPKG_PKGDEST}" --bundle-path "\${SYNOPKG_PKGDEST}" \\
|
|
446
|
+
--pkg-var "\${SYNOPKG_PKGVAR}" --pkg-home "\${SYNOPKG_PKGHOME}" >/tmp/synopkgland-status.$$ 2>/dev/null || true
|
|
447
|
+
if grep -q '"registrationStatus":"removed"' /tmp/synopkgland-status.$$ 2>/dev/null; then
|
|
448
|
+
rm -f /tmp/synopkgland-status.$$
|
|
449
|
+
exit 150
|
|
450
|
+
fi
|
|
451
|
+
if grep -q '"registrationStatus":"unknown"' /tmp/synopkgland-status.$$ 2>/dev/null; then
|
|
452
|
+
rm -f /tmp/synopkgland-status.$$
|
|
453
|
+
exit 4
|
|
454
|
+
fi
|
|
455
|
+
rm -f /tmp/synopkgland-status.$$
|
|
456
|
+
fi
|
|
457
|
+
exit 0
|
|
458
|
+
;;
|
|
459
|
+
*) exit 1 ;;
|
|
460
|
+
esac
|
|
461
|
+
`;
|
|
462
|
+
}
|
|
463
|
+
if (name === "preinst" || name === "preupgrade") {
|
|
464
|
+
return `#!/bin/sh
|
|
465
|
+
SOCKET="\${BROKER_SOCKET:-/var/packages/SynoPkgLandService/var/installer.sock}"
|
|
466
|
+
if [ ! -S "$SOCKET" ]; then
|
|
467
|
+
echo "SynoPkgLandService installer socket is not available" >&2
|
|
468
|
+
exit 1
|
|
469
|
+
fi
|
|
470
|
+
exit 0
|
|
471
|
+
`;
|
|
472
|
+
}
|
|
473
|
+
if (name === "postinst" || name === "postupgrade") {
|
|
474
|
+
return renderInstallerCallScript(contract, name === "postupgrade" ? "update" : "install");
|
|
475
|
+
}
|
|
476
|
+
if (name === "preuninst") {
|
|
477
|
+
return renderInstallerCallScript(contract, "uninstall").replace("set -eu\n", 'set -eu\nif [ "${SYNOPKG_PKG_STATUS:-}" = "UPGRADE" ]; then\n exit 0\nfi\n');
|
|
478
|
+
}
|
|
479
|
+
if (name === "postuninst") {
|
|
480
|
+
return `#!/bin/sh
|
|
481
|
+
set -eu
|
|
482
|
+
PKGVAR="\${SYNOPKG_PKGVAR}"
|
|
483
|
+
PKGHOME="\${SYNOPKG_PKGHOME}"
|
|
484
|
+
SECRETS="\${PKGHOME}/secrets"
|
|
485
|
+
should_delete=0
|
|
486
|
+
case "\${delete_app_data:-}" in
|
|
487
|
+
true|True|TRUE|yes|1) should_delete=1 ;;
|
|
488
|
+
esac
|
|
489
|
+
case "\${wizard_delete_app_data:-}" in
|
|
490
|
+
true|True|TRUE|yes|1) should_delete=1 ;;
|
|
491
|
+
esac
|
|
492
|
+
if [ "$should_delete" -eq 1 ]; then
|
|
493
|
+
delete_root() {
|
|
494
|
+
target="$1"
|
|
495
|
+
if [ ! -e "$target" ] && [ ! -L "$target" ]; then
|
|
496
|
+
return 0
|
|
497
|
+
fi
|
|
498
|
+
if [ -L "$target" ]; then
|
|
499
|
+
real=$(readlink -f "$target" 2>/dev/null || true)
|
|
500
|
+
case "$real" in
|
|
501
|
+
*/@appdata/*|*/@apphome/*)
|
|
502
|
+
;;
|
|
503
|
+
*)
|
|
504
|
+
echo "Delete App Data skipped symlink: $target" >&2
|
|
505
|
+
return 0
|
|
506
|
+
;;
|
|
507
|
+
esac
|
|
508
|
+
else
|
|
509
|
+
real="$target"
|
|
510
|
+
fi
|
|
511
|
+
if [ ! -e "$real" ]; then
|
|
512
|
+
return 0
|
|
513
|
+
fi
|
|
514
|
+
chmod -R u+w "$real" 2>/dev/null || true
|
|
515
|
+
if [ -d "$real" ]; then
|
|
516
|
+
find "$real" -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null || true
|
|
517
|
+
leftover=$(find "$real" -mindepth 1 -maxdepth 1 2>/dev/null | head -n 1 || true)
|
|
518
|
+
if [ -n "$leftover" ]; then
|
|
519
|
+
echo "Delete App Data remnant: $real" >&2
|
|
520
|
+
fi
|
|
521
|
+
else
|
|
522
|
+
rm -f "$real" || echo "Delete App Data remnant: $real" >&2
|
|
523
|
+
fi
|
|
524
|
+
}
|
|
525
|
+
delete_root "$PKGVAR"
|
|
526
|
+
delete_root "$SECRETS"
|
|
527
|
+
fi
|
|
528
|
+
exit 0
|
|
529
|
+
`;
|
|
530
|
+
}
|
|
531
|
+
return `#!/bin/sh
|
|
532
|
+
exit 0
|
|
533
|
+
`;
|
|
534
|
+
}
|
|
535
|
+
function renderInstallerCallScript(contract, phase) {
|
|
536
|
+
const params = collectInstallParameters(contract);
|
|
537
|
+
const secrets = params.filter((item) => item.param.type === "secret");
|
|
538
|
+
const nonSecrets = params.filter((item) => item.param.type !== "secret");
|
|
539
|
+
const lines = [
|
|
540
|
+
"#!/bin/sh",
|
|
541
|
+
"set -eu",
|
|
542
|
+
'INSTALLER="${BROKER_INSTALLER:-/var/packages/SynoPkgLandService/target/bin/synopkgland-installer}"',
|
|
543
|
+
'SOCKET="${BROKER_SOCKET:-/var/packages/SynoPkgLandService/var/installer.sock}"',
|
|
544
|
+
`PACKAGE_ROOT="/var/packages/${contract.package}"`,
|
|
545
|
+
'ATTEMPT_ID="${SYNOPKG_PKGNAME}-${SYNOPKG_PKGVER}-$$"',
|
|
546
|
+
'WORK="$(mktemp -d /tmp/synopkgland-install.XXXXXX)"',
|
|
547
|
+
'chmod 700 "$WORK"',
|
|
548
|
+
"cleanup() { rm -rf \"$WORK\"; }",
|
|
549
|
+
"trap cleanup EXIT",
|
|
550
|
+
'mkdir -p "${SYNOPKG_PKGVAR}" "${SYNOPKG_PKGHOME}/secrets"',
|
|
551
|
+
'chmod 770 "${SYNOPKG_PKGVAR}" "${SYNOPKG_PKGHOME}" "${SYNOPKG_PKGHOME}/secrets" 2>/dev/null || true',
|
|
552
|
+
'chgrp synopkgs "${SYNOPKG_PKGVAR}" "${SYNOPKG_PKGHOME}" "${SYNOPKG_PKGHOME}/secrets" 2>/dev/null || true',
|
|
553
|
+
'VARS=""',
|
|
554
|
+
'SECRET_FILES=""',
|
|
555
|
+
];
|
|
556
|
+
for (const { name, param } of secrets) {
|
|
557
|
+
if (param.generate === true) {
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
lines.push(`if [ -n "\${${name}+x}" ]; then`);
|
|
561
|
+
lines.push(" umask 077");
|
|
562
|
+
lines.push(` printf '%s' "\$${name}" > "$WORK/${name}"`);
|
|
563
|
+
lines.push(` unset ${name}`);
|
|
564
|
+
lines.push(` SECRET_FILES="$SECRET_FILES --secret-file ${name}=$WORK/${name}"`);
|
|
565
|
+
lines.push("fi");
|
|
566
|
+
}
|
|
567
|
+
for (const { name } of nonSecrets) {
|
|
568
|
+
lines.push(`if [ -n "\${${name}+x}" ]; then`);
|
|
569
|
+
lines.push(` VARS="$VARS --var ${name}=\$${name}"`);
|
|
570
|
+
lines.push("fi");
|
|
571
|
+
}
|
|
572
|
+
const generateFlags = secrets
|
|
573
|
+
.filter((item) => item.param.generate === true)
|
|
574
|
+
.map((item) => `--generate-secret ${item.name}`)
|
|
575
|
+
.join(" ");
|
|
576
|
+
lines.push(`"$INSTALLER" --socket "$SOCKET" --phase ${phase} --attempt-id "$ATTEMPT_ID" \\`);
|
|
577
|
+
lines.push(` --package-identity "${contract.package}" --package-version "\${SYNOPKG_PKGVER}" \\`);
|
|
578
|
+
lines.push(' --package-path "$PACKAGE_ROOT/target" --bundle-path "$PACKAGE_ROOT/target" \\');
|
|
579
|
+
lines.push(' --pkg-var "$PACKAGE_ROOT/var" --pkg-home "$PACKAGE_ROOT/home" \\');
|
|
580
|
+
if (generateFlags) {
|
|
581
|
+
lines.push(` ${generateFlags} \\`);
|
|
582
|
+
}
|
|
583
|
+
lines.push(' $VARS $SECRET_FILES');
|
|
584
|
+
lines.push("exit $?");
|
|
585
|
+
return `${lines.join("\n")}\n`;
|
|
586
|
+
}
|
|
587
|
+
async function createDeterministicSpk(stagingDir, outputPath) {
|
|
588
|
+
const payloadDir = join(stagingDir, "package");
|
|
589
|
+
if (existsSync(payloadDir) && statSync(payloadDir).isDirectory()) {
|
|
590
|
+
await createReproducibleTar(payloadDir, join(stagingDir, "package.tgz"), { gzip: true });
|
|
591
|
+
rmSync(payloadDir, { recursive: true, force: true });
|
|
592
|
+
}
|
|
593
|
+
await createReproducibleTar(stagingDir, outputPath);
|
|
594
|
+
}
|
|
595
|
+
function walkFiles(root, prefix = "") {
|
|
596
|
+
const files = [];
|
|
597
|
+
for (const entry of readdirSync(root).sort()) {
|
|
598
|
+
const relative = prefix ? `${prefix}/${entry}` : entry;
|
|
599
|
+
const absolute = join(root, entry);
|
|
600
|
+
if (statSync(absolute).isDirectory()) {
|
|
601
|
+
files.push(...walkFiles(absolute, relative));
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
files.push(relative);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return files;
|
|
608
|
+
}
|
|
609
|
+
export async function digestSpkPayload(spkPath) {
|
|
610
|
+
const dir = mkdtempSync(join(tmpdir(), "syno-spk-digest-"));
|
|
611
|
+
try {
|
|
612
|
+
await extractSpk(spkPath, dir);
|
|
613
|
+
const hash = createHash("sha256");
|
|
614
|
+
for (const file of walkFiles(dir)) {
|
|
615
|
+
hash.update(file);
|
|
616
|
+
hash.update(readFileSync(join(dir, file)));
|
|
617
|
+
}
|
|
618
|
+
return hash.digest("hex");
|
|
619
|
+
}
|
|
620
|
+
finally {
|
|
621
|
+
rmSync(dir, { recursive: true, force: true });
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
export async function extractSpk(spkPath, targetDir) {
|
|
625
|
+
mkdirSync(targetDir, { recursive: true });
|
|
626
|
+
await tar.x({ file: spkPath, cwd: targetDir });
|
|
627
|
+
const tgz = join(targetDir, "package.tgz");
|
|
628
|
+
if (existsSync(tgz)) {
|
|
629
|
+
const packageDir = join(targetDir, "package");
|
|
630
|
+
mkdirSync(packageDir, { recursive: true });
|
|
631
|
+
await tar.x({ file: tgz, cwd: packageDir });
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
export function readInfoPackage(spkDir) {
|
|
635
|
+
const info = readFileSync(join(spkDir, "INFO"), "utf8");
|
|
636
|
+
const match = info.match(/^package="([^"]+)"/m);
|
|
637
|
+
return match?.[1] ?? null;
|
|
638
|
+
}
|
|
639
|
+
export function collectWizardTexts(spkDir) {
|
|
640
|
+
const install = readFileSync(join(spkDir, "WIZARD_UIFILES", INSTALL_UIFILE), "utf8");
|
|
641
|
+
const upgrade = readFileSync(join(spkDir, "WIZARD_UIFILES", UPGRADE_UIFILE), "utf8");
|
|
642
|
+
const uninstall = readFileSync(join(spkDir, "WIZARD_UIFILES", UNINSTALL_UIFILE), "utf8");
|
|
643
|
+
return `${install}\n${upgrade}\n${uninstall}`;
|
|
644
|
+
}
|
|
645
|
+
export function readInstallWizard(spkDir) {
|
|
646
|
+
return JSON.parse(readFileSync(join(spkDir, "WIZARD_UIFILES", INSTALL_UIFILE), "utf8"))[0];
|
|
647
|
+
}
|
|
648
|
+
export function readUpgradeWizard(spkDir) {
|
|
649
|
+
return JSON.parse(readFileSync(join(spkDir, "WIZARD_UIFILES", UPGRADE_UIFILE), "utf8"))[0];
|
|
650
|
+
}
|
|
651
|
+
export function scriptModeInSpk(spkDir, scriptName) {
|
|
652
|
+
return statSync(join(spkDir, "scripts", scriptName)).mode & 0o777;
|
|
653
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type NativeArchiveKind = "tar.gz" | "tar.xz" | "zip" | "raw";
|
|
2
|
+
export interface NativeSource {
|
|
3
|
+
path?: string;
|
|
4
|
+
url?: string;
|
|
5
|
+
sha256?: string;
|
|
6
|
+
strip_components?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface BuildNativePayloadOptions {
|
|
9
|
+
source: NativeSource;
|
|
10
|
+
projectRoot: string;
|
|
11
|
+
outputTgz: string;
|
|
12
|
+
/** Build-time network injection. The default uses the platform fetch implementation. */
|
|
13
|
+
download?: (url: string) => Promise<Buffer>;
|
|
14
|
+
}
|
|
15
|
+
export interface NativePayloadResult {
|
|
16
|
+
outputTgz: string;
|
|
17
|
+
sha256: string;
|
|
18
|
+
sourceSha256: string;
|
|
19
|
+
entries: string[];
|
|
20
|
+
}
|
|
21
|
+
export declare function archiveKindFor(nameOrUrl: string): NativeArchiveKind;
|
|
22
|
+
export declare function buildNativePayload(options: BuildNativePayloadOptions): Promise<NativePayloadResult>;
|