@synopackageland/cli 0.24.1 → 0.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +10 -0
- package/dist/build-spk/ui-texts.d.ts +14 -2
- package/dist/build-spk/ui-texts.js +16 -4
- package/dist/contract/app-launch.d.ts +4 -1
- package/dist/contract/app-launch.js +2 -1
- package/dist/error-registry.js +35 -0
- package/dist/runtime/executor-digests.json +1 -12
- package/dist/runtime/sdk-source.js +1 -1
- package/dist/standalone/daemon-ingress.d.ts +21 -0
- package/dist/standalone/daemon-ingress.js +61 -0
- package/dist/standalone/daemon.d.ts +26 -0
- package/dist/standalone/daemon.js +87 -4
- package/dist/standalone/dsm-window.d.ts +6 -1
- package/dist/standalone/dsm-window.js +58 -10
- package/dist/standalone/inspect-spk.js +103 -16
- package/dist/standalone/lifecycle.d.ts +3 -0
- package/dist/standalone/lifecycle.js +35 -10
- package/dist/standalone/manifest.d.ts +9 -0
- package/dist/standalone/manifest.js +36 -0
- package/dist/standalone/resource.d.ts +4 -0
- package/dist/standalone/resource.js +5 -1
- package/dist/standalone/spk.d.ts +5 -0
- package/dist/standalone/spk.js +25 -5
- package/package.json +2 -2
- package/skills/SKILL.md +21 -0
- package/skills/reference/portal.md +9 -0
- package/skills/reference/ui-json.md +29 -0
- package/skills/reference/ui-vue.md +41 -0
- package/skills/reference/upstream-fork.md +5 -0
|
@@ -21,7 +21,8 @@ import { familyOfArchToken, NATIVE_ARCH_FAMILIES, } from "../build-spk/dsm-arch.
|
|
|
21
21
|
import { CONTAINER_MIN_DSM, isDsmAtLeast, PORTAL_EXECUTOR_MIN_DSM, PORTAL_MIN_DSM, PORTAL_MIN_DSM_REASON, } from "../build-spk/dsm-version.js";
|
|
22
22
|
import { ELF_MACHINE } from "./binary.js";
|
|
23
23
|
import { CGI_PAYLOAD_PATH, entryLauncherAccess } from "./dsm-window.js";
|
|
24
|
-
import { daemonConfig, readDaemon, readLifecycleTimeouts } from "./daemon.js";
|
|
24
|
+
import { daemonConfig, readDaemon, readLifecycleTimeouts, renderNodeLauncher, nodeDependency, NODE_LAUNCHER } from "./daemon.js";
|
|
25
|
+
import { DAEMON_INGRESS_CONFIG, daemonIngressMinDsmProblem, readDaemonIngress, renderDaemonIngressConfig } from "./daemon-ingress.js";
|
|
25
26
|
import { ROOT_SCRIPT_HINTS } from "./root-ops.js";
|
|
26
27
|
import { DELETE_STATE_KEY } from "../build-spk/uninstall-wizard.js";
|
|
27
28
|
import { readUninstallRecord, stateItemTexts } from "./uninstall.js";
|
|
@@ -36,7 +37,7 @@ import { renderNativePrivilege } from "../build-spk/native-lifecycle.js";
|
|
|
36
37
|
import { renderUiConfig } from "./dsm-window.js";
|
|
37
38
|
import { identityGatewayConfig, gatewayAdmissionReport, gatewayAdmissionPolicy, readIdentityGateway, } from "./identity-gateway.js";
|
|
38
39
|
import { checkCgiBinary } from "./binary.js";
|
|
39
|
-
import { matchesStandaloneScript, renderStandaloneScript } from "./lifecycle.js";
|
|
40
|
+
import { matchesStandaloneScript, renderStandaloneScript, matchesStandaloneScriptPrefix } from "./lifecycle.js";
|
|
40
41
|
import { listPortExposures, loadCompose } from "../compose/analyze.js";
|
|
41
42
|
import { uninstallCleanup, } from "../runtime/uninstall-cleanup.js";
|
|
42
43
|
import { PORTAL_JSON_RELPATH, PORTAL_MUSTACHE_RELPATH, renderPortalJson, renderPortalLifecycle, renderPortalMustache, renderPortalWizardStep, } from "../portal/package.js";
|
|
@@ -112,7 +113,8 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
112
113
|
].includes(String(name))) ||
|
|
113
114
|
new Set(raw.capabilities).size !== raw.capabilities.length ||
|
|
114
115
|
(raw.entry !== undefined &&
|
|
115
|
-
!["admin-only", "app-privilege"].includes(raw.entry))
|
|
116
|
+
!["admin-only", "app-privilege"].includes(raw.entry)) ||
|
|
117
|
+
(raw.console !== undefined && raw.console !== true))
|
|
116
118
|
throw new Error();
|
|
117
119
|
if (raw.lifecycleTimeouts !== undefined && (!readLifecycleTimeouts(raw.lifecycleTimeouts, () => { }) || Object.keys(raw.lifecycleTimeouts).length !== 4))
|
|
118
120
|
throw new Error();
|
|
@@ -154,12 +156,14 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
154
156
|
const combined = metadata.compose === undefined
|
|
155
157
|
? undefined
|
|
156
158
|
: await recoverCompose(dir, metadata.compose, findings);
|
|
157
|
-
const
|
|
159
|
+
const daemonNode = recoverDaemonNode(metadata.daemon_node, findings);
|
|
160
|
+
const info = readInfo(join(dir, "INFO"), findings, kind, Boolean(combined), daemonNode);
|
|
158
161
|
if (combined &&
|
|
159
162
|
(!info.os_min_ver || !isDsmAtLeast(info.os_min_ver, CONTAINER_MIN_DSM)))
|
|
160
163
|
findings.push("compose identity_gateway requires DSM >= 7.2.1");
|
|
161
164
|
await checkArch(dir, info, kind, paths.has(CGI_PAYLOAD_PATH), findings);
|
|
162
165
|
const nativeGateway = await recoverNativeGateway(dir, metadata.native_gateway, info, findings);
|
|
166
|
+
const daemonIngress = await recoverDaemonIngress(dir, metadata.daemon_ingress, info, paths, findings);
|
|
163
167
|
const background = compiledKind && paths.has(BACKGROUND_WORKER_PAYLOAD);
|
|
164
168
|
// Before any fixed-lifecycle comparison: those see the SDK steps alone.
|
|
165
169
|
const appScripts = standalone || kind === "container"
|
|
@@ -178,7 +182,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
178
182
|
!isDsmAtLeast(info.os_min_ver, PORTAL_MIN_DSM)) {
|
|
179
183
|
findings.push(`INFO os_min_ver "${info.os_min_ver}" is below the DSM ${PORTAL_MIN_DSM} a portal needs; ${PORTAL_MIN_DSM_REASON}`);
|
|
180
184
|
}
|
|
181
|
-
checkConf(dir, findings, kind, background, sharedFolder, info.package, portal, combined?.compose, nativeGateway, info.os_min_ver);
|
|
185
|
+
checkConf(dir, findings, kind, background, sharedFolder, info.package, portal, combined?.compose, nativeGateway, info.os_min_ver, daemonIngress);
|
|
182
186
|
checkScripts(dir, findings);
|
|
183
187
|
checkUninstall(dir, findings);
|
|
184
188
|
if (kind === "container")
|
|
@@ -206,7 +210,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
206
210
|
if (standalone && portal)
|
|
207
211
|
findings.push(...inspectPortalInfo(info, hasExecutor));
|
|
208
212
|
const daemon = standalone
|
|
209
|
-
? await checkDaemon(dir, info, paths, payload, findings, hasExecutor, background, combined, nativeGateway, uninstallHook?.uninstall)
|
|
213
|
+
? await checkDaemon(dir, info, paths, payload, findings, hasExecutor, background, combined, nativeGateway, uninstallHook?.uninstall, { ingress: daemonIngress, node: daemonNode })
|
|
210
214
|
: undefined;
|
|
211
215
|
if (background)
|
|
212
216
|
checkBackground(dir, info, payload, findings, hasExecutor, daemon, kind !== "native-go", metadata.lifecycleTimeouts, nativeGateway);
|
|
@@ -272,7 +276,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
272
276
|
}
|
|
273
277
|
if (metadata.entry)
|
|
274
278
|
await checkEntryLauncher(dir, info.package, metadata.entry, findings);
|
|
275
|
-
const launch = await checkLaunchConfiguration(dir, info, standalone, metadata.launch, portal?.alias ?? executor?.policy.portal?.alias, findings);
|
|
279
|
+
const launch = await checkLaunchConfiguration(dir, info, standalone, metadata.launch, portal?.alias ?? executor?.policy.portal?.alias, findings, metadata.console === true);
|
|
276
280
|
const loginStyle = standalone
|
|
277
281
|
? await inspectLoginStyle(dir, info.package, Boolean(portal || executor?.policy.portal), findings)
|
|
278
282
|
: undefined;
|
|
@@ -336,7 +340,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
336
340
|
const LAUNCHER_TYPE_MISMATCH = "LAUNCHER_TYPE_MISMATCH";
|
|
337
341
|
const LAUNCHER_PORTAL_MISMATCH = "LAUNCHER_PORTAL_MISMATCH";
|
|
338
342
|
/** Compare the actual launcher and Portal fields to the packaged declaration. */
|
|
339
|
-
async function checkLaunchConfiguration(dir, info, native, declared, alias, findings) {
|
|
343
|
+
async function checkLaunchConfiguration(dir, info, native, declared, alias, findings, console = false) {
|
|
340
344
|
const target = join(dir, "launch-inspection");
|
|
341
345
|
mkdirSync(target);
|
|
342
346
|
await tar.x({
|
|
@@ -387,6 +391,10 @@ async function checkLaunchConfiguration(dir, info, native, declared, alias, find
|
|
|
387
391
|
catch {
|
|
388
392
|
/* report the missing or malformed launcher below */
|
|
389
393
|
}
|
|
394
|
+
if (console) {
|
|
395
|
+
checkConsoleLaunchers(target, info, native, findings);
|
|
396
|
+
return launch;
|
|
397
|
+
}
|
|
390
398
|
const type = dsmLauncherType(launch, native);
|
|
391
399
|
if (!launcher ||
|
|
392
400
|
launcher.type !== type ||
|
|
@@ -406,6 +414,35 @@ async function checkLaunchConfiguration(dir, info, native, declared, alias, find
|
|
|
406
414
|
}
|
|
407
415
|
return launch;
|
|
408
416
|
}
|
|
417
|
+
/**
|
|
418
|
+
* A console App's ui/config: `.Instance` is the url launcher to the gateway,
|
|
419
|
+
* `.Console` the administrator-only SDK window Package Center's Open starts.
|
|
420
|
+
*/
|
|
421
|
+
function checkConsoleLaunchers(target, info, native, findings) {
|
|
422
|
+
const ns = `SYNO.SDS.${info.package}`;
|
|
423
|
+
let config = {};
|
|
424
|
+
try {
|
|
425
|
+
config = JSON.parse(readFileSync(join(target, "ui/config"), "utf8"));
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
/* reported below as a missing launcher */
|
|
429
|
+
}
|
|
430
|
+
const find = (id) => Object.values(config).map((entries) => entries?.[id]).find(Boolean);
|
|
431
|
+
const instance = find(`${ns}.Instance`);
|
|
432
|
+
const console = find(`${ns}.Console`);
|
|
433
|
+
if (!native ||
|
|
434
|
+
instance?.type !== "url" ||
|
|
435
|
+
instance.url !== `/webman/3rdparty/${info.package}/sso/open.cgi` ||
|
|
436
|
+
console?.type !== "app" ||
|
|
437
|
+
console.appWindow !== `${ns}.MainWindow` ||
|
|
438
|
+
console.allUsers !== false ||
|
|
439
|
+
console.grantPrivilege !== undefined ||
|
|
440
|
+
info.dsmuidir !== "ui" ||
|
|
441
|
+
info.dsmappname !== `${ns}.Instance ${ns}.Console` ||
|
|
442
|
+
info.dsmapplaunchname !== `${ns}.Console`) {
|
|
443
|
+
findings.push(`${LAUNCHER_TYPE_MISMATCH} console requires a url launcher at ${ns}.Instance to sso/open.cgi, an administrator-only app launcher at ${ns}.Console and INFO dsmapplaunchname=${ns}.Console; rebuild with synopkgland build`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
409
446
|
/** The launcher in ui/config must carry exactly the access the declared entry mode fixes. */
|
|
410
447
|
async function checkEntryLauncher(dir, pkg, entry, findings) {
|
|
411
448
|
const target = join(dir, "entry-inspection");
|
|
@@ -813,7 +850,7 @@ async function recoverCompose(dir, value, findings) {
|
|
|
813
850
|
return undefined;
|
|
814
851
|
}
|
|
815
852
|
}
|
|
816
|
-
async function checkDaemon(dir, info, paths, payload, findings, executor, background, combined, nativeGateway, uninstall) {
|
|
853
|
+
async function checkDaemon(dir, info, paths, payload, findings, executor, background, combined, nativeGateway, uninstall, extras = {}) {
|
|
817
854
|
if (![...paths].some((path) => path.startsWith("daemon/")))
|
|
818
855
|
return;
|
|
819
856
|
if (info.precheckstartstop !== "yes")
|
|
@@ -827,7 +864,7 @@ async function checkDaemon(dir, info, paths, payload, findings, executor, backgr
|
|
|
827
864
|
await tar.x({
|
|
828
865
|
file: join(dir, "package.tgz"),
|
|
829
866
|
cwd: unpacked,
|
|
830
|
-
filter: (path) => path.replace(/^\.\//, "")
|
|
867
|
+
filter: (path) => ["daemon/config.json", `daemon/payload/${NODE_LAUNCHER}`].includes(path.replace(/^\.\//, "")),
|
|
831
868
|
});
|
|
832
869
|
try {
|
|
833
870
|
const config = JSON.parse(readFileSync(join(unpacked, "daemon/config.json"), "utf8"));
|
|
@@ -853,6 +890,14 @@ async function checkDaemon(dir, info, paths, payload, findings, executor, backgr
|
|
|
853
890
|
}, dir, ["x86_64"], () => { });
|
|
854
891
|
if (!daemon)
|
|
855
892
|
throw new Error("invalid config");
|
|
893
|
+
// The packaged config already carries the launcher argv; only the launcher itself is checked here.
|
|
894
|
+
if (extras.node &&
|
|
895
|
+
(daemon.exec !== NODE_LAUNCHER ||
|
|
896
|
+
!daemon.args[0]?.startsWith("${SYNOPKG_PKGDEST}/daemon/payload/") ||
|
|
897
|
+
readOptional(join(unpacked, "daemon/payload", NODE_LAUNCHER)) !== renderNodeLauncher(extras.node)))
|
|
898
|
+
throw new Error("Node launcher differs from its build declaration");
|
|
899
|
+
if (extras.ingress)
|
|
900
|
+
daemon.ingress = extras.ingress;
|
|
856
901
|
if (config.operation_seconds !== undefined && (!Number.isInteger(config.operation_seconds) || config.operation_seconds < 1 || config.operation_seconds > 3600))
|
|
857
902
|
throw new Error('invalid operation budget');
|
|
858
903
|
if (combined &&
|
|
@@ -921,7 +966,7 @@ function archivedInfoTranslations(info) {
|
|
|
921
966
|
}
|
|
922
967
|
return translations;
|
|
923
968
|
}
|
|
924
|
-
function readInfo(path, findings, kind, compose = false) {
|
|
969
|
+
function readInfo(path, findings, kind, compose = false, daemonNode) {
|
|
925
970
|
if (!existsSync(path)) {
|
|
926
971
|
findings.push("INFO is missing");
|
|
927
972
|
return {};
|
|
@@ -950,7 +995,10 @@ function readInfo(path, findings, kind, compose = false) {
|
|
|
950
995
|
: kind === "native-node"
|
|
951
996
|
? /^Node\.js_v[0-9]{2}([><=].*)?$/
|
|
952
997
|
: undefined;
|
|
953
|
-
if (info.install_dep_packages
|
|
998
|
+
if (daemonNode && info.install_dep_packages !== nodeDependency(daemonNode))
|
|
999
|
+
findings.push(`daemon.node requires INFO install_dep_packages="${nodeDependency(daemonNode)}"`);
|
|
1000
|
+
else if (!daemonNode &&
|
|
1001
|
+
info.install_dep_packages &&
|
|
954
1002
|
!(allowed &&
|
|
955
1003
|
info.install_dep_packages.split(":").every((dep) => allowed.test(dep)))) {
|
|
956
1004
|
findings.push(`INFO depends on other packages: ${info.install_dep_packages}`);
|
|
@@ -1054,7 +1102,44 @@ async function recoverNativeGateway(dir, value, info, findings) {
|
|
|
1054
1102
|
return undefined;
|
|
1055
1103
|
}
|
|
1056
1104
|
}
|
|
1057
|
-
function
|
|
1105
|
+
function recoverDaemonNode(value, findings) {
|
|
1106
|
+
if (value === undefined)
|
|
1107
|
+
return undefined;
|
|
1108
|
+
const daemon = readDaemon({ exec: "x", targets: {}, node: value }, tmpdir(), [], () => { });
|
|
1109
|
+
if (!daemon?.node || !isDeepStrictEqual(daemon.node, value)) {
|
|
1110
|
+
findings.push("DAEMON_NODE_INVALID sdk-package.json daemon_node is not a Node.js package declaration build writes");
|
|
1111
|
+
return undefined;
|
|
1112
|
+
}
|
|
1113
|
+
return daemon.node;
|
|
1114
|
+
}
|
|
1115
|
+
async function recoverDaemonIngress(dir, value, info, paths, findings) {
|
|
1116
|
+
if (value === undefined)
|
|
1117
|
+
return undefined;
|
|
1118
|
+
try {
|
|
1119
|
+
const ingress = readDaemonIngress(value, () => { });
|
|
1120
|
+
if (!ingress || !isDeepStrictEqual(ingress, value))
|
|
1121
|
+
throw new Error("invalid ingress record");
|
|
1122
|
+
const minDsm = daemonIngressMinDsmProblem(info.os_min_ver);
|
|
1123
|
+
if (minDsm)
|
|
1124
|
+
throw new Error(minDsm);
|
|
1125
|
+
if (!paths.has("daemon/config.json"))
|
|
1126
|
+
throw new Error("ingress needs the package daemon");
|
|
1127
|
+
const root = join(dir, "daemon-ingress-inspection");
|
|
1128
|
+
mkdirSync(root);
|
|
1129
|
+
await tar.x({ file: join(dir, "package.tgz"), cwd: root, filter: (path) => path.replace(/^\.\//, "") === DAEMON_INGRESS_CONFIG });
|
|
1130
|
+
if (readOptional(join(root, DAEMON_INGRESS_CONFIG)) !== renderDaemonIngressConfig(info.package, ingress))
|
|
1131
|
+
throw new Error("ingress location differs from its fixed proxy");
|
|
1132
|
+
for (const key of ["instuninst_restart_services", "startstop_restart_services"])
|
|
1133
|
+
if (!(info[key] ?? "").split(/\s+/).includes("nginx.service"))
|
|
1134
|
+
throw new Error(`ingress requires INFO ${key}`);
|
|
1135
|
+
return ingress;
|
|
1136
|
+
}
|
|
1137
|
+
catch (e) {
|
|
1138
|
+
findings.push(`DAEMON_INGRESS_INVALID ${e.message}`);
|
|
1139
|
+
return undefined;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
function checkConf(dir, findings, kind, background, sharedFolder, pkg, portal, compose, nativeGateway, osMinVer, daemonIngress) {
|
|
1058
1143
|
const confDir = join(dir, "conf");
|
|
1059
1144
|
const entries = existsSync(confDir) ? readdirSync(confDir) : [];
|
|
1060
1145
|
const nativeResource = kind !== "container"
|
|
@@ -1067,6 +1152,7 @@ function checkConf(dir, findings, kind, background, sharedFolder, pkg, portal, c
|
|
|
1067
1152
|
? { jobs: { worker: "sha256-v1" } }
|
|
1068
1153
|
: {},
|
|
1069
1154
|
...(sharedFolder ? { sharedFolder } : {}),
|
|
1155
|
+
...(daemonIngress ? { daemon: { ingress: daemonIngress } } : {}),
|
|
1070
1156
|
}, portal)
|
|
1071
1157
|
: undefined;
|
|
1072
1158
|
if (nativeResource !== undefined &&
|
|
@@ -1229,8 +1315,8 @@ async function inspectInstallSettings(dir, payload, metadata, findings) {
|
|
|
1229
1315
|
mkdirSync(root);
|
|
1230
1316
|
await tar.x({ file: join(dir, 'package.tgz'), cwd: root });
|
|
1231
1317
|
const info = readInfo(join(dir, 'INFO'), findings, 'binary', Boolean(metadata.compose));
|
|
1232
|
-
const gateway = metadata.compose?.gateway ??
|
|
1233
|
-
const m = { package: info.package, installSettings: settings, identityGateway: gateway };
|
|
1318
|
+
const gateway = metadata.compose?.gateway ?? metadata.native_gateway;
|
|
1319
|
+
const m = { package: info.package, os_min_ver: info.os_min_ver, capabilities: {}, installSettings: settings, identityGateway: gateway };
|
|
1234
1320
|
try {
|
|
1235
1321
|
if (readOptional(join(root, 'install/settings.json')) !== JSON.stringify(settingsConfig(m), null, 2) + '\n')
|
|
1236
1322
|
throw new Error('settings inventory differs from its declaration or gateway port');
|
|
@@ -1243,7 +1329,8 @@ async function inspectInstallSettings(dir, payload, metadata, findings) {
|
|
|
1243
1329
|
// App wrappers remain intact here; remove exactly the SDK prefix within them.
|
|
1244
1330
|
for (const name of ['preinst', 'postinst', 'postupgrade', 'preuninst']) {
|
|
1245
1331
|
const file = join(dir, 'scripts', name), text = readOptional(file), prefix = renderSettingsHook(name, m);
|
|
1246
|
-
|
|
1332
|
+
const sdk = text && sdkScriptOf(name, text)?.sdk;
|
|
1333
|
+
if (!sdk || !matchesStandaloneScriptPrefix(name, m, sdk) || text.split(prefix).length !== 2)
|
|
1247
1334
|
throw new Error(`scripts/${name} lacks the exact SDK settings hook`);
|
|
1248
1335
|
writeFileSync(file, text.replace(prefix, ''));
|
|
1249
1336
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { StandaloneManifest } from "./manifest.js";
|
|
2
2
|
/** Package-account lifecycle hooks for CGI, daemon and background packages. */
|
|
3
3
|
export declare function renderStandaloneScript(name: string, manifest: StandaloneManifest): string;
|
|
4
|
+
/** Settings inspection peels its hook only at the position build's renderer writes.
|
|
5
|
+
* The backend inspector then replays the entire remaining lifecycle script. */
|
|
6
|
+
export declare function matchesStandaloneScriptPrefix(name: string, manifest: StandaloneManifest, actual: string): boolean;
|
|
4
7
|
/** Accept the exact current, diagnostic-only, and released lifecycle variants. */
|
|
5
8
|
export declare function matchesStandaloneScript(name: string, manifest: StandaloneManifest, actual: string | undefined): boolean;
|
|
@@ -15,23 +15,37 @@ const UNINSTALL_ONLY = '[ "${SYNOPKG_PKG_STATUS:-}" = UPGRADE ] || synopkgland_u
|
|
|
15
15
|
/** Package-account lifecycle hooks for CGI, daemon and background packages. */
|
|
16
16
|
export function renderStandaloneScript(name, manifest) {
|
|
17
17
|
const script = renderStandaloneScriptInternal(name, manifest);
|
|
18
|
-
const
|
|
19
|
-
const prefix = renderNativeGatewayDiagnostic(manifest, name) + renderNativeGatewayPortWait(manifest, name) + renderSettingsHook(name, manifest) + prepared;
|
|
18
|
+
const prefix = renderStandaloneScriptPrefix(name, manifest);
|
|
20
19
|
const sdk = prefix ? script.replace('#!/bin/sh\n', () => '#!/bin/sh\n' + prefix) : script;
|
|
21
20
|
return manifest.appScripts?.some((app) => app === name)
|
|
22
21
|
? composeAppScript(name, sdk)
|
|
23
22
|
: sdk;
|
|
24
23
|
}
|
|
24
|
+
function renderStandaloneScriptPrefix(name, manifest) {
|
|
25
|
+
const prepared = ['postinst', 'postupgrade'].includes(name) ? renderNativeGatewayPrepare(manifest) : '';
|
|
26
|
+
return renderNativeGatewayDiagnostic(manifest, name) + renderNativeGatewayPortWait(manifest, name) + renderSettingsHook(name, manifest) + prepared;
|
|
27
|
+
}
|
|
28
|
+
/** Settings inspection peels its hook only at the position build's renderer writes.
|
|
29
|
+
* The backend inspector then replays the entire remaining lifecycle script. */
|
|
30
|
+
export function matchesStandaloneScriptPrefix(name, manifest, actual) {
|
|
31
|
+
const blocks = [renderNativeGatewayDiagnostic(manifest, name), renderNativeGatewayPortWait(manifest, name)].filter(Boolean);
|
|
32
|
+
return standaloneScriptVariants(name, manifest, renderStandaloneScriptPrefix(name, manifest)).some(prefix => actual.startsWith('#!/bin/sh\n' + prefix) &&
|
|
33
|
+
// A released prefix omits these blocks entirely; moving one after the
|
|
34
|
+
// settings hook must not masquerade as that released variant.
|
|
35
|
+
blocks.every(block => prefix.includes(block) || !actual.includes(block)));
|
|
36
|
+
}
|
|
25
37
|
/** Accept the exact current, diagnostic-only, and released lifecycle variants. */
|
|
26
38
|
export function matchesStandaloneScript(name, manifest, actual) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
39
|
+
return actual !== undefined && standaloneScriptVariants(name, manifest, renderStandaloneScript(name, manifest)).includes(actual);
|
|
40
|
+
}
|
|
41
|
+
function standaloneScriptVariants(name, manifest, expected) {
|
|
30
42
|
const diagnostic = renderNativeGatewayDiagnostic(manifest, name);
|
|
31
43
|
const wait = renderNativeGatewayPortWait(manifest, name);
|
|
32
44
|
const diagnosticOnly = wait ? expected.replace(wait, '') : expected;
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
const variants = [expected, ...(wait ? [diagnosticOnly] : []), ...(diagnostic ? [diagnosticOnly.replace(diagnostic, '')] : [])];
|
|
46
|
+
// Released packages stop the daemon in preupgrade without the first-daemon guard.
|
|
47
|
+
const unguarded = (v) => v.replace(daemonGuard(manifest.package), '').replace(delegatedDaemonGuard(manifest.package), '');
|
|
48
|
+
return name === 'preupgrade' ? [...new Set([...variants, ...variants.map(unguarded)])] : variants;
|
|
35
49
|
}
|
|
36
50
|
function renderStandaloneScriptInternal(name, manifest) {
|
|
37
51
|
if (manifest.compose) {
|
|
@@ -201,11 +215,11 @@ function renderDelegatedDaemonScript(name, manifest) {
|
|
|
201
215
|
const worker = (action) => `/usr/syno/bin/synosystemctl ${action} ${backgroundUnitName(pkg)}`;
|
|
202
216
|
// Try every owner even on failure; never leave protected ingress open merely
|
|
203
217
|
// because a producer could not stop. A partial teardown remains a failure.
|
|
204
|
-
const stop = (daemonAction, executorAction, executorStop = executor(executorAction)) => `result=0\nstop_background_worker || result=1\n${daemon} ${daemonAction} ${paths} || result=1\n${executorStop} || result=1\nexit "$result"\n`;
|
|
218
|
+
const stop = (daemonAction, executorAction, executorStop = executor(executorAction), guard = "") => `result=0\nstop_background_worker || result=1\n${guard}${daemon} ${daemonAction} ${paths} || result=1\n${executorStop} || result=1\nexit "$result"\n`;
|
|
205
219
|
const workerStop = renderBackgroundWorkerStop(pkg);
|
|
206
220
|
if (name !== "start-stop-status") {
|
|
207
221
|
if (name === "preupgrade")
|
|
208
|
-
return `#!/bin/sh\n${renderExecutorUpgradeStop(pkg)}${workerStop}${stop("stop", "stop", UPGRADE_STOP)}`;
|
|
222
|
+
return `#!/bin/sh\n${renderExecutorUpgradeStop(pkg)}${workerStop}${stop("stop", "stop", UPGRADE_STOP, delegatedDaemonGuard(pkg))}`;
|
|
209
223
|
const purge = manifest.uninstall
|
|
210
224
|
? stop(manifest.daemon?.uninstall_args ? "uninstall" : "stop", "purge $synopkgland_purge_args").replace(executor("purge $synopkgland_purge_args"), () => `synopkgland_uninstall_hook\n${executor("purge $synopkgland_purge_args")}`)
|
|
211
225
|
: stop(manifest.daemon?.uninstall_args ? "uninstall" : "stop", "purge");
|
|
@@ -262,6 +276,17 @@ function renderExecutorUpgradeStop(pkg) {
|
|
|
262
276
|
}
|
|
263
277
|
`;
|
|
264
278
|
}
|
|
279
|
+
/**
|
|
280
|
+
* preupgrade runs before DSM replaces the payload, so it sees the outgoing
|
|
281
|
+
* package. Only a payload without any daemon/ is an upgrade that introduces
|
|
282
|
+
* the daemon; a partial or symlinked daemon/ still stops or fails closed.
|
|
283
|
+
*/
|
|
284
|
+
const daemonAbsent = (pkg) => {
|
|
285
|
+
const dir = `"\${SYNOPKG_PKGDEST:-/var/packages/${pkg}/target}/daemon"`;
|
|
286
|
+
return `[ ! -e ${dir} ] && [ ! -L ${dir} ]`;
|
|
287
|
+
};
|
|
288
|
+
const daemonGuard = (pkg) => `if ${daemonAbsent(pkg)}; then exit 0; fi\n`;
|
|
289
|
+
const delegatedDaemonGuard = (pkg) => `${daemonAbsent(pkg)} || `;
|
|
265
290
|
function renderDaemonScript(name, manifest) {
|
|
266
291
|
const pkg = manifest.package;
|
|
267
292
|
const control = `"/var/packages/${pkg}/target/daemon/control"`;
|
|
@@ -272,7 +297,7 @@ function renderDaemonScript(name, manifest) {
|
|
|
272
297
|
const uninstall = name === "preuninst" && manifest.daemon?.uninstall_args;
|
|
273
298
|
const upgrade = uninstall
|
|
274
299
|
? `if [ "\${SYNOPKG_PKG_STATUS:-}" = "UPGRADE" ]; then\n exec ${control} stop ${paths}\nfi\n`
|
|
275
|
-
: "";
|
|
300
|
+
: name === "preupgrade" ? daemonGuard(pkg) : "";
|
|
276
301
|
return `#!/bin/sh\n${upgrade}exec ${control} ${uninstall ? "uninstall" : "stop"} ${paths}\n`;
|
|
277
302
|
}
|
|
278
303
|
if (name !== "start-stop-status")
|
|
@@ -100,6 +100,12 @@ export interface StandaloneManifest {
|
|
|
100
100
|
entry?: EntryMode;
|
|
101
101
|
/** `portal`: registers the DSM Application Portal entry; an administrator activates it. */
|
|
102
102
|
portal?: PortalDeclaration;
|
|
103
|
+
/**
|
|
104
|
+
* `console`: the main menu gets two icons, the gateway's native UI in a new
|
|
105
|
+
* tab and the SDK UI in an administrator-only DSM window titled `title`
|
|
106
|
+
* (default "<displayname> for DSM"). Absent keeps the single window icon.
|
|
107
|
+
*/
|
|
108
|
+
console?: ConsoleDeclaration;
|
|
103
109
|
/** The project's dsm-scripts/<DSM script name> files, each run inside the generated script of that name. */
|
|
104
110
|
appScripts?: AppScriptName[];
|
|
105
111
|
/** `icon`: a square PNG of at least 256 px, or a directory of icon_<size>.png; absent uses the SDK default. */
|
|
@@ -112,6 +118,9 @@ export interface StandaloneManifest {
|
|
|
112
118
|
* accounts DSM's Control Panel > Application Privileges allows for the app.
|
|
113
119
|
*/
|
|
114
120
|
export type EntryMode = "admin-only" | "app-privilege";
|
|
121
|
+
export interface ConsoleDeclaration {
|
|
122
|
+
title?: string;
|
|
123
|
+
}
|
|
115
124
|
export interface Capabilities {
|
|
116
125
|
composeMounts?: true;
|
|
117
126
|
webapi?: WebAPIProfiles;
|
|
@@ -11,6 +11,7 @@ import { checkGlossary, loadCatalogs, readInfoTranslations, } from "./i18n.js";
|
|
|
11
11
|
import { loadRootOps, readRootSettings, rootBinaryProblem, ROOT_OPS_DIR, } from "./root-ops.js";
|
|
12
12
|
import { readComposeApp } from "./compose.js";
|
|
13
13
|
import { readDaemon, readLifecycleTimeouts } from "./daemon.js";
|
|
14
|
+
import { daemonIngressMinDsmProblem } from "./daemon-ingress.js";
|
|
14
15
|
import { readIdentityGateway, } from "./identity-gateway.js";
|
|
15
16
|
import { readUninstall } from "./uninstall.js";
|
|
16
17
|
import { readAppScripts } from "./app-scripts.js";
|
|
@@ -56,6 +57,7 @@ export const KNOWN_FIELDS = new Set([
|
|
|
56
57
|
"uninstall",
|
|
57
58
|
"entry",
|
|
58
59
|
"portal",
|
|
60
|
+
"console",
|
|
59
61
|
"icon",
|
|
60
62
|
"reproducible",
|
|
61
63
|
"root",
|
|
@@ -65,6 +67,7 @@ const I18N_FIELDS = new Set(["messages", "glossary", "info"]);
|
|
|
65
67
|
const PACKAGE_ID = /^[A-Za-z][A-Za-z0-9]{1,63}$/;
|
|
66
68
|
const VERSION = /^\d+(\.\d+){0,3}$/;
|
|
67
69
|
const OS_MIN_VER = /^\d+\.\d+(\.\d+)?-\d+$/;
|
|
70
|
+
const CONSOLE_TITLE = /^[^"\\\x00-\x1f\x7f]{1,64}$/;
|
|
68
71
|
const GO_MAIN = /^\.\/[A-Za-z0-9_./-]*$/;
|
|
69
72
|
function isRecord(value) {
|
|
70
73
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -275,6 +278,13 @@ function readManifest(raw, c) {
|
|
|
275
278
|
c.fail("COMPOSE_WEB_INVALID", "Compose upstream is derived from the SDK Compose ingress endpoint; remove identity_gateway.upstream.");
|
|
276
279
|
// A failed Compose plan already explains the invalid upstream declaration.
|
|
277
280
|
const identityGateway = raw.compose !== undefined && !compose ? undefined : readIdentityGateway(gatewayValue, daemon || (compose ? { targets: compose.targets } : undefined), (e) => c.fail(e.code, e.message), false, Boolean(compose?.plan.ingress));
|
|
281
|
+
if (daemon?.ingress) {
|
|
282
|
+
const problem = daemonIngressMinDsmProblem(osMinVer);
|
|
283
|
+
if (problem)
|
|
284
|
+
c.fail("STANDALONE_DAEMON_INGRESS_MIN_DSM", problem);
|
|
285
|
+
}
|
|
286
|
+
if ((daemon?.ingress || daemon?.node) && raw.identity_gateway !== undefined)
|
|
287
|
+
c.fail("STANDALONE_DAEMON_INVALID", "daemon.node and daemon.ingress run the App's own daemon; identity_gateway runs its gateway as the daemon. Use one or the other.");
|
|
278
288
|
if (compose && identityGateway && compose.sdk !== identityGateway.sdk)
|
|
279
289
|
c.fail("STANDALONE_IDENTITY_GATEWAY_INVALID", "compose and identity_gateway must use the same SDK release.");
|
|
280
290
|
if (compose &&
|
|
@@ -399,6 +409,7 @@ function readManifest(raw, c) {
|
|
|
399
409
|
: undefined;
|
|
400
410
|
if (entryProblem)
|
|
401
411
|
c.fail("STANDALONE_PORTAL_ENTRY_INVALID", entryProblem);
|
|
412
|
+
const dsmConsole = raw.console === undefined ? undefined : readConsole(raw, ui?.launch, c);
|
|
402
413
|
const loginStyle = raw.login_style === undefined
|
|
403
414
|
? undefined
|
|
404
415
|
: readLoginStyle(raw.login_style, raw.portal !== undefined, c);
|
|
@@ -454,6 +465,7 @@ function readManifest(raw, c) {
|
|
|
454
465
|
...(installSettings ? { installSettings } : {}),
|
|
455
466
|
...(entry ? { entry } : {}),
|
|
456
467
|
...(portal ? { portal } : {}),
|
|
468
|
+
...(dsmConsole ? { console: dsmConsole } : {}),
|
|
457
469
|
...(icon ? { icon } : {}),
|
|
458
470
|
...(loginStyle ? { loginStyle } : {}),
|
|
459
471
|
...(appScripts ? { appScripts } : {}),
|
|
@@ -503,6 +515,30 @@ const LOGIN_STYLE_FIELDS = [
|
|
|
503
515
|
"wallpaper_thumbnail",
|
|
504
516
|
"title",
|
|
505
517
|
];
|
|
518
|
+
/**
|
|
519
|
+
* `console`: {} or { title }. The native icon is the gateway's url launcher,
|
|
520
|
+
* so the App needs identity_gateway and its own SDK window; that window is the
|
|
521
|
+
* console, never a new page or a portal.
|
|
522
|
+
*/
|
|
523
|
+
function readConsole(raw, launch, c) {
|
|
524
|
+
const value = raw.console;
|
|
525
|
+
if (!isRecord(value) ||
|
|
526
|
+
Object.keys(value).some((k) => k !== "title") ||
|
|
527
|
+
(value.title !== undefined &&
|
|
528
|
+
(typeof value.title !== "string" || !CONSOLE_TITLE.test(value.title)))) {
|
|
529
|
+
c.fail("STANDALONE_CONSOLE_INVALID", 'console must be {} or { title: <1-64 characters, no quotes, backslashes or control characters> }.');
|
|
530
|
+
return undefined;
|
|
531
|
+
}
|
|
532
|
+
if (raw.identity_gateway === undefined || raw.compose !== undefined) {
|
|
533
|
+
c.fail("STANDALONE_CONSOLE_REQUIRES_GATEWAY", "console needs identity_gateway on an App with its own SDK UI: the native icon opens the gateway, the console opens the SDK UI. Compose and Apps without a gateway keep one icon.");
|
|
534
|
+
return undefined;
|
|
535
|
+
}
|
|
536
|
+
if (launch !== undefined)
|
|
537
|
+
c.fail("STANDALONE_CONSOLE_LAUNCH", "console always opens in a DSM window and the native icon in a new tab; remove ui.launch.");
|
|
538
|
+
if (raw.portal !== undefined)
|
|
539
|
+
c.fail("STANDALONE_CONSOLE_PORTAL", "console and portal both claim the App's SDK UI; declare one of them.");
|
|
540
|
+
return value.title === undefined ? {} : { title: value.title };
|
|
541
|
+
}
|
|
506
542
|
/** `login_style`: a portal's colour, wallpaper (with its thumbnail) and title image, each optional. */
|
|
507
543
|
function readLoginStyle(value, portal, c) {
|
|
508
544
|
const invalid = (message) => c.fail("STANDALONE_LOGIN_STYLE_INVALID", message);
|
|
@@ -3,6 +3,7 @@ import type { Capabilities } from "./manifest.js";
|
|
|
3
3
|
import { type SharedFolder } from "./shared-folder.js";
|
|
4
4
|
import { type PortalPackage } from "../portal/package.js";
|
|
5
5
|
import { type NativeGatewayPlan } from './native-gateway.js';
|
|
6
|
+
import { type DaemonIngress } from "./daemon-ingress.js";
|
|
6
7
|
/**
|
|
7
8
|
* `conf/resource` for a native package: only DSM's official `data-share`,
|
|
8
9
|
* `systemd-user-unit` and `web-config` workers, in a fixed key order.
|
|
@@ -15,4 +16,7 @@ export declare function renderStandaloneResource(m: {
|
|
|
15
16
|
capabilities: Capabilities;
|
|
16
17
|
compose?: ComposeApp;
|
|
17
18
|
sharedFolder?: SharedFolder;
|
|
19
|
+
daemon?: {
|
|
20
|
+
ingress?: DaemonIngress;
|
|
21
|
+
};
|
|
18
22
|
}, portal?: PortalPackage): string | undefined;
|
|
@@ -2,6 +2,7 @@ import { renderDockerProjectResource } from "../build-spk/resource-worker.js";
|
|
|
2
2
|
import { sharedFolderResource } from "./shared-folder.js";
|
|
3
3
|
import { portalResource } from "../portal/package.js";
|
|
4
4
|
import { nativeGatewayResource } from './native-gateway.js';
|
|
5
|
+
import { daemonIngressResource } from "./daemon-ingress.js";
|
|
5
6
|
/**
|
|
6
7
|
* `conf/resource` for a native package: only DSM's official `data-share`,
|
|
7
8
|
* `systemd-user-unit` and `web-config` workers, in a fixed key order.
|
|
@@ -9,7 +10,10 @@ import { nativeGatewayResource } from './native-gateway.js';
|
|
|
9
10
|
*/
|
|
10
11
|
export function renderStandaloneResource(m, portal) {
|
|
11
12
|
const dataShare = m.sharedFolder && sharedFolderResource(m.package, m.sharedFolder);
|
|
12
|
-
const
|
|
13
|
+
const nginx = [nativeGatewayResource(m), m.daemon?.ingress && daemonIngressResource()]
|
|
14
|
+
.flatMap((r) => r?.["nginx-static-config"]?.enable ?? []);
|
|
15
|
+
const webConfig = { ...(portal ? portalResource()['web-config'] : {}),
|
|
16
|
+
...(nginx.length ? { 'nginx-static-config': { enable: nginx } } : {}) };
|
|
13
17
|
const resource = {
|
|
14
18
|
...(m.compose?.plan.devices?.gpu && !m.compose.plan.mounts_blocked ? { "video-driver": {} } : {}),
|
|
15
19
|
...(m.compose && !m.compose.plan.mounts_blocked
|
package/dist/standalone/spk.d.ts
CHANGED
|
@@ -36,4 +36,9 @@ export interface AssembleOptions {
|
|
|
36
36
|
*/
|
|
37
37
|
export declare function assembleStandaloneSpk(options: AssembleOptions): Promise<void>;
|
|
38
38
|
/** Manifest validation already refused quotes, backslashes and newlines. */
|
|
39
|
+
/**
|
|
40
|
+
* INFO `dsmappname` lists the App's launchable classes. With a console,
|
|
41
|
+
* Package Center's Open (`dsmapplaunchname`) opens the console window.
|
|
42
|
+
*/
|
|
43
|
+
export declare function consoleInfo(manifest: Pick<StandaloneManifest, "package" | "console">): Array<[string, string]>;
|
|
39
44
|
export declare function renderInfo(manifest: StandaloneManifest, arch: NativeArchFamily, checksum: string, executorDigest?: string): string;
|
package/dist/standalone/spk.js
CHANGED
|
@@ -29,7 +29,8 @@ import { copyAppTree } from "./bridge-app.js";
|
|
|
29
29
|
import { isBridgeBackend } from "./bridge-backend.js";
|
|
30
30
|
import { hostScriptName, renderHostAdapterScript } from "./host-adapter.js";
|
|
31
31
|
import { writeComposePayload } from "./compose.js";
|
|
32
|
-
import { copyDaemonPayload } from "./daemon.js";
|
|
32
|
+
import { copyDaemonPayload, nodeDependency } from "./daemon.js";
|
|
33
|
+
import { DAEMON_INGRESS_CONFIG, renderDaemonIngressConfig } from "./daemon-ingress.js";
|
|
33
34
|
import { GATEWAY_CGI_PATH, GATEWAY_CONFIG_PATH, GATEWAY_EXEC, identityGatewayConfig, identityGatewayDeclaration, } from "./identity-gateway.js";
|
|
34
35
|
import { BACKGROUND_UNIT_DIR, BACKGROUND_WORKER_PAYLOAD, backgroundUnitName, renderBackgroundUnit, BRIDGE_WORKER_ARGS, } from "./background.js";
|
|
35
36
|
import { renderStandaloneResource } from "./resource.js";
|
|
@@ -91,7 +92,7 @@ export async function assembleStandaloneSpk(options) {
|
|
|
91
92
|
writeFile(join(uiRoot, windowScriptName(manifest)), renderWindowScript(manifest, assetVersion, icons, options.portalText));
|
|
92
93
|
writeFile(join(uiRoot, hostScriptName(manifest.build)), renderHostAdapterScript(manifest.package));
|
|
93
94
|
writeDsmWindowIcons(uiRoot, icons);
|
|
94
|
-
writeUiTexts(uiRoot, appTitle(manifest));
|
|
95
|
+
writeUiTexts(uiRoot, appTitle(manifest), manifest.console);
|
|
95
96
|
if (options.apiDescription)
|
|
96
97
|
writeFile(join(staging, "package", "synopkgland-api.json"), JSON.stringify(options.apiDescription) + "\n");
|
|
97
98
|
const cgiTarget = join(staging, "package", CGI_PAYLOAD_PATH);
|
|
@@ -104,6 +105,8 @@ export async function assembleStandaloneSpk(options) {
|
|
|
104
105
|
if (!options.daemonControl)
|
|
105
106
|
throw new StandaloneBuildError("STANDALONE_DAEMON_ARTIFACT_REQUIRED", "daemon controller artifact required.");
|
|
106
107
|
copyDaemonPayload(manifest, options.arch, join(staging, "package", "daemon"), options.daemonControl);
|
|
108
|
+
if (manifest.daemon?.ingress)
|
|
109
|
+
writeFile(join(staging, "package", DAEMON_INGRESS_CONFIG), renderDaemonIngressConfig(manifest.package, manifest.daemon.ingress));
|
|
107
110
|
}
|
|
108
111
|
if (manifest.identityGateway) {
|
|
109
112
|
if (!options.identityGateway)
|
|
@@ -230,6 +233,8 @@ export async function assembleStandaloneSpk(options) {
|
|
|
230
233
|
backend: manifest.backend.kind,
|
|
231
234
|
...(gatewayAdmissionPolicy(manifest.identityGateway, manifest.entry) ? { provider_only_oidc: gatewayAdmissionPolicy(manifest.identityGateway, manifest.entry) } : {}),
|
|
232
235
|
...(nativeGatewaySupported(manifest) ? { native_gateway: { sdk: manifest.identityGateway.sdk, listen: manifest.identityGateway.listen } } : {}),
|
|
236
|
+
...(manifest.daemon?.ingress ? { daemon_ingress: manifest.daemon.ingress } : {}),
|
|
237
|
+
...(manifest.daemon?.node ? { daemon_node: manifest.daemon.node } : {}),
|
|
233
238
|
launch: resolvedSdkLaunch(manifest),
|
|
234
239
|
capabilities: Object.keys(manifest.capabilities).sort(),
|
|
235
240
|
...(manifest.compose
|
|
@@ -242,6 +247,7 @@ export async function assembleStandaloneSpk(options) {
|
|
|
242
247
|
}
|
|
243
248
|
: {}),
|
|
244
249
|
...(manifest.entry ? { entry: manifest.entry } : {}),
|
|
250
|
+
...(manifest.console ? { console: true } : {}),
|
|
245
251
|
...(manifest.uninstall
|
|
246
252
|
? {
|
|
247
253
|
uninstall: uninstallRecord(manifest.uninstall, manifest.i18n?.catalogs),
|
|
@@ -319,6 +325,16 @@ function quote(value) {
|
|
|
319
325
|
return `"${value.replace(/([\\"$`])/g, "\\$1")}"`;
|
|
320
326
|
}
|
|
321
327
|
/** Manifest validation already refused quotes, backslashes and newlines. */
|
|
328
|
+
/**
|
|
329
|
+
* INFO `dsmappname` lists the App's launchable classes. With a console,
|
|
330
|
+
* Package Center's Open (`dsmapplaunchname`) opens the console window.
|
|
331
|
+
*/
|
|
332
|
+
export function consoleInfo(manifest) {
|
|
333
|
+
const ns = `SYNO.SDS.${manifest.package}`;
|
|
334
|
+
return manifest.console
|
|
335
|
+
? [["dsmappname", `${ns}.Instance ${ns}.Console`], ["dsmapplaunchname", `${ns}.Console`]]
|
|
336
|
+
: [["dsmappname", `${ns}.Instance`]];
|
|
337
|
+
}
|
|
322
338
|
export function renderInfo(manifest, arch, checksum, executorDigest) {
|
|
323
339
|
const fields = [
|
|
324
340
|
["package", manifest.package],
|
|
@@ -329,7 +345,7 @@ export function renderInfo(manifest, arch, checksum, executorDigest) {
|
|
|
329
345
|
["arch", infoArchValue(arch)],
|
|
330
346
|
["os_min_ver", manifest.os_min_ver],
|
|
331
347
|
["dsmuidir", "ui"],
|
|
332
|
-
|
|
348
|
+
...consoleInfo(manifest),
|
|
333
349
|
["thirdparty", "yes"],
|
|
334
350
|
["startable", "yes"],
|
|
335
351
|
["checksum", checksum],
|
|
@@ -349,6 +365,8 @@ export function renderInfo(manifest, arch, checksum, executorDigest) {
|
|
|
349
365
|
const backend = manifest.backend;
|
|
350
366
|
if (isBridgeBackend(backend) && backend.node)
|
|
351
367
|
fields.push(["install_dep_packages", backend.node.package]);
|
|
368
|
+
if (manifest.daemon?.node)
|
|
369
|
+
fields.push(["install_dep_packages", nodeDependency(manifest.daemon.node)]);
|
|
352
370
|
fields.push(...metadataInfoFields(manifest.metadata));
|
|
353
371
|
// Background packages need prestart: it runs before DSM's systemd-user-unit acquire.
|
|
354
372
|
if (manifest.daemon || manifest.compose || manifest.capabilities.jobs)
|
|
@@ -360,9 +378,11 @@ export function renderInfo(manifest, arch, checksum, executorDigest) {
|
|
|
360
378
|
if (text?.description)
|
|
361
379
|
fields.push([`description_${DSM_LANGS[locale]}`, text.description]);
|
|
362
380
|
}
|
|
363
|
-
|
|
381
|
+
// DSM reloads nginx on start and stop so the location follows the package state.
|
|
382
|
+
const nginx = nativeGatewaySupported(manifest) || Boolean(manifest.daemon?.ingress);
|
|
383
|
+
if (nginx)
|
|
364
384
|
fields.push(['startstop_restart_services', 'nginx.service']);
|
|
365
|
-
return portalInfoFields(fields, Boolean(manifest.portal) ||
|
|
385
|
+
return portalInfoFields(fields, Boolean(manifest.portal) || nginx).map(([key, value]) => `${key}=${quote(value)}\n`).join("");
|
|
366
386
|
}
|
|
367
387
|
function copyUiTree(source, target) {
|
|
368
388
|
for (const entry of ["app.js", "app.css"]) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synopackageland/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.1",
|
|
4
4
|
"description": "Syno Package Land Developer Kit CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@napi-rs/keyring": "^2.1.1",
|
|
30
|
-
"@synopackageland/templates": "0.
|
|
30
|
+
"@synopackageland/templates": "0.25.0",
|
|
31
31
|
"ajv": "^8.17.1",
|
|
32
32
|
"commander": "^13.1.0",
|
|
33
33
|
"json-canonicalize": "^3.0.1",
|