@synopackageland/cli 0.9.6 → 0.10.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/CHANGELOG.md +144 -5
- package/README.md +5 -0
- package/dist/add.js +7 -8
- package/dist/build-spk/dsm-arch.js +1 -1
- package/dist/build-spk/dsm-icons.d.ts +8 -1
- package/dist/build-spk/dsm-icons.js +26 -12
- package/dist/build-spk/dsm-version.d.ts +1 -1
- package/dist/build-spk/dsm-version.js +4 -4
- package/dist/build-spk/gateway.js +2 -2
- package/dist/build-spk/generator.js +1 -1
- package/dist/build-spk/png.js +16 -16
- package/dist/capabilities.js +1 -1
- package/dist/capability-overlays.d.ts +16 -5
- package/dist/capability-overlays.js +30 -16
- package/dist/cli.js +107 -107
- package/dist/compose/analyze.js +23 -23
- package/dist/compose/privilege.js +12 -12
- package/dist/contract/i18n.js +3 -3
- package/dist/contract/icon.js +2 -2
- package/dist/contract/posix-all-shares.js +2 -2
- package/dist/contract/schema-validate.js +3 -3
- package/dist/contract/validate.js +16 -16
- package/dist/contract/web-launch.d.ts +2 -1
- package/dist/contract/web-launch.js +3 -2
- package/dist/deploy/deploy.js +8 -8
- package/dist/deploy/homes-values.js +1 -1
- package/dist/deploy/identity-shared.js +1 -1
- package/dist/deploy/native-install-values.js +1 -1
- package/dist/deploy/param-values.js +4 -4
- package/dist/deploy/plan.d.ts +1 -1
- package/dist/deploy/port-preflight.js +3 -3
- package/dist/deploy/run-as-values.js +2 -2
- package/dist/deploy/share-permission-preflight.js +6 -6
- package/dist/deploy/share-values.js +3 -3
- package/dist/error-registry.js +369 -346
- package/dist/errors.js +1 -1
- package/dist/host/dsm-port-profile.d.ts +1 -1
- package/dist/host/dsm-port-profile.js +1 -1
- package/dist/info.js +11 -11
- package/dist/init.d.ts +2 -0
- package/dist/init.js +85 -38
- package/dist/inspect.js +5 -5
- package/dist/portal/alias.d.ts +1 -1
- package/dist/portal/alias.js +4 -4
- package/dist/runtime/executor-build.js +12 -1
- package/dist/runtime/executor-digests.json +8 -85
- package/dist/runtime/executor-inspect.js +11 -4
- package/dist/runtime/executor-policy.d.ts +9 -2
- package/dist/runtime/executor-policy.js +27 -8
- package/dist/runtime/root-programs.d.ts +47 -0
- package/dist/runtime/root-programs.js +153 -0
- package/dist/runtime/sdk-source.d.ts +5 -4
- package/dist/runtime/sdk-source.js +8 -4
- package/dist/skill.js +14 -14
- package/dist/spk/check-info.js +17 -17
- package/dist/spk/check-metadata.js +18 -18
- package/dist/spk/check-payload.js +10 -10
- package/dist/spk/rules.d.ts +57 -57
- package/dist/spk/rules.js +50 -50
- package/dist/spk/validate-spk.js +5 -5
- package/dist/standalone/api-permissions.js +1 -1
- package/dist/standalone/background.d.ts +3 -1
- package/dist/standalone/background.js +8 -4
- package/dist/standalone/binary.js +7 -7
- package/dist/standalone/bridge-app.d.ts +23 -0
- package/dist/standalone/bridge-app.js +141 -0
- package/dist/standalone/bridge-backend.d.ts +40 -0
- package/dist/standalone/bridge-backend.js +95 -0
- package/dist/standalone/bridge-build.d.ts +21 -0
- package/dist/standalone/bridge-build.js +62 -0
- package/dist/standalone/bridge-json.d.ts +16 -0
- package/dist/standalone/bridge-json.js +59 -0
- package/dist/standalone/bridge-runtime.d.ts +16 -0
- package/dist/standalone/bridge-runtime.js +46 -0
- package/dist/standalone/bridge-rust.d.ts +16 -0
- package/dist/standalone/bridge-rust.js +89 -0
- package/dist/standalone/build.js +18 -14
- package/dist/standalone/daemon.js +6 -6
- package/dist/standalone/dsm-window.d.ts +10 -5
- package/dist/standalone/dsm-window.js +64 -16
- package/dist/standalone/i18n.js +18 -18
- package/dist/standalone/inspect-bridge.d.ts +31 -0
- package/dist/standalone/inspect-bridge.js +111 -0
- package/dist/standalone/inspect-spk.d.ts +1 -1
- package/dist/standalone/inspect-spk.js +77 -69
- package/dist/standalone/lifecycle.js +22 -3
- package/dist/standalone/manifest.d.ts +17 -9
- package/dist/standalone/manifest.js +143 -65
- package/dist/standalone/portal-window.js +14 -7
- package/dist/standalone/request-timeout.d.ts +9 -0
- package/dist/standalone/request-timeout.js +13 -0
- package/dist/standalone/root-ops.d.ts +23 -2
- package/dist/standalone/root-ops.js +118 -13
- package/dist/standalone/sdk-check.js +4 -4
- package/dist/standalone/shared-folder.js +7 -7
- package/dist/standalone/spk.d.ts +2 -0
- package/dist/standalone/spk.js +30 -11
- package/dist/standalone/uninstall-hook.js +22 -4
- package/dist/standalone/uninstall.js +7 -7
- package/dist/standalone/window-size.d.ts +17 -0
- package/dist/standalone/window-size.js +31 -0
- package/dist/types.d.ts +1 -1
- package/dist/validate.js +13 -13
- package/package.json +2 -2
- package/schema/bridge-manifest.v1.schema.json +86 -0
- package/skills/reference/create.md +76 -2
- package/skills/reference/ui-json.md +2 -1
- package/skills/reference/ui-vue.md +9 -3
- package/skills/reference/wrap.md +1 -1
- package/dist/standalone/rust-build.d.ts +0 -35
- package/dist/standalone/rust-build.js +0 -127
package/dist/compose/analyze.js
CHANGED
|
@@ -320,17 +320,17 @@ function collectPermanentRouteErrors(contract, parsed, composeFile) {
|
|
|
320
320
|
const basePath = `services.${serviceName}`;
|
|
321
321
|
if ("build" in service && service.build !== undefined) {
|
|
322
322
|
const path = `${basePath}.build`;
|
|
323
|
-
errors.push(makeError("COMPOSE_BUILD_FORBIDDEN", composeSpanForPath(compose, composeFile, path), `service
|
|
323
|
+
errors.push(makeError("COMPOSE_BUILD_FORBIDDEN", composeSpanForPath(compose, composeFile, path), `service "${serviceName}" declares build:; the D0 route does not support install-time build.`, "Remove build: and use an existing image (preferably pinned with image@sha256:...), then run check again."));
|
|
324
324
|
}
|
|
325
325
|
if (dsmUserParams.length > 0 && typeof service.user === "string" && service.user.trim() !== "") {
|
|
326
326
|
const path = `${basePath}.user`;
|
|
327
|
-
errors.push(makeError("COMPOSE_USER_CONFLICTS_DSM_USER", composeSpanForPath(compose, composeFile, path), `service
|
|
327
|
+
errors.push(makeError("COMPOSE_USER_CONFLICTS_DSM_USER", composeSpanForPath(compose, composeFile, path), `service "${serviceName}" defines user: "${service.user.trim()}", which conflicts with the dsm_user declared in the contract.`, "Remove the user field from Compose; the generator injects user: \"${NAME_UID}:${NAME_GID}\" and group_add from dsm_user."));
|
|
328
328
|
}
|
|
329
329
|
errors.push(...validateSocketMounts(compose, service, composeFile, `${basePath}.volumes`));
|
|
330
330
|
}
|
|
331
331
|
}
|
|
332
332
|
for (const finding of scanComposePrivilege(compose)) {
|
|
333
|
-
errors.push(makeError(finding.code, composeSpanForPath(compose, composeFile, finding.path), `service
|
|
333
|
+
errors.push(makeError(finding.code, composeSpanForPath(compose, composeFile, finding.path), `service "${finding.serviceName}" uses ${finding.flag}; the D0 route permanently rejects high-privilege capabilities, even when the contract declares them.`, "Remove privileged, host network/PID/IPC, devices or cap_add, and use a package-only static-zero-config Compose."));
|
|
334
334
|
}
|
|
335
335
|
return errors;
|
|
336
336
|
}
|
|
@@ -398,13 +398,13 @@ function collectDynamicRouteError(contract, parsed, composeFile) {
|
|
|
398
398
|
? unsupportedInterpolation
|
|
399
399
|
: undefined;
|
|
400
400
|
const interpolationSuggestion = undeclaredName
|
|
401
|
-
?
|
|
402
|
-
: "
|
|
403
|
-
return makeError("COMPOSE_DYNAMIC_ROUTE_UNSUPPORTED", composeSpanForPath(compose, composeFile, path), "Compose dynamic route
|
|
401
|
+
? `Declare ${undeclaredName} in the synology-app.yaml install with type string or boolean, or remove the reference from Compose.`
|
|
402
|
+
: "Use only the ${NAME} form and declare the matching string or boolean parameter in the synology-app.yaml install.";
|
|
403
|
+
return makeError("COMPOSE_DYNAMIC_ROUTE_UNSUPPORTED", composeSpanForPath(compose, composeFile, path), "The Compose dynamic route contains an input outside the D1/D2 allowlist (" +
|
|
404
404
|
reasonText +
|
|
405
|
-
"
|
|
405
|
+
"); this route is rejected and does not silently fall back to another execution mode.", undeclaredName
|
|
406
406
|
? interpolationSuggestion
|
|
407
|
-
: `${interpolationSuggestion} secret/config
|
|
407
|
+
: `${interpolationSuggestion} secret/config are rejected because the DSM worker does not read docker-compose.override.yml; remove them and run check again.`);
|
|
408
408
|
}
|
|
409
409
|
function parsedComposeForAnalysis(compose) {
|
|
410
410
|
const source = composeSources.get(compose) ?? "";
|
|
@@ -563,7 +563,7 @@ export function validateComposeSecurity(compose, composeFile, options) {
|
|
|
563
563
|
const dependsOn = normalizeDependsOn(service.depends_on);
|
|
564
564
|
for (const dependency of dependsOn) {
|
|
565
565
|
if (!compose.services?.[dependency]) {
|
|
566
|
-
errors.push(makeError("COMPOSE_SERVICE_NOT_FOUND", composeSpanForPath(compose, composeFile, `${basePath}.depends_on`), `Compose
|
|
566
|
+
errors.push(makeError("COMPOSE_SERVICE_NOT_FOUND", composeSpanForPath(compose, composeFile, `${basePath}.depends_on`), `Compose references a service that does not exist: "${dependency}".`, "Fix depends_on, or add the matching service definition."));
|
|
567
567
|
}
|
|
568
568
|
}
|
|
569
569
|
}
|
|
@@ -592,8 +592,8 @@ export function validateComposeNumericCommandArgs(compose, composeFile) {
|
|
|
592
592
|
if (numericStrings.length === 0) {
|
|
593
593
|
continue;
|
|
594
594
|
}
|
|
595
|
-
const quotedValues = numericStrings.map((item) =>
|
|
596
|
-
errors.push(makeError("COMPOSE_NUMERIC_COMMAND_ARG_FORBIDDEN", composeSpanForPath(compose, composeFile, `${basePath}.${field}`),
|
|
595
|
+
const quotedValues = numericStrings.map((item) => `"${item}"`).join(", ");
|
|
596
|
+
errors.push(makeError("COMPOSE_NUMERIC_COMMAND_ARG_FORBIDDEN", composeSpanForPath(compose, composeFile, `${basePath}.${field}`), `The ${field} array contains the pure numeric string ${quotedValues}. This Compose goes through the DSM Docker Project resource worker; the worker may drop the quotes during a YAML round-trip and turn it into an integer, so project acquire fails.`, `Wrap the whole command in one string, for example ${field}: ["sh", "-c", "httpd -f -p 8080 -h /srv"], or join it with the flag as "-p8080".`));
|
|
597
597
|
}
|
|
598
598
|
}
|
|
599
599
|
return errors;
|
|
@@ -616,7 +616,7 @@ function validateSocketMounts(compose, service, composeFile, basePath) {
|
|
|
616
616
|
for (const mount of volumes) {
|
|
617
617
|
const source = mount.split(":")[0]?.trim() ?? "";
|
|
618
618
|
if (isForbiddenSocketMount(source)) {
|
|
619
|
-
errors.push(makeError("COMPOSE_SOCKET_MOUNT_FORBIDDEN", composeSpanForPath(compose, composeFile, `${basePath}.volumes`), "Managed Profile
|
|
619
|
+
errors.push(makeError("COMPOSE_SOCKET_MOUNT_FORBIDDEN", composeSpanForPath(compose, composeFile, `${basePath}.volumes`), "A Managed Profile does not allow mounting the Docker or Container Manager socket.", "Remove the docker.sock/container-manager socket volume mount."));
|
|
620
620
|
}
|
|
621
621
|
}
|
|
622
622
|
return errors;
|
|
@@ -753,29 +753,29 @@ function classifyVolumeSource(compose, mount, namedVolumes, context, composeFile
|
|
|
753
753
|
return null;
|
|
754
754
|
}
|
|
755
755
|
if (context.installParamNames.has(varName)) {
|
|
756
|
-
return makeError("COMPOSE_BIND_SOURCE_FORBIDDEN", span,
|
|
756
|
+
return makeError("COMPOSE_BIND_SOURCE_FORBIDDEN", span, `The parameter "${varName}" cannot be a host bind source.`, "Use ${SYNOPKG_PKGVAR}, ${SYNO_APP_SECRETS_DIR}, an install parameter with type shared_folder, or the platform-resolved type homes slot.");
|
|
757
757
|
}
|
|
758
|
-
return makeError("COMPOSE_BIND_SOURCE_UNDECLARED", span, `volume source
|
|
758
|
+
return makeError("COMPOSE_BIND_SOURCE_UNDECLARED", span, `The volume source references an undeclared parameter "${varName}".`, `Declare ${varName} in the synology-app.yaml install with type shared_folder or homes.`);
|
|
759
759
|
}
|
|
760
760
|
if (isProjectRelativePath(source, projectRoot)) {
|
|
761
761
|
return null;
|
|
762
762
|
}
|
|
763
763
|
if (isLiteralHostPath(source)) {
|
|
764
|
-
return makeError("COMPOSE_HOST_BIND_FORBIDDEN", span, "
|
|
764
|
+
return makeError("COMPOSE_HOST_BIND_FORBIDDEN", span, "Writing /volume*, an absolute path or a developer machine path as a Compose volume source is forbidden.", "Use an install parameter with type: shared_folder or homes and take ${PARAM} as the bind source.");
|
|
765
765
|
}
|
|
766
766
|
if (mount.type === "tmpfs") {
|
|
767
767
|
return null;
|
|
768
768
|
}
|
|
769
769
|
if (mount.type === "bind") {
|
|
770
|
-
return makeError("COMPOSE_BIND_SOURCE_FORBIDDEN", span, "
|
|
770
|
+
return makeError("COMPOSE_BIND_SOURCE_FORBIDDEN", span, "Mounting a host source the platform does not allow with type: bind is forbidden.", "Use ${SYNOPKG_PKGVAR}, ${SYNO_APP_SECRETS_DIR} or a ${shared_folder parameter} from the contract.");
|
|
771
771
|
}
|
|
772
772
|
if (!namedVolumes.has(source)) {
|
|
773
|
-
return makeError("COMPOSE_NAMED_VOLUME_UNDECLARED", span,
|
|
773
|
+
return makeError("COMPOSE_NAMED_VOLUME_UNDECLARED", span, `The short-syntax named volume "${source}" is not declared in the top-level volumes, so it is unclear whether it is a Docker volume or a Share name.`, `Declare ${source} in the top-level volumes, or use \${PARAM} as the shared_folder bind source.`);
|
|
774
774
|
}
|
|
775
775
|
return null;
|
|
776
776
|
}
|
|
777
777
|
function invalidBindSuffix(span) {
|
|
778
|
-
return makeError("COMPOSE_BIND_SUFFIX_INVALID", span, "volume source
|
|
778
|
+
return makeError("COMPOSE_BIND_SUFFIX_INVALID", span, "The path suffix of the volume source is invalid.", "The suffix can only be / followed by one or more [A-Za-z0-9._-] segments, and must not contain .., ${ or an absolute path.");
|
|
779
779
|
}
|
|
780
780
|
function isValidatedBindSuffix(suffix) {
|
|
781
781
|
if (suffix === "") {
|
|
@@ -853,18 +853,18 @@ export function validateComposeRunAs(compose, composeFile, context) {
|
|
|
853
853
|
const user = raw.trim();
|
|
854
854
|
const span = composeSpanForPath(compose, composeFile, `services.${serviceName}.user`);
|
|
855
855
|
if (context.hasDsmUser) {
|
|
856
|
-
errors.push(makeError("COMPOSE_USER_CONFLICTS_DSM_USER", span, `service
|
|
856
|
+
errors.push(makeError("COMPOSE_USER_CONFLICTS_DSM_USER", span, `service "${serviceName}" defines user: "${user}", which conflicts with the dsm_user declared in the contract.`, "Remove the user field from Compose; the generator injects user: \"${NAME_UID}:${NAME_GID}\" and group_add from dsm_user."));
|
|
857
857
|
continue;
|
|
858
858
|
}
|
|
859
859
|
if (user.includes("${")) {
|
|
860
860
|
continue;
|
|
861
861
|
}
|
|
862
862
|
if (context.hasSharedFolder && ROOT_USERS.has(user)) {
|
|
863
|
-
errors.push(makeError("COMPOSE_USER_ROOT_FORBIDDEN", span, "
|
|
863
|
+
errors.push(makeError("COMPOSE_USER_ROOT_FORBIDDEN", span, "With a Shared Folder, a container must not write to the Share as uid 0/root.", "Declare dsm_user and use ${NAME_UID}:${NAME_GID} as the Compose user.", "warning"));
|
|
864
864
|
continue;
|
|
865
865
|
}
|
|
866
866
|
if (NUMERIC_USER.test(user)) {
|
|
867
|
-
errors.push(makeError("COMPOSE_USER_NUMERIC_FORBIDDEN", span, "
|
|
867
|
+
errors.push(makeError("COMPOSE_USER_NUMERIC_FORBIDDEN", span, "The contract forbids hard-coded numeric uids.", "Use ${NAME_UID}:${NAME_GID} from the install dsm_user parameter."));
|
|
868
868
|
}
|
|
869
869
|
}
|
|
870
870
|
return errors;
|
|
@@ -873,14 +873,14 @@ export function validateComposeRunAs(compose, composeFile, context) {
|
|
|
873
873
|
export function validateGatewayPortLoopback(adminport, compose, composeFile) {
|
|
874
874
|
return listPortExposures(compose)
|
|
875
875
|
.filter((exposure) => exposure.hostPort === adminport && !exposure.loopbackOnly)
|
|
876
|
-
.map((exposure) => makeError("COMPOSE_GATEWAY_PORT_EXPOSED", composeSpanForPath(compose, composeFile, `services.${exposure.service}.ports`),
|
|
876
|
+
.map((exposure) => makeError("COMPOSE_GATEWAY_PORT_EXPOSED", composeSpanForPath(compose, composeFile, `services.${exposure.service}.ports`), `The port ${exposure.entry} of ${exposure.service} binds to every network interface, so the container is reachable without DSM verification.`, `Bind it to loopback only: \"127.0.0.1:${adminport}:${exposure.containerPort ?? adminport}\", or choose public explicitly in integration.web.access in synology-app.yaml.`));
|
|
877
877
|
}
|
|
878
878
|
export function validateAdminPortMapping(adminport, compose, composeFile) {
|
|
879
879
|
const published = collectPublishedTcpPorts(compose);
|
|
880
880
|
const services = published.get(adminport) ?? [];
|
|
881
881
|
if (services.length !== 1) {
|
|
882
882
|
return [
|
|
883
|
-
makeError("CONTRACT_ADMINPORT_UNMAPPED", composeSpanForPath(compose, composeFile, `adminport:${adminport}`), `adminport ${adminport}
|
|
883
|
+
makeError("CONTRACT_ADMINPORT_UNMAPPED", composeSpanForPath(compose, composeFile, `adminport:${adminport}`), `adminport ${adminport} must map to exactly one published TCP port in Compose.`, "Publish the same host port as adminport in compose.yaml and make sure only one service uses it."),
|
|
884
884
|
];
|
|
885
885
|
}
|
|
886
886
|
return [];
|
|
@@ -9,33 +9,33 @@ export const PRIVILEGE_FLAGS = [
|
|
|
9
9
|
const FLAG_ERRORS = {
|
|
10
10
|
privileged: {
|
|
11
11
|
code: "COMPOSE_PRIVILEGED_FORBIDDEN",
|
|
12
|
-
message: "Compose
|
|
13
|
-
suggestion: "
|
|
12
|
+
message: "Compose uses a privileged container but the contract does not declare privilege.privileged.",
|
|
13
|
+
suggestion: "Add privilege.privileged: true to synology-app.yaml, or remove privileged: true.",
|
|
14
14
|
},
|
|
15
15
|
host_network: {
|
|
16
16
|
code: "COMPOSE_HOST_NETWORK_FORBIDDEN",
|
|
17
|
-
message: "Compose
|
|
18
|
-
suggestion: "
|
|
17
|
+
message: "Compose uses host network but the contract does not declare privilege.host_network.",
|
|
18
|
+
suggestion: "Add privilege.host_network: true to synology-app.yaml, or use a controlled network mode.",
|
|
19
19
|
},
|
|
20
20
|
host_pid: {
|
|
21
21
|
code: "COMPOSE_HOST_PID_FORBIDDEN",
|
|
22
|
-
message: "Compose
|
|
23
|
-
suggestion: "
|
|
22
|
+
message: "Compose uses host PID but the contract does not declare privilege.host_pid.",
|
|
23
|
+
suggestion: "Add privilege.host_pid: true to synology-app.yaml, or remove pid: host.",
|
|
24
24
|
},
|
|
25
25
|
host_ipc: {
|
|
26
26
|
code: "COMPOSE_HOST_IPC_FORBIDDEN",
|
|
27
|
-
message: "Compose
|
|
28
|
-
suggestion: "
|
|
27
|
+
message: "Compose uses host IPC but the contract does not declare privilege.host_ipc.",
|
|
28
|
+
suggestion: "Add privilege.host_ipc: true to synology-app.yaml, or remove ipc: host.",
|
|
29
29
|
},
|
|
30
30
|
devices: {
|
|
31
31
|
code: "COMPOSE_DEVICES_FORBIDDEN",
|
|
32
|
-
message: "Compose
|
|
33
|
-
suggestion: "
|
|
32
|
+
message: "Compose mounts devices but the contract does not declare privilege.devices.",
|
|
33
|
+
suggestion: "Add privilege.devices: true to synology-app.yaml, or remove devices.",
|
|
34
34
|
},
|
|
35
35
|
cap_add: {
|
|
36
36
|
code: "COMPOSE_CAP_ADD_FORBIDDEN",
|
|
37
|
-
message: "Compose
|
|
38
|
-
suggestion: "
|
|
37
|
+
message: "Compose uses extra cap_add but the contract does not declare privilege.cap_add.",
|
|
38
|
+
suggestion: "Add privilege.cap_add: true to synology-app.yaml, or remove cap_add.",
|
|
39
39
|
},
|
|
40
40
|
};
|
|
41
41
|
export function declaredPrivilegeFlags(contract) {
|
package/dist/contract/i18n.js
CHANGED
|
@@ -28,12 +28,12 @@ export function validateContractI18n(parsed, contractFile) {
|
|
|
28
28
|
const errors = [];
|
|
29
29
|
const invalid = (why) => {
|
|
30
30
|
const pos = getValuePosition(doc, ["i18n"]) ?? { line: 1, column: 1 };
|
|
31
|
-
errors.push(makeError("CONTRACT_I18N_INVALID", contractSpan(contractFile, pos.line, pos.column, "i18n"), `i18n ${why}`, "i18n
|
|
31
|
+
errors.push(makeError("CONTRACT_I18N_INVALID", contractSpan(contractFile, pos.line, pos.column, "i18n"), `i18n ${why}`, "i18n accepts only info; info is keyed by a supported language such as zh-TW or ja with single-line displayname / description values without \" or \\ (English goes at the top level)."));
|
|
32
32
|
return undefined;
|
|
33
33
|
};
|
|
34
|
-
const text = (value, field) => plain(value) ? value : invalid(`${field}
|
|
34
|
+
const text = (value, field) => plain(value) ? value : invalid(`${field} must be single-line, non-empty text without " or \\.`);
|
|
35
35
|
if (!isRecord(contract.i18n) || Object.keys(contract.i18n).some((key) => key !== "info")) {
|
|
36
|
-
return invalid("
|
|
36
|
+
return invalid("Only info is accepted (per-language displayname and description).") ?? errors;
|
|
37
37
|
}
|
|
38
38
|
const info = readInfoTranslations(contract.i18n.info, { invalid: (why) => invalid(`.info ${why}`), text });
|
|
39
39
|
if (info && Object.keys(info).length > 0) {
|
package/dist/contract/icon.js
CHANGED
|
@@ -11,11 +11,11 @@ export function validateContractIcon(parsed, contractFile) {
|
|
|
11
11
|
if (contract.icon === undefined || !contractPath)
|
|
12
12
|
return [];
|
|
13
13
|
const pos = getValuePosition(doc, ["icon"]) ?? { line: 1, column: 1 };
|
|
14
|
-
const invalid = (message) => [makeError("CONTRACT_ICON_INVALID", contractSpan(contractFile, pos.line, pos.column, "icon"), `icon
|
|
14
|
+
const invalid = (message) => [makeError("CONTRACT_ICON_INVALID", contractSpan(contractFile, pos.line, pos.column, "icon"), `icon: ${message}`, "Set icon to a square PNG inside the project (at least 256x256), or a directory of icon_<size>.png including icon_256.png; omit it to use the SDK default icon.")];
|
|
15
15
|
const root = dirname(contractPath);
|
|
16
16
|
const rel = typeof contract.icon === "string" && contract.icon.trim() !== "" ? relative(root, resolve(root, contract.icon)) : undefined;
|
|
17
17
|
if (rel === undefined || isAbsolute(String(contract.icon)) || rel.startsWith("..") || isAbsolute(rel)) {
|
|
18
|
-
return invalid(
|
|
18
|
+
return invalid(`"${String(contract.icon)}" must be a relative path inside the project directory.`);
|
|
19
19
|
}
|
|
20
20
|
try {
|
|
21
21
|
renderPackageIcons(resolve(root, rel));
|
|
@@ -39,12 +39,12 @@ export function validatePosixAllSharesSources(compose, composeFile, declaredPara
|
|
|
39
39
|
const path = `services.${serviceName}.volumes.${index}`;
|
|
40
40
|
if (declaredParameters.has(name)) {
|
|
41
41
|
if (suffix !== "") {
|
|
42
|
-
errors.push(makeError("COMPOSE_POSIX_ALL_SHARES_SUFFIX_FORBIDDEN", composeSpan(composeFile, 1, 1, path), `POSIX all-shares
|
|
42
|
+
errors.push(makeError("COMPOSE_POSIX_ALL_SHARES_SUFFIX_FORBIDDEN", composeSpan(composeFile, 1, 1, path), `The POSIX all-shares parameter "${name}" can only be the complete volume source, with no subpath.`, `Change ${source} to \${${name}} and keep the Compose container target; the package-only preinst expands the Share names at install time.`));
|
|
43
43
|
}
|
|
44
44
|
continue;
|
|
45
45
|
}
|
|
46
46
|
if (RESERVED_MARKER_NAMES.has(name)) {
|
|
47
|
-
errors.push(makeError("COMPOSE_POSIX_ALL_SHARES_UNDECLARED", composeSpan(composeFile, 1, 1, path), `Compose
|
|
47
|
+
errors.push(makeError("COMPOSE_POSIX_ALL_SHARES_UNDECLARED", composeSpan(composeFile, 1, 1, path), `Compose uses an undeclared POSIX all-shares marker "${name}".`, `Declare install.${name}.type: ${POSIX_ALL_SHARES_TYPE} in synology-app.yaml and let the wizard/CLI take the Share list at install time.`));
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
}
|
|
@@ -20,14 +20,14 @@ function mapSchemaError(error, contractFile, parsed) {
|
|
|
20
20
|
const path = error.instancePath.replace(/^\//, "").replace(/\//g, ".") || "schema";
|
|
21
21
|
const pos = getValuePosition(parsed.doc, path.split(".").filter(Boolean)) ?? { line: 1, column: 1 };
|
|
22
22
|
if (error.keyword === "const" && path === "schema") {
|
|
23
|
-
return makeError("CONTRACT_SCHEMA_UNSUPPORTED", contractSpan(contractFile, pos.line, pos.column, "schema"), "
|
|
23
|
+
return makeError("CONTRACT_SCHEMA_UNSUPPORTED", contractSpan(contractFile, pos.line, pos.column, "schema"), "Unsupported App Contract schema version.", "Set schema to the integer 1.");
|
|
24
24
|
}
|
|
25
25
|
if (error.keyword === "required") {
|
|
26
26
|
const missing = String(error.params.missingProperty);
|
|
27
27
|
const missingPos = getValuePosition(parsed.doc, [missing]) ?? { line: 1, column: 1 };
|
|
28
|
-
return makeError("CONTRACT_MISSING_FIELD", contractSpan(contractFile, missingPos.line, missingPos.column, missing),
|
|
28
|
+
return makeError("CONTRACT_MISSING_FIELD", contractSpan(contractFile, missingPos.line, missingPos.column, missing), `Missing required field "${missing}".`, `Add ${missing} to synology-app.yaml.`);
|
|
29
29
|
}
|
|
30
|
-
return makeError("CONTRACT_SCHEMA_INVALID", contractSpan(contractFile, pos.line, pos.column, path), `App Contract
|
|
30
|
+
return makeError("CONTRACT_SCHEMA_INVALID", contractSpan(contractFile, pos.line, pos.column, path), `App Contract does not match the JSON Schema: ${error.message ?? error.keyword}.`, "Fix the field formats in synology-app.yaml.");
|
|
31
31
|
}
|
|
32
32
|
export function validateContractSchema(parsed, contractFile) {
|
|
33
33
|
const validate = getValidator();
|
|
@@ -37,18 +37,18 @@ export function validateContract(parsed, contractFile) {
|
|
|
37
37
|
// does not.
|
|
38
38
|
if (typeof contract.package === "string" && contract.package.length > DSM_PACKAGE_IDENTITY_MAX) {
|
|
39
39
|
const pos = getValuePosition(doc, ["package"]) ?? { line: 1, column: 1 };
|
|
40
|
-
errors.push(makeError("CONTRACT_PACKAGE_TOO_LONG", contractSpan(contractFile, pos.line, pos.column, "package"), `Package Identity
|
|
40
|
+
errors.push(makeError("CONTRACT_PACKAGE_TOO_LONG", contractSpan(contractFile, pos.line, pos.column, "package"), `The Package Identity exceeds the DSM limit of ${DSM_PACKAGE_IDENTITY_MAX} characters (currently ${contract.package.length}).`, "Shorten package, or Package Center rejects the install with \"Invalid file format\"."));
|
|
41
41
|
}
|
|
42
42
|
// Formal Package Identity length limit (28 characters to reserve 4 characters for .dev suffix)
|
|
43
43
|
if (typeof contract.package === "string" &&
|
|
44
44
|
contract.package.length > FORMAL_PACKAGE_IDENTITY_MAX_LENGTH) {
|
|
45
45
|
const pos = getValuePosition(doc, ["package"]) ?? { line: 1, column: 1 };
|
|
46
|
-
errors.push(makeError("PACKAGE_IDENTITY_TOO_LONG", contractSpan(contractFile, pos.line, pos.column, "package"), `Package Identity
|
|
46
|
+
errors.push(makeError("PACKAGE_IDENTITY_TOO_LONG", contractSpan(contractFile, pos.line, pos.column, "package"), `The Package Identity length ${contract.package.length} exceeds the formal identity limit of ${FORMAL_PACKAGE_IDENTITY_MAX_LENGTH} characters (the DSM limit is 32, and .dev needs 4 reserved).`, `Shorten package to ${FORMAL_PACKAGE_IDENTITY_MAX_LENGTH} characters or fewer for --dev deploys (limit ${PACKAGE_IDENTITY_MAX_LENGTH} characters).`));
|
|
47
47
|
}
|
|
48
48
|
if (contract.adminport !== undefined) {
|
|
49
49
|
if (typeof contract.adminport !== "number" || !Number.isInteger(contract.adminport)) {
|
|
50
50
|
const pos = getValuePosition(doc, ["adminport"]) ?? { line: 1, column: 1 };
|
|
51
|
-
errors.push(makeError("CONTRACT_ADMINPORT_INVALID", contractSpan(contractFile, pos.line, pos.column, "adminport"), "adminport
|
|
51
|
+
errors.push(makeError("CONTRACT_ADMINPORT_INVALID", contractSpan(contractFile, pos.line, pos.column, "adminport"), "adminport must be an integer.", "Provide a numeric adminport that matches the Compose published port."));
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
errors.push(...validateInstallParameters(parsed, contractFile));
|
|
@@ -85,11 +85,11 @@ function validateWebLaunch(parsed, contractFile) {
|
|
|
85
85
|
}
|
|
86
86
|
const pos = getValuePosition(doc, ["integration", "web", "launch"]) ?? { line: 1, column: 1 };
|
|
87
87
|
if (!isWebLaunchMode(web.launch)) {
|
|
88
|
-
errors.push(makeError("CONTRACT_LAUNCH_INVALID", contractSpan(contractFile, pos.line, pos.column, "integration.web.launch"), "integration.web.launch
|
|
88
|
+
errors.push(makeError("CONTRACT_LAUNCH_INVALID", contractSpan(contractFile, pos.line, pos.column, "integration.web.launch"), "integration.web.launch can only be dsm-window or new-page.", "Use dsm-window (DSM window, the default) or new-page (new tab); omit this field for a headless App."));
|
|
89
89
|
return errors;
|
|
90
90
|
}
|
|
91
91
|
if (contract.adminport === undefined) {
|
|
92
|
-
errors.push(makeError("CONTRACT_LAUNCH_REQUIRES_WEB", contractSpan(contractFile, pos.line, pos.column, "integration.web.launch"), "
|
|
92
|
+
errors.push(makeError("CONTRACT_LAUNCH_REQUIRES_WEB", contractSpan(contractFile, pos.line, pos.column, "integration.web.launch"), "A headless App does not need a Launch Mode.", "Remove integration.web.launch, or add the adminport that matches the Compose published port."));
|
|
93
93
|
}
|
|
94
94
|
return errors;
|
|
95
95
|
}
|
|
@@ -105,12 +105,12 @@ function validateWebAccess(parsed, contractFile) {
|
|
|
105
105
|
};
|
|
106
106
|
if (contract.adminport === undefined) {
|
|
107
107
|
return [
|
|
108
|
-
makeError("CONTRACT_ACCESS_REQUIRES_WEB", at(web.access !== undefined ? "access" : "groups"), "
|
|
108
|
+
makeError("CONTRACT_ACCESS_REQUIRES_WEB", at(web.access !== undefined ? "access" : "groups"), "A headless App has no web entry, so it does not need access or groups.", "Remove integration.web.access/groups, or add the adminport that matches the Compose published port."),
|
|
109
109
|
];
|
|
110
110
|
}
|
|
111
111
|
if (web.access !== undefined && !isWebAccessMode(web.access)) {
|
|
112
112
|
return [
|
|
113
|
-
makeError("CONTRACT_ACCESS_INVALID", at("access"), "integration.web.access
|
|
113
|
+
makeError("CONTRACT_ACCESS_INVALID", at("access"), "integration.web.access can only be dsm-users, dsm-admins, dsm-groups or public.", "Use dsm-users (signed-in DSM users, the default), dsm-admins, dsm-groups (with groups), or public to keep an anonymous Web Station entry."),
|
|
114
114
|
];
|
|
115
115
|
}
|
|
116
116
|
const groups = web.groups;
|
|
@@ -119,7 +119,7 @@ function validateWebAccess(parsed, contractFile) {
|
|
|
119
119
|
groups.every((g) => typeof g === "string" && DSM_GROUP_NAME.test(g));
|
|
120
120
|
if (web.access === "dsm-groups" ? !groupsValid : groups !== undefined) {
|
|
121
121
|
return [
|
|
122
|
-
makeError("CONTRACT_ACCESS_GROUPS_INVALID", at("groups"), "integration.web.groups
|
|
122
|
+
makeError("CONTRACT_ACCESS_GROUPS_INVALID", at("groups"), "integration.web.groups needs access: dsm-groups and must be a non-empty list of DSM local groups or full DOMAIN\\group/group@domain names (domain up to 64, group up to 128 characters).", "Use access: dsm-groups and list the group names, or remove groups."),
|
|
123
123
|
];
|
|
124
124
|
}
|
|
125
125
|
return [];
|
|
@@ -133,17 +133,17 @@ function validateWebPortal(parsed, contractFile) {
|
|
|
133
133
|
const pos = getValuePosition(doc, ["integration", "web", "portal"]) ?? { line: 1, column: 1 };
|
|
134
134
|
// A Compose App has no root executor, and only its purge can stop nginx serving the alias after uninstall.
|
|
135
135
|
return [
|
|
136
|
-
makeError("CONTRACT_PORTAL_REQUIRES_EXECUTOR", contractSpan(contractFile, pos.line, pos.column, "integration.web.portal"), "Compose App
|
|
136
|
+
makeError("CONTRACT_PORTAL_REQUIRES_EXECUTOR", contractSpan(contractFile, pos.line, pos.column, "integration.web.portal"), "A Compose App has no executor and cannot declare an Application Portal: after uninstall DSM does not reload nginx, and only root can remove the alias.", "Remove integration.web.portal; for a sign-in alias, ask an administrator to configure a reverse proxy in DSM."),
|
|
137
137
|
];
|
|
138
138
|
}
|
|
139
139
|
function validateDeveloperString(value, contractFile, path, pos) {
|
|
140
140
|
const errors = [];
|
|
141
141
|
const violations = stringViolations(value);
|
|
142
142
|
if (violations.hasControlChars) {
|
|
143
|
-
errors.push(makeError("CONTRACT_INVALID_STRING", contractSpan(contractFile, pos.line, pos.column, path), "
|
|
143
|
+
errors.push(makeError("CONTRACT_INVALID_STRING", contractSpan(contractFile, pos.line, pos.column, path), "The contract string contains a disallowed control character.", "Remove the control character and validate again."));
|
|
144
144
|
}
|
|
145
145
|
if (violations.hasInterpolation) {
|
|
146
|
-
errors.push(makeError("CONTRACT_INVALID_STRING", contractSpan(contractFile, pos.line, pos.column, path), "
|
|
146
|
+
errors.push(makeError("CONTRACT_INVALID_STRING", contractSpan(contractFile, pos.line, pos.column, path), "A contract string must not contain ${ interpolation.", "Use a fixed literal value and declare install parameters in the install block."));
|
|
147
147
|
}
|
|
148
148
|
return errors;
|
|
149
149
|
}
|
|
@@ -157,10 +157,10 @@ function validateInstallParameters(parsed, contractFile) {
|
|
|
157
157
|
const pos = getValuePosition(doc, ["install", name, "default"]) ?? { line: 1, column: 1 };
|
|
158
158
|
const violations = stringViolations(param.default);
|
|
159
159
|
if (violations.tooLong) {
|
|
160
|
-
errors.push(makeError("INSTALL_STRING_TOO_LONG", contractSpan(contractFile, pos.line, pos.column, `${basePath}.default`), `Install Parameter
|
|
160
|
+
errors.push(makeError("INSTALL_STRING_TOO_LONG", contractSpan(contractFile, pos.line, pos.column, `${basePath}.default`), `The Install Parameter "${name}" default exceeds 256 characters.`, "Shorten the string default."));
|
|
161
161
|
}
|
|
162
162
|
if (violations.hasControlChars || violations.hasInterpolation) {
|
|
163
|
-
errors.push(makeError("INSTALL_STRING_INVALID", contractSpan(contractFile, pos.line, pos.column, `${basePath}.default`), `Install Parameter
|
|
163
|
+
errors.push(makeError("INSTALL_STRING_INVALID", contractSpan(contractFile, pos.line, pos.column, `${basePath}.default`), `The Install Parameter "${name}" string value is invalid.`, "Remove control characters and ${ interpolation."));
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
// A45/A46: both types are in the schema enum but neither has a working
|
|
@@ -181,7 +181,7 @@ function validateInstallParameters(parsed, contractFile) {
|
|
|
181
181
|
// script generation -- turning those red would cost real coverage for no
|
|
182
182
|
// extra safety. Nothing unshippable can reach a NAS or the community repo,
|
|
183
183
|
// because both gates refuse; the author still learns at validate time.
|
|
184
|
-
errors.push(makeError(code, contractSpan(contractFile, pos.line, pos.column, `${basePath}.type`), `Install Parameter
|
|
184
|
+
errors.push(makeError(code, contractSpan(contractFile, pos.line, pos.column, `${basePath}.type`), `Install Parameter "${name}" with type: ${type} has no usable install path today: a Package Center install ends in DSM broken/150, and deploy and publish refuse it.`, entry?.suggestion ?? "Use another install parameter type.", "warning"));
|
|
185
185
|
// Deliberately no `continue`: the type-specific checks below still apply.
|
|
186
186
|
// Skipping them once silently dropped CONTRACT_HOMES_PATH_FORBIDDEN.
|
|
187
187
|
}
|
|
@@ -195,7 +195,7 @@ function validateInstallParameters(parsed, contractFile) {
|
|
|
195
195
|
const pos = getValuePosition(doc, ["install", name, field]) ??
|
|
196
196
|
getValuePosition(doc, ["install", name]) ??
|
|
197
197
|
{ line: 1, column: 1 };
|
|
198
|
-
errors.push(makeError("CONTRACT_HOMES_PATH_FORBIDDEN", contractSpan(contractFile, pos.line, pos.column, `${basePath}.${field}`), `homes Install Parameter
|
|
198
|
+
errors.push(makeError("CONTRACT_HOMES_PATH_FORBIDDEN", contractSpan(contractFile, pos.line, pos.column, `${basePath}.${field}`), `The homes Install Parameter "${name}" must not set a NAS physical path or default in the contract field "${field}".`, `Keep install.${name}.type: homes and reference only \${${name}} in Compose; the package resolves the path from this NAS's User Home setting at install time.`));
|
|
199
199
|
}
|
|
200
200
|
if (isPosixAllSharesType(type)) {
|
|
201
201
|
const configuredValue = Object.entries(param).find(([key, value]) => key !== "type" && (key === "default" || value !== undefined));
|
|
@@ -204,7 +204,7 @@ function validateInstallParameters(parsed, contractFile) {
|
|
|
204
204
|
const pos = getValuePosition(doc, ["install", name, field]) ??
|
|
205
205
|
getValuePosition(doc, ["install", name]) ??
|
|
206
206
|
{ line: 1, column: 1 };
|
|
207
|
-
errors.push(makeError("CONTRACT_POSIX_ALL_SHARES_VALUE_FORBIDDEN", contractSpan(contractFile, pos.line, pos.column, `${basePath}.${field}`), `POSIX all-shares Install Parameter
|
|
207
|
+
errors.push(makeError("CONTRACT_POSIX_ALL_SHARES_VALUE_FORBIDDEN", contractSpan(contractFile, pos.line, pos.column, `${basePath}.${field}`), `The POSIX all-shares Install Parameter "${name}" must not set a default or NAS physical path; the Share list is snapshotted by the CLI through the DSM API only at install time.`, `Keep only install.${name}.type: posix_all_shares and use \${${name}} in Compose as the complete volume source.`));
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
210
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PackageIcons } from "../build-spk/dsm-icons.js";
|
|
1
2
|
import type { AppContract } from "../types.js";
|
|
2
3
|
/** DSM Web Station portal alias max length (error 1311 above this). */
|
|
3
4
|
export declare const WEB_STATION_ALIAS_MAX = 20;
|
|
@@ -44,4 +45,4 @@ export declare function cookieStripHeaders(contract: AppContract): Array<{
|
|
|
44
45
|
export declare function webStationServiceName(alias: string): string;
|
|
45
46
|
export declare function webStationServiceRoot(alias: string): string;
|
|
46
47
|
export declare function renderWebStationResource(contract: AppContract): string;
|
|
47
|
-
export declare function renderDsmWindowConfig(contract: AppContract): string;
|
|
48
|
+
export declare function renderDsmWindowConfig(contract: AppContract, icons: PackageIcons): string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { uiTitleFields } from "../build-spk/ui-texts.js";
|
|
3
|
+
import { windowIconTemplate } from "../build-spk/dsm-icons.js";
|
|
3
4
|
import { contractAppTitle } from "./i18n.js";
|
|
4
5
|
/** DSM Web Station portal alias max length (error 1311 above this). */
|
|
5
6
|
export const WEB_STATION_ALIAS_MAX = 20;
|
|
@@ -139,7 +140,7 @@ export function renderWebStationResource(contract) {
|
|
|
139
140
|
}
|
|
140
141
|
return `${JSON.stringify({ webservice: { services: [service], portals: [portal] } }, null, 2)}\n`;
|
|
141
142
|
}
|
|
142
|
-
export function renderDsmWindowConfig(contract) {
|
|
143
|
+
export function renderDsmWindowConfig(contract, icons) {
|
|
143
144
|
const packageId = String(contract.package);
|
|
144
145
|
return `${JSON.stringify({
|
|
145
146
|
".url": {
|
|
@@ -147,7 +148,7 @@ export function renderDsmWindowConfig(contract) {
|
|
|
147
148
|
type: "url",
|
|
148
149
|
allUsers: true,
|
|
149
150
|
...uiTitleFields(contractAppTitle(contract)),
|
|
150
|
-
icon:
|
|
151
|
+
icon: windowIconTemplate(icons),
|
|
151
152
|
url: trustedAppEntryPath(contract),
|
|
152
153
|
},
|
|
153
154
|
},
|
package/dist/deploy/deploy.js
CHANGED
|
@@ -25,11 +25,11 @@ export async function deployProject(options) {
|
|
|
25
25
|
return {
|
|
26
26
|
ok: false,
|
|
27
27
|
code: first.code,
|
|
28
|
-
message: `Install Parameter
|
|
29
|
-
"Package Center
|
|
28
|
+
message: `The Install Parameter "${first.name}" with type: ${first.type} has no usable install path today, ` +
|
|
29
|
+
"a Package Center install ends in DSM broken/150, so deploy refuses to deploy." +
|
|
30
30
|
(first.type === "homes"
|
|
31
|
-
? "
|
|
32
|
-
: "
|
|
31
|
+
? "If you need an administrator-chosen folder, use type: shared_folder."
|
|
32
|
+
: "Use type: string, or have the App read the file from ${SYNO_APP_SECRETS_DIR}."),
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
35
|
const validation = validateProject(options.project);
|
|
@@ -41,7 +41,7 @@ export async function deployProject(options) {
|
|
|
41
41
|
return {
|
|
42
42
|
ok: false,
|
|
43
43
|
code: "PACKAGE_IDENTITY_TOO_LONG",
|
|
44
|
-
message:
|
|
44
|
+
message: `The final Package Identity "${identity.packageId}" length ${identity.packageId.length} exceeds the DSM limit of ${PACKAGE_IDENTITY_MAX_LENGTH} characters.`,
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
const packageInstalled = await options.host.isPackageInstalled(identity.packageId);
|
|
@@ -66,7 +66,7 @@ export async function deployProject(options) {
|
|
|
66
66
|
return {
|
|
67
67
|
ok: false,
|
|
68
68
|
code: POSIX_ALL_SHARES_LOOKUP_FAILED,
|
|
69
|
-
message:
|
|
69
|
+
message: `The Shared Folders of this NAS could not be listed with an administrator session, so the POSIX all-shares parameter "${name}" cannot be filled.`,
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
72
|
paramValues[name] = names.join(",");
|
|
@@ -171,7 +171,7 @@ export async function deployProject(options) {
|
|
|
171
171
|
return {
|
|
172
172
|
ok: false,
|
|
173
173
|
code: "NATIVE_ARCH_UNDETERMINED",
|
|
174
|
-
message: "
|
|
174
|
+
message: "The CPU architecture could not be determined from this NAS. Specify --arch x86_64 or --arch aarch64 explicitly.",
|
|
175
175
|
};
|
|
176
176
|
}
|
|
177
177
|
}
|
|
@@ -179,7 +179,7 @@ export async function deployProject(options) {
|
|
|
179
179
|
return {
|
|
180
180
|
ok: false,
|
|
181
181
|
code: "CONTAINER_ARCH_UNSUPPORTED",
|
|
182
|
-
message: `Container Manager
|
|
182
|
+
message: `Container Manager Apps support only ${CONTAINER_ARCH_FAMILIES.join(", ")}; DSM does not provide Container Manager for ${archFamily}.`,
|
|
183
183
|
};
|
|
184
184
|
}
|
|
185
185
|
const build = await buildSpk({
|
|
@@ -19,7 +19,7 @@ export async function resolveHomesExtraValues(input) {
|
|
|
19
19
|
return {
|
|
20
20
|
ok: false,
|
|
21
21
|
code: "HOMES_NOT_ENABLED",
|
|
22
|
-
message: "
|
|
22
|
+
message: "This NAS has DSM User Home disabled, or the DSM API cannot resolve the user home path; an App that needs homes cannot be installed. Enable User Home in DSM and retry.",
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
25
|
return {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const PACKAGE_IDENTITY_MAX_LENGTH = 32;
|
|
2
2
|
export const FORMAL_PACKAGE_IDENTITY_MAX_LENGTH = 28;
|
|
3
3
|
const DEV_SUFFIX = ".dev";
|
|
4
|
-
const DEV_DISPLAY_SUFFIX = "
|
|
4
|
+
const DEV_DISPLAY_SUFFIX = " (Dev)";
|
|
5
5
|
export function developmentPackageIdentity(packageIdentity) {
|
|
6
6
|
return `${packageIdentity}${DEV_SUFFIX}`;
|
|
7
7
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const USAGE = "synopkgland deploy --param delegate_share=<shared folder
|
|
1
|
+
const USAGE = "synopkgland deploy --param delegate_share=<shared folder>, or --param delegate_home=true";
|
|
2
2
|
/** Mirrors the preinst draft check in build-spk/delegate-scope.ts. */
|
|
3
3
|
function validShareName(name) {
|
|
4
4
|
return (name !== "" && name !== "." && !name.includes("..") &&
|
|
@@ -17,7 +17,7 @@ export function resolveInstallParameterExtraValues(input) {
|
|
|
17
17
|
return {
|
|
18
18
|
ok: false,
|
|
19
19
|
code: "DEPLOY_PARAM_UNDECLARED",
|
|
20
|
-
message: `--param
|
|
20
|
+
message: `--param provided an undeclared Install Parameter "${name}". Only string/boolean names declared in synology-app.yaml install are accepted.`,
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
if (param.type === "boolean") {
|
|
@@ -25,7 +25,7 @@ export function resolveInstallParameterExtraValues(input) {
|
|
|
25
25
|
return {
|
|
26
26
|
ok: false,
|
|
27
27
|
code: "DEPLOY_PARAM_TYPE_INVALID",
|
|
28
|
-
message: `--param ${name}=...
|
|
28
|
+
message: `--param ${name}=... must be the boolean true or false; DSM multiselect passes these two strings to Installation/install.`,
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
31
|
extraValues[name] = raw;
|
|
@@ -35,7 +35,7 @@ export function resolveInstallParameterExtraValues(input) {
|
|
|
35
35
|
return {
|
|
36
36
|
ok: false,
|
|
37
37
|
code: "DEPLOY_PARAM_TYPE_INVALID",
|
|
38
|
-
message: `--param ${name}=...
|
|
38
|
+
message: `--param ${name}=... maps to contract type ${String(param.type ?? "unspecified")}; deploy --param accepts only string/boolean.`,
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
const violations = stringViolations(raw);
|
|
@@ -43,7 +43,7 @@ export function resolveInstallParameterExtraValues(input) {
|
|
|
43
43
|
return {
|
|
44
44
|
ok: false,
|
|
45
45
|
code: "DEPLOY_PARAM_TYPE_INVALID",
|
|
46
|
-
message: `--param ${name}=...
|
|
46
|
+
message: `--param ${name}=... does not meet the string Install Parameter rules (no blanks, control characters, \${ interpolation or more than 256 characters).`,
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
extraValues[name] = raw;
|
package/dist/deploy/plan.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { NativeArchFamily } from "../build-spk/dsm-arch.js";
|
|
|
3
3
|
import type { NasHost } from "../host/types.js";
|
|
4
4
|
export interface DeploymentPlan {
|
|
5
5
|
root: string;
|
|
6
|
-
kind: "native-go" | "native-rust" | "binary" | "native-layout" | "container";
|
|
6
|
+
kind: "native-go" | "native-python" | "native-node" | "native-rust" | "binary" | "native-layout" | "container";
|
|
7
7
|
packageId: string;
|
|
8
8
|
version: string;
|
|
9
9
|
arch?: NativeArchFamily;
|
|
@@ -32,11 +32,11 @@ export async function runPortPreflight(input) {
|
|
|
32
32
|
if (!conflict) {
|
|
33
33
|
return { ok: true };
|
|
34
34
|
}
|
|
35
|
-
const occupants = conflict.occupants.map(formatPortOccupant).join("
|
|
35
|
+
const occupants = conflict.occupants.map(formatPortOccupant).join(", ");
|
|
36
36
|
return {
|
|
37
37
|
ok: false,
|
|
38
38
|
code: PORT_ALREADY_IN_USE,
|
|
39
|
-
message: `Published port ${conflict.port}
|
|
40
|
-
"
|
|
39
|
+
message: `Published port ${conflict.port} is already taken by ${occupants} (Compose services: ${conflict.services.join(", ")}).` +
|
|
40
|
+
"Use a host port no other service uses; existing containers of the same Package Identity are allowed during upgrade/reinstall.",
|
|
41
41
|
};
|
|
42
42
|
}
|