@synopackageland/cli 0.3.9 → 0.4.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 +20 -9
- package/dist/bin.js +1 -2
- package/dist/build-spk/generator.d.ts +0 -3
- package/dist/build-spk/generator.js +135 -287
- package/dist/build-spk/native-lifecycle.js +0 -3
- package/dist/build-spk/native-payload.js +0 -1
- package/dist/cli.js +171 -151
- package/dist/compose/analyze.d.ts +1 -2
- package/dist/compose/analyze.js +5 -48
- package/dist/contract/unsupported-types.d.ts +10 -0
- package/dist/contract/unsupported-types.js +49 -0
- package/dist/contract/validate.js +25 -2
- package/dist/contract/web-launch.js +0 -2
- package/dist/deploy/deploy.d.ts +1 -4
- package/dist/deploy/deploy.js +57 -156
- package/dist/deploy/homes-values.js +1 -1
- package/dist/deploy/plan.d.ts +18 -0
- package/dist/deploy/plan.js +54 -0
- package/dist/deploy/port-preflight.d.ts +21 -0
- package/dist/deploy/port-preflight.js +42 -0
- package/dist/deploy/spk-version.d.ts +2 -14
- package/dist/deploy/spk-version.js +25 -111
- package/dist/dev.d.ts +1 -3
- package/dist/dev.js +14 -28
- package/dist/error-registry.js +810 -857
- package/dist/host/dsm-client.js +23 -263
- package/dist/host/dsm-port-profile.d.ts +47 -0
- package/dist/host/dsm-port-profile.js +206 -0
- package/dist/host/port-info-lookup.d.ts +9 -0
- package/dist/host/port-info-lookup.js +87 -0
- package/dist/host/port-info.d.ts +33 -0
- package/dist/host/port-info.js +66 -0
- package/dist/host/types.d.ts +6 -78
- package/dist/index.d.ts +0 -9
- package/dist/index.js +0 -6
- package/dist/info.d.ts +0 -1
- package/dist/info.js +3 -10
- package/dist/init.d.ts +11 -1
- package/dist/init.js +112 -4
- package/dist/log.js +2 -3
- package/dist/native/validate.js +0 -1
- package/dist/standalone/binary.d.ts +9 -0
- package/dist/standalone/binary.js +67 -0
- package/dist/standalone/build.d.ts +25 -0
- package/dist/standalone/build.js +108 -0
- package/dist/standalone/dsm-window.d.ts +26 -0
- package/dist/standalone/dsm-window.js +251 -0
- package/dist/standalone/helper.d.ts +31 -0
- package/dist/standalone/helper.js +104 -0
- package/dist/standalone/i18n.d.ts +28 -0
- package/dist/standalone/i18n.js +161 -0
- package/dist/standalone/inspect-spk.d.ts +21 -0
- package/dist/standalone/inspect-spk.js +160 -0
- package/dist/standalone/manifest.d.ts +71 -0
- package/dist/standalone/manifest.js +276 -0
- package/dist/standalone/root-messages.d.ts +7 -0
- package/dist/standalone/root-messages.js +250 -0
- package/dist/standalone/root-ops.d.ts +42 -0
- package/dist/standalone/root-ops.js +101 -0
- package/dist/standalone/root-window.d.ts +35 -0
- package/dist/standalone/root-window.js +290 -0
- package/dist/standalone/spk.d.ts +22 -0
- package/dist/standalone/spk.js +205 -0
- package/dist/templates/binary/synology-app.yaml +7 -0
- package/dist/templates/hello-files/compose.yaml +0 -1
- package/dist/templates/hello-web/compose.yaml +13 -10
- package/dist/templates/hello-web/html/app.js +1 -23
- package/dist/templates/hello-web/html/index.html +1 -3
- package/dist/templates/hello-web-page/compose.yaml +13 -10
- package/dist/templates/hello-web-page/html/app.js +1 -23
- package/dist/templates/hello-web-page/html/index.html +1 -3
- package/dist/templates/native-go/README.md +268 -0
- package/dist/templates/native-go/backend/cmd/app/main.go +49 -0
- package/dist/templates/native-go/backend/cmd/helper/main.go +38 -0
- package/dist/templates/native-go/backend/go.mod +3 -0
- package/dist/templates/native-go/backend/internal/api/errors.go +33 -0
- package/dist/templates/native-go/backend/internal/api/registry.go +94 -0
- package/dist/templates/native-go/backend/internal/api/registry_test.go +329 -0
- package/dist/templates/native-go/backend/internal/api/server.go +155 -0
- package/dist/templates/native-go/backend/internal/app/root_none.go +9 -0
- package/dist/templates/native-go/backend/internal/app/routes.go +127 -0
- package/dist/templates/native-go/backend/internal/app/routes_test.go +167 -0
- package/dist/templates/native-go/backend/internal/dsm/auth.go +118 -0
- package/dist/templates/native-go/backend/internal/dsm/auth_test.go +139 -0
- package/dist/templates/native-go/backend/internal/helper/control.go +219 -0
- package/dist/templates/native-go/backend/internal/helper/control_test.go +242 -0
- package/dist/templates/native-go/backend/internal/helper/peer_linux.go +44 -0
- package/dist/templates/native-go/backend/internal/helper/peer_other.go +7 -0
- package/dist/templates/native-go/backend/internal/helper/process_linux.go +167 -0
- package/dist/templates/native-go/backend/internal/helper/process_other.go +5 -0
- package/dist/templates/native-go/backend/internal/helper/server_linux.go +83 -0
- package/dist/templates/native-go/backend/internal/helper/status.go +271 -0
- package/dist/templates/native-go/backend/internal/helper/status_test.go +205 -0
- package/dist/templates/native-go/backend/internal/helper/supervision.go +84 -0
- package/dist/templates/native-go/backend/internal/helper/supervision_test.go +124 -0
- package/dist/templates/native-go/backend/internal/helper/supervisor_linux.go +192 -0
- package/dist/templates/native-go/backend/internal/helper/supervisor_linux_test.go +302 -0
- package/dist/templates/native-go/dsm-package.yaml +58 -0
- package/dist/templates/native-go/gitignore +1 -0
- package/dist/templates/native-go/i18n/glossary.json +662 -0
- package/dist/templates/native-go/i18n/messages/de.json +28 -0
- package/dist/templates/native-go/i18n/messages/en.json +28 -0
- package/dist/templates/native-go/i18n/messages/es.json +28 -0
- package/dist/templates/native-go/i18n/messages/fr.json +28 -0
- package/dist/templates/native-go/i18n/messages/it.json +28 -0
- package/dist/templates/native-go/i18n/messages/ja.json +28 -0
- package/dist/templates/native-go/i18n/messages/ko.json +28 -0
- package/dist/templates/native-go/i18n/messages/zh-CN.json +28 -0
- package/dist/templates/native-go/i18n/messages/zh-TW.json +28 -0
- package/dist/templates/native-go/ui/app.css +35 -0
- package/dist/templates/native-go/ui/app.js +94 -0
- package/dist/templates/native-go-better-auth/backend/internal/app/identity.go +208 -0
- package/dist/templates/native-go-better-auth/backend/internal/app/identity_test.go +113 -0
- package/dist/templates/native-go-better-auth/node/app.mjs +72 -0
- package/dist/templates/native-go-better-auth/node/build.mjs +2 -0
- package/dist/templates/native-go-better-auth/node/package-lock.json +862 -0
- package/dist/templates/native-go-better-auth/node/package.json +1 -0
- package/dist/templates/native-go-better-auth/node/test/auth.test.mjs +31 -0
- package/dist/templates/native-go-integration/backend/internal/app/identity_none.go +6 -0
- package/dist/templates/native-go-integration/backend/internal/app/personal.go +203 -0
- package/dist/templates/native-go-integration/backend/internal/app/personal_test.go +119 -0
- package/dist/templates/native-go-root/backend/internal/app/root.go +107 -0
- package/dist/templates/native-go-root/backend/internal/app/root_test.go +124 -0
- package/dist/templates/native-go-root/root/whoami.sh +6 -0
- package/dist/templates/native-go-vue/README.md +110 -0
- package/dist/templates/native-go-vue/frontend/DESIGN.md +78 -0
- package/dist/templates/native-go-vue/frontend/package-lock.json +2507 -0
- package/dist/templates/native-go-vue/frontend/package.json +6 -0
- package/dist/templates/native-go-vue/frontend/src/App.vue +132 -0
- package/dist/templates/native-go-vue/frontend/src/context.ts +17 -0
- package/dist/templates/native-go-vue/frontend/src/main.ts +26 -0
- package/dist/templates/native-go-vue/frontend/src/style.css +14 -0
- package/dist/templates/native-go-vue/frontend/test/apps.test.ts +148 -0
- package/dist/templates/native-go-vue/frontend/tsconfig.json +1 -0
- package/dist/templates/native-go-vue/frontend/vite.config.ts +8 -0
- package/dist/templates/native-go-vue/gitignore +2 -0
- package/dist/types.d.ts +1 -1
- package/dist/validate.js +11 -7
- package/package.json +3 -7
- package/schema/synology-app.schema.json +36 -37
- package/skills/SKILL.md +4 -48
- package/skills/reference/create.md +11 -83
- package/skills/reference/deploy.md +7 -78
- package/skills/reference/diagnose.md +4 -101
- package/skills/reference/files.md +5 -41
- package/skills/reference/identity.md +3 -29
- package/skills/reference/wrap.md +0 -2
- package/templates/binary/synology-app.yaml +7 -0
- package/templates/hello-files/compose.yaml +0 -1
- package/templates/hello-web/compose.yaml +13 -10
- package/templates/hello-web/html/app.js +1 -23
- package/templates/hello-web/html/index.html +1 -3
- package/templates/hello-web-page/compose.yaml +13 -10
- package/templates/hello-web-page/html/app.js +1 -23
- package/templates/hello-web-page/html/index.html +1 -3
- package/templates/native-go/README.md +268 -0
- package/templates/native-go/backend/cmd/app/main.go +49 -0
- package/templates/native-go/backend/cmd/helper/main.go +38 -0
- package/templates/native-go/backend/go.mod +3 -0
- package/templates/native-go/backend/internal/api/errors.go +33 -0
- package/templates/native-go/backend/internal/api/registry.go +94 -0
- package/templates/native-go/backend/internal/api/registry_test.go +329 -0
- package/templates/native-go/backend/internal/api/server.go +155 -0
- package/templates/native-go/backend/internal/app/root_none.go +9 -0
- package/templates/native-go/backend/internal/app/routes.go +127 -0
- package/templates/native-go/backend/internal/app/routes_test.go +167 -0
- package/templates/native-go/backend/internal/dsm/auth.go +118 -0
- package/templates/native-go/backend/internal/dsm/auth_test.go +139 -0
- package/templates/native-go/backend/internal/helper/control.go +219 -0
- package/templates/native-go/backend/internal/helper/control_test.go +242 -0
- package/templates/native-go/backend/internal/helper/peer_linux.go +44 -0
- package/templates/native-go/backend/internal/helper/peer_other.go +7 -0
- package/templates/native-go/backend/internal/helper/process_linux.go +167 -0
- package/templates/native-go/backend/internal/helper/process_other.go +5 -0
- package/templates/native-go/backend/internal/helper/server_linux.go +83 -0
- package/templates/native-go/backend/internal/helper/status.go +271 -0
- package/templates/native-go/backend/internal/helper/status_test.go +205 -0
- package/templates/native-go/backend/internal/helper/supervision.go +84 -0
- package/templates/native-go/backend/internal/helper/supervision_test.go +124 -0
- package/templates/native-go/backend/internal/helper/supervisor_linux.go +192 -0
- package/templates/native-go/backend/internal/helper/supervisor_linux_test.go +302 -0
- package/templates/native-go/dsm-package.yaml +58 -0
- package/templates/native-go/gitignore +1 -0
- package/templates/native-go/i18n/glossary.json +662 -0
- package/templates/native-go/i18n/messages/de.json +28 -0
- package/templates/native-go/i18n/messages/en.json +28 -0
- package/templates/native-go/i18n/messages/es.json +28 -0
- package/templates/native-go/i18n/messages/fr.json +28 -0
- package/templates/native-go/i18n/messages/it.json +28 -0
- package/templates/native-go/i18n/messages/ja.json +28 -0
- package/templates/native-go/i18n/messages/ko.json +28 -0
- package/templates/native-go/i18n/messages/zh-CN.json +28 -0
- package/templates/native-go/i18n/messages/zh-TW.json +28 -0
- package/templates/native-go/ui/app.css +35 -0
- package/templates/native-go/ui/app.js +94 -0
- package/templates/native-go-better-auth/backend/internal/app/identity.go +208 -0
- package/templates/native-go-better-auth/backend/internal/app/identity_test.go +113 -0
- package/templates/native-go-better-auth/node/app.mjs +72 -0
- package/templates/native-go-better-auth/node/build.mjs +2 -0
- package/templates/native-go-better-auth/node/package-lock.json +862 -0
- package/templates/native-go-better-auth/node/package.json +1 -0
- package/templates/native-go-better-auth/node/test/auth.test.mjs +31 -0
- package/templates/native-go-integration/backend/internal/app/identity_none.go +6 -0
- package/templates/native-go-integration/backend/internal/app/personal.go +203 -0
- package/templates/native-go-integration/backend/internal/app/personal_test.go +119 -0
- package/templates/native-go-root/backend/internal/app/root.go +107 -0
- package/templates/native-go-root/backend/internal/app/root_test.go +124 -0
- package/templates/native-go-root/root/whoami.sh +6 -0
- package/templates/native-go-vue/README.md +110 -0
- package/templates/native-go-vue/frontend/DESIGN.md +78 -0
- package/templates/native-go-vue/frontend/package-lock.json +2507 -0
- package/templates/native-go-vue/frontend/package.json +6 -0
- package/templates/native-go-vue/frontend/src/App.vue +132 -0
- package/templates/native-go-vue/frontend/src/context.ts +17 -0
- package/templates/native-go-vue/frontend/src/main.ts +26 -0
- package/templates/native-go-vue/frontend/src/style.css +14 -0
- package/templates/native-go-vue/frontend/test/apps.test.ts +148 -0
- package/templates/native-go-vue/frontend/tsconfig.json +1 -0
- package/templates/native-go-vue/frontend/vite.config.ts +8 -0
- package/templates/native-go-vue/gitignore +2 -0
- package/dist/attestation/app-package.d.ts +0 -86
- package/dist/attestation/app-package.js +0 -206
- package/dist/attestation/install-on-host.d.ts +0 -14
- package/dist/attestation/install-on-host.js +0 -52
- package/dist/attestation/keyring.d.ts +0 -21
- package/dist/attestation/keyring.js +0 -168
- package/dist/attestation/platform-service.d.ts +0 -70
- package/dist/attestation/platform-service.js +0 -250
- package/dist/build-spk/broker-glue.d.ts +0 -16
- package/dist/build-spk/broker-glue.js +0 -40
- package/dist/deploy/catalog-publish.d.ts +0 -3
- package/dist/deploy/catalog-publish.js +0 -7
- package/dist/doctor.d.ts +0 -22
- package/dist/doctor.js +0 -721
- package/dist/error-collector.d.ts +0 -2
- package/dist/error-collector.js +0 -46
- package/dist/host/broker-paths.d.ts +0 -1
- package/dist/host/broker-paths.js +0 -1
- package/dist/host/broker-upgrade-preflight.d.ts +0 -61
- package/dist/host/broker-upgrade-preflight.js +0 -156
- package/dist/host/digests.d.ts +0 -5
- package/dist/host/digests.js +0 -63
- package/dist/host/fake-nas.d.ts +0 -41
- package/dist/host/fake-nas.js +0 -174
- package/dist/mock-broker/bootstrap.d.ts +0 -2
- package/dist/mock-broker/bootstrap.js +0 -8
- package/dist/mock-broker/files.d.ts +0 -60
- package/dist/mock-broker/files.js +0 -408
- package/dist/mock-broker/oidc.d.ts +0 -31
- package/dist/mock-broker/oidc.js +0 -161
- package/dist/mock-broker/path.d.ts +0 -9
- package/dist/mock-broker/path.js +0 -50
- package/dist/mock-broker/server.d.ts +0 -19
- package/dist/mock-broker/server.js +0 -350
- package/dist/publish/build-ledger.d.ts +0 -22
- package/dist/publish/build-ledger.js +0 -26
- package/dist/publish/community-path.d.ts +0 -6
- package/dist/publish/community-path.js +0 -41
- package/dist/publish/curated-release.d.ts +0 -78
- package/dist/publish/curated-release.js +0 -883
- package/dist/publish/file-build-ledger.d.ts +0 -18
- package/dist/publish/file-build-ledger.js +0 -268
- package/dist/publish/github-pr.d.ts +0 -25
- package/dist/publish/github-pr.js +0 -85
- package/dist/publish/github-repo.d.ts +0 -8
- package/dist/publish/github-repo.js +0 -21
- package/dist/publish/platform-service-release.d.ts +0 -60
- package/dist/publish/platform-service-release.js +0 -333
- package/dist/publish/preflight.d.ts +0 -24
- package/dist/publish/preflight.js +0 -138
- package/dist/publish/publish.d.ts +0 -27
- package/dist/publish/publish.js +0 -136
- package/dist/publish/sync-definition.d.ts +0 -4
- package/dist/publish/sync-definition.js +0 -48
- package/dist/release-bin.d.ts +0 -2
- package/dist/release-bin.js +0 -23
- package/dist/templates/hello-web/html/sdk.js +0 -68
- package/dist/templates/hello-web-page/html/sdk.js +0 -68
- package/dist/templates/native/synology-app.yaml +0 -7
- package/dist/test-host.d.ts +0 -15
- package/dist/test-host.js +0 -37
- package/templates/hello-web/html/sdk.js +0 -68
- package/templates/hello-web-page/html/sdk.js +0 -68
- package/templates/native/synology-app.yaml +0 -7
- /package/dist/templates/{native → binary}/native.yaml +0 -0
- /package/templates/{native → binary}/native.yaml +0 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { collectInstallParameters } from "./load.js";
|
|
2
|
+
/**
|
|
3
|
+
* Install parameter types that the schema still accepts but that have no working
|
|
4
|
+
* install path on the live package-only route.
|
|
5
|
+
*
|
|
6
|
+
* Measured on vt-nas 2026-08-27: both end in DSM broken/150 with no Container
|
|
7
|
+
* Manager project, while a no-parameter control package installed fine on the
|
|
8
|
+
* same NAS. `secret` renders its password field and fails; `homes` renders no
|
|
9
|
+
* input at all. `validate` reports these as warnings so build-spk keeps working
|
|
10
|
+
* for fixtures that declare them incidentally -- the refusal belongs at the two
|
|
11
|
+
* gates that actually put an App somewhere: deploy and publish.
|
|
12
|
+
*/
|
|
13
|
+
const UNSUPPORTED_INSTALL_TYPES = new Map([
|
|
14
|
+
["secret", "CONTRACT_SECRET_UNSUPPORTED"],
|
|
15
|
+
["homes", "CONTRACT_HOMES_UNSUPPORTED"],
|
|
16
|
+
]);
|
|
17
|
+
/** Stable gate code for an install type that only the CLI can materialize. */
|
|
18
|
+
export const POSIX_ALL_SHARES_LISTING_UNSUPPORTED_CODE = "CONTRACT_POSIX_ALL_SHARES_LISTING_UNSUPPORTED";
|
|
19
|
+
/**
|
|
20
|
+
* Install parameter types that work through `synopkgland deploy` but cannot
|
|
21
|
+
* honor their declared Share scope through Package Center or a curated
|
|
22
|
+
* listing. Keep this separate from UNSUPPORTED_INSTALL_TYPES: deploy must
|
|
23
|
+
* continue to resolve these values from the NAS administrator session.
|
|
24
|
+
*/
|
|
25
|
+
const LISTING_UNSUPPORTED_INSTALL_TYPES = new Map([
|
|
26
|
+
["posix_all_shares", POSIX_ALL_SHARES_LISTING_UNSUPPORTED_CODE],
|
|
27
|
+
]);
|
|
28
|
+
export function findUnsupportedInstallTypes(contract) {
|
|
29
|
+
const found = [];
|
|
30
|
+
for (const { name, param } of collectInstallParameters(contract)) {
|
|
31
|
+
const type = typeof param.type === "string" ? param.type : "";
|
|
32
|
+
const code = UNSUPPORTED_INSTALL_TYPES.get(type);
|
|
33
|
+
if (code) {
|
|
34
|
+
found.push({ name, type, code });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return found;
|
|
38
|
+
}
|
|
39
|
+
export function findListingUnsupportedInstallTypes(contract) {
|
|
40
|
+
const found = [];
|
|
41
|
+
for (const { name, param } of collectInstallParameters(contract)) {
|
|
42
|
+
const type = typeof param.type === "string" ? param.type : "";
|
|
43
|
+
const code = LISTING_UNSUPPORTED_INSTALL_TYPES.get(type);
|
|
44
|
+
if (code) {
|
|
45
|
+
found.push({ name, type, code });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return found;
|
|
49
|
+
}
|
|
@@ -6,6 +6,7 @@ import { validateContractSchema } from "./schema-validate.js";
|
|
|
6
6
|
import { isWebLaunchMode, readWebIntegration } from "./web-launch.js";
|
|
7
7
|
import { loadCompose } from "../compose/analyze.js";
|
|
8
8
|
import { collectPosixAllSharesParameters, isPosixAllSharesType, validatePosixAllSharesSources, } from "./posix-all-shares.js";
|
|
9
|
+
import { getErrorCode } from "../error-registry.js";
|
|
9
10
|
import { FORMAL_PACKAGE_IDENTITY_MAX_LENGTH, PACKAGE_IDENTITY_MAX_LENGTH, } from "../deploy/identity-shared.js";
|
|
10
11
|
/** DSM 7.4 Package Center rejects a longer package identity outright. */
|
|
11
12
|
const DSM_PACKAGE_IDENTITY_MAX = 32;
|
|
@@ -113,6 +114,28 @@ function validateInstallParameters(parsed, contractFile) {
|
|
|
113
114
|
errors.push(makeError("INSTALL_STRING_INVALID", contractSpan(contractFile, pos.line, pos.column, `${basePath}.default`), `Install Parameter「${name}」的字串值不合法。`, "請移除控制字元與 ${ 插值語法。"));
|
|
114
115
|
}
|
|
115
116
|
}
|
|
117
|
+
// A45/A46: both types are in the schema enum but neither has a working
|
|
118
|
+
// install path on the live package-only route -- measured on vt-nas as DSM
|
|
119
|
+
// broken/150 with no Container Manager project, against a no-parameter
|
|
120
|
+
// control that installed fine. Refusing here is what makes the failure
|
|
121
|
+
// honest: `homes` used to validate, build a valid SPK, and only break at
|
|
122
|
+
// install time, telling the author nothing.
|
|
123
|
+
if (type === "secret" || type === "homes") {
|
|
124
|
+
const pos = getValuePosition(doc, ["install", name, "type"]) ??
|
|
125
|
+
getValuePosition(doc, ["install", name]) ??
|
|
126
|
+
{ line: 1, column: 1 };
|
|
127
|
+
const code = type === "secret" ? "CONTRACT_SECRET_UNSUPPORTED" : "CONTRACT_HOMES_UNSUPPORTED";
|
|
128
|
+
const entry = getErrorCode(code);
|
|
129
|
+
// A warning here, a hard refusal in deploy/publish. Making validate itself
|
|
130
|
+
// fail would also fail build-spk, and several existing fixtures declare
|
|
131
|
+
// these types only incidentally while testing wizard copy, permissions and
|
|
132
|
+
// script generation -- turning those red would cost real coverage for no
|
|
133
|
+
// extra safety. Nothing unshippable can reach a NAS or the community repo,
|
|
134
|
+
// because both gates refuse; the author still learns at validate time.
|
|
135
|
+
errors.push(makeError(code, contractSpan(contractFile, pos.line, pos.column, `${basePath}.type`), `Install Parameter「${name}」的 type: ${type} 目前沒有可用的安裝路徑,經 Package Center 安裝會以 DSM broken/150 結束;deploy 與 publish 會拒絕。`, entry?.suggestion ?? "請改用其他 install parameter type。", "warning"));
|
|
136
|
+
// Deliberately no `continue`: the type-specific checks below still apply.
|
|
137
|
+
// Skipping them once silently dropped CONTRACT_HOMES_PATH_FORBIDDEN.
|
|
138
|
+
}
|
|
116
139
|
if (type === "homes") {
|
|
117
140
|
const configuredPath = Object.entries(param).find(([key, value]) => key !== "type" &&
|
|
118
141
|
(key === "default" || (typeof value === "string" && isPhysicalHostPath(value))));
|
|
@@ -123,7 +146,7 @@ function validateInstallParameters(parsed, contractFile) {
|
|
|
123
146
|
const pos = getValuePosition(doc, ["install", name, field]) ??
|
|
124
147
|
getValuePosition(doc, ["install", name]) ??
|
|
125
148
|
{ line: 1, column: 1 };
|
|
126
|
-
errors.push(makeError("CONTRACT_HOMES_PATH_FORBIDDEN", contractSpan(contractFile, pos.line, pos.column, `${basePath}.${field}`), `homes Install Parameter「${name}」不得在契約欄位「${field}」指定 NAS 實體路徑或預設值。`, `請保留 install.${name}.type: homes,Compose 只引用 \${${name}}
|
|
149
|
+
errors.push(makeError("CONTRACT_HOMES_PATH_FORBIDDEN", contractSpan(contractFile, pos.line, pos.column, `${basePath}.${field}`), `homes Install Parameter「${name}」不得在契約欄位「${field}」指定 NAS 實體路徑或預設值。`, `請保留 install.${name}.type: homes,Compose 只引用 \${${name}};由套件 在安裝時依這台 NAS 的 User Home 設定解析路徑。`));
|
|
127
150
|
}
|
|
128
151
|
if (isPosixAllSharesType(type)) {
|
|
129
152
|
const configuredValue = Object.entries(param).find(([key, value]) => key !== "type" && (key === "default" || value !== undefined));
|
|
@@ -132,7 +155,7 @@ function validateInstallParameters(parsed, contractFile) {
|
|
|
132
155
|
const pos = getValuePosition(doc, ["install", name, field]) ??
|
|
133
156
|
getValuePosition(doc, ["install", name]) ??
|
|
134
157
|
{ line: 1, column: 1 };
|
|
135
|
-
errors.push(makeError("CONTRACT_POSIX_ALL_SHARES_VALUE_FORBIDDEN", contractSpan(contractFile, pos.line, pos.column, `${basePath}.${field}`), `POSIX all-shares Install Parameter「${name}」不得指定 default 或 NAS 實體路徑;Share 清單只在安裝時由
|
|
158
|
+
errors.push(makeError("CONTRACT_POSIX_ALL_SHARES_VALUE_FORBIDDEN", contractSpan(contractFile, pos.line, pos.column, `${basePath}.${field}`), `POSIX all-shares Install Parameter「${name}」不得指定 default 或 NAS 實體路徑;Share 清單只在安裝時由 CLI 透過 DSM API 取得快照。`, `請只保留 install.${name}.type: posix_all_shares,Compose 使用 \${${name}} 作為完整 volume source。`));
|
|
136
159
|
}
|
|
137
160
|
}
|
|
138
161
|
}
|
|
@@ -81,8 +81,6 @@ export function renderWebStationResource(contract) {
|
|
|
81
81
|
const alias = webStationAlias(String(contract.package));
|
|
82
82
|
const launch = resolvedWebLaunch(contract);
|
|
83
83
|
// DSM synowebapi crashes on type=reverse_proxy. Package acquire of reverse_proxy
|
|
84
|
-
// also creates a preserve portal that Broker cannot rebind (error 1300).
|
|
85
|
-
// Static + alias matches Broker; cookie strip / proxy_pass live in nginx user.conf.
|
|
86
84
|
const service = {
|
|
87
85
|
service: webStationServiceName(alias),
|
|
88
86
|
display_name: String(contract.displayname ?? contract.package),
|
package/dist/deploy/deploy.d.ts
CHANGED
|
@@ -2,16 +2,13 @@ import type { NativeArchFamily } from "../build-spk/dsm-arch.js";
|
|
|
2
2
|
import type { ProjectRoot } from "../discovery.js";
|
|
3
3
|
import type { NasHost } from "../host/types.js";
|
|
4
4
|
import { resolveDeploySpkVersion } from "./spk-version.js";
|
|
5
|
-
import type { PublishSpkResult } from "@synopackageland/package-source/publish-spk";
|
|
6
5
|
export interface DeployProjectOptions {
|
|
7
6
|
project: ProjectRoot;
|
|
8
7
|
/** native App only: which target to build. Omitted means "ask the NAS". */
|
|
9
8
|
archFamily?: NativeArchFamily;
|
|
10
9
|
host: NasHost;
|
|
11
10
|
dev?: boolean;
|
|
12
|
-
replaceCurated?: boolean;
|
|
13
11
|
spkVersionResolver?: typeof resolveDeploySpkVersion;
|
|
14
|
-
catalogPublish?: (spkPath: string) => Promise<PublishSpkResult>;
|
|
15
12
|
shareValues?: Record<string, string>;
|
|
16
13
|
runAsValues?: Record<string, string>;
|
|
17
14
|
paramValues?: Record<string, string>;
|
|
@@ -23,7 +20,7 @@ export interface DeployProjectResult {
|
|
|
23
20
|
packageId?: string;
|
|
24
21
|
spkPath?: string;
|
|
25
22
|
openUrl?: string;
|
|
26
|
-
|
|
23
|
+
version?: string;
|
|
27
24
|
/** Non-blocking pre-flight remarks, e.g. a read-only Shared Folder grant. */
|
|
28
25
|
notes?: string[];
|
|
29
26
|
}
|
package/dist/deploy/deploy.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { buildSpk } from "../build-spk/generator.js";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { loadContract } from "../contract/load.js";
|
|
4
|
-
import {
|
|
4
|
+
import { findUnsupportedInstallTypes } from "../contract/unsupported-types.js";
|
|
5
5
|
import { appOriginFromContract } from "../host/open-url.js";
|
|
6
6
|
import { validateProject } from "../validate.js";
|
|
7
|
-
import { publishBuiltSpk, packageSourceOrigin } from "./catalog-publish.js";
|
|
8
7
|
import { resolveDeployIdentity, PACKAGE_IDENTITY_MAX_LENGTH } from "./identity.js";
|
|
9
8
|
import { resolveDeploySpkVersion } from "./spk-version.js";
|
|
10
9
|
import { resolveSharedFolderExtraValues, sharedFolderParameterNames } from "./share-values.js";
|
|
@@ -12,43 +11,26 @@ import { resolveDsmUserExtraValues, dsmUserParameterNames } from "./run-as-value
|
|
|
12
11
|
import { runSharePermissionPreflight } from "./share-permission-preflight.js";
|
|
13
12
|
import { homesParameterNames, resolveHomesExtraValues } from "./homes-values.js";
|
|
14
13
|
import { resolveInstallParameterExtraValues } from "./param-values.js";
|
|
15
|
-
import {
|
|
16
|
-
|
|
14
|
+
import { loadCompose } from "../compose/analyze.js";
|
|
15
|
+
import { runPortPreflight } from "./port-preflight.js";
|
|
17
16
|
const POSIX_ALL_SHARES_LOOKUP_FAILED = "DEPLOY_POSIX_ALL_SHARES_LOOKUP_FAILED";
|
|
18
|
-
function brokerCapabilitiesFromContract(project) {
|
|
19
|
-
const contract = loadContract(project.contractPath).contract;
|
|
20
|
-
const capabilities = ["identity"];
|
|
21
|
-
const integration = contract.integration;
|
|
22
|
-
if (integration?.files) {
|
|
23
|
-
capabilities.push("files");
|
|
24
|
-
}
|
|
25
|
-
return capabilities;
|
|
26
|
-
}
|
|
27
|
-
function versionBuildIsCurated(version) {
|
|
28
|
-
if (!version) {
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
const match = /-(\d+)$/.exec(version);
|
|
32
|
-
if (!match) {
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
const build = Number(match[1]);
|
|
36
|
-
return Number.isSafeInteger(build) && build >= CURATED_BUILD_OFFSET;
|
|
37
|
-
}
|
|
38
|
-
async function detectCuratedInstall(host, packageId) {
|
|
39
|
-
let brokerRecord = false;
|
|
40
|
-
try {
|
|
41
|
-
brokerRecord = (await host.getAttestationInfo(packageId)).attestationKind === "app-package";
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
// requestAttestation below remains the authoritative error path when the Broker info probe is unavailable.
|
|
45
|
-
}
|
|
46
|
-
return {
|
|
47
|
-
brokerRecord,
|
|
48
|
-
installedVersion: await host.getInstalledPackageVersion?.(packageId),
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
17
|
export async function deployProject(options) {
|
|
18
|
+
const contract = loadContract(options.project.contractPath).contract;
|
|
19
|
+
// A45/A46: refuse before anything is built or uploaded. validate only warns,
|
|
20
|
+
// so this is the gate that keeps an uninstallable App off a NAS.
|
|
21
|
+
const unsupported = findUnsupportedInstallTypes(contract);
|
|
22
|
+
if (unsupported.length > 0) {
|
|
23
|
+
const first = unsupported[0];
|
|
24
|
+
return {
|
|
25
|
+
ok: false,
|
|
26
|
+
code: first.code,
|
|
27
|
+
message: `Install Parameter「${first.name}」的 type: ${first.type} 目前沒有可用的安裝路徑,` +
|
|
28
|
+
"Package Center 安裝會以 DSM broken/150 結束,因此 deploy 拒絕部署。" +
|
|
29
|
+
(first.type === "homes"
|
|
30
|
+
? "若需要管理員選定的資料夾,請改用 type: shared_folder。"
|
|
31
|
+
: "請改用 type: string,或讓 App 自行從 ${SYNO_APP_SECRETS_DIR} 讀檔。"),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
52
34
|
const validation = validateProject(options.project);
|
|
53
35
|
if (!validation.ok) {
|
|
54
36
|
return { ok: false, code: "VALIDATION_FAILED", message: validation.errors.map((e) => e.code).join(", ") };
|
|
@@ -61,14 +43,8 @@ export async function deployProject(options) {
|
|
|
61
43
|
message: `最終 Package Identity「${identity.packageId}」長度 ${identity.packageId.length} 超過 DSM 上限 ${PACKAGE_IDENTITY_MAX_LENGTH} 字元。`,
|
|
62
44
|
};
|
|
63
45
|
}
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
: await detectCuratedInstall(options.host, identity.packageId);
|
|
67
|
-
const hasCuratedInstall = curatedInstall?.brokerRecord === true || versionBuildIsCurated(curatedInstall?.installedVersion);
|
|
68
|
-
if (hasCuratedInstall && !options.replaceCurated) {
|
|
69
|
-
return { ok: false, code: "CURATED_DEPLOY_REFUSED", message: CURATED_DEPLOY_REFUSED_MESSAGE };
|
|
70
|
-
}
|
|
71
|
-
const contract = loadContract(options.project.contractPath).contract;
|
|
46
|
+
const packageInstalled = await options.host.isPackageInstalled(identity.packageId);
|
|
47
|
+
const explicitParamNames = new Set(Object.keys(options.paramValues ?? {}));
|
|
72
48
|
const paramValues = { ...(options.paramValues ?? {}) };
|
|
73
49
|
const posixAllSharesNames = Object.entries(contract.install ?? {})
|
|
74
50
|
.filter(([, parameter]) => parameter?.type === "posix_all_shares")
|
|
@@ -109,18 +85,16 @@ export async function deployProject(options) {
|
|
|
109
85
|
return { ok: false, code: homesResolved.code, message: homesResolved.message };
|
|
110
86
|
}
|
|
111
87
|
const shareResolved = await resolveSharedFolderExtraValues({
|
|
112
|
-
requiredKeys: sharedFolderParameterNames(contract),
|
|
88
|
+
requiredKeys: sharedFolderParameterNames(contract).filter((key) => !packageInstalled || options.shareValues?.[key] !== undefined),
|
|
113
89
|
provided: options.shareValues ?? {},
|
|
114
|
-
existingEnv: (await options.host.getInstalledProjectEnv?.(identity.packageId)) ?? {},
|
|
115
90
|
lookup: async (logicalName) => options.host.resolveSharePath?.(logicalName),
|
|
116
91
|
});
|
|
117
92
|
if (!shareResolved.ok) {
|
|
118
93
|
return { ok: false, code: shareResolved.code, message: shareResolved.message };
|
|
119
94
|
}
|
|
120
95
|
const runAsResolved = resolveDsmUserExtraValues({
|
|
121
|
-
requiredKeys: dsmUserParameterNames(contract),
|
|
96
|
+
requiredKeys: dsmUserParameterNames(contract).filter((key) => !packageInstalled || options.runAsValues?.[key] !== undefined),
|
|
122
97
|
provided: options.runAsValues ?? {},
|
|
123
|
-
existingEnv: (await options.host.getInstalledProjectEnv?.(identity.packageId)) ?? {},
|
|
124
98
|
});
|
|
125
99
|
if (!runAsResolved.ok) {
|
|
126
100
|
return { ok: false, code: runAsResolved.code, message: runAsResolved.message };
|
|
@@ -143,76 +117,45 @@ export async function deployProject(options) {
|
|
|
143
117
|
if (!sharePermission.ok) {
|
|
144
118
|
return { ok: false, code: sharePermission.code, message: sharePermission.message };
|
|
145
119
|
}
|
|
120
|
+
const portPreflight = await runPortPreflight({
|
|
121
|
+
compose: loadCompose(options.project.composePath)?.compose,
|
|
122
|
+
packageIdentity: identity.packageId,
|
|
123
|
+
listPortInfo: options.host.listPortInfo
|
|
124
|
+
? () => options.host.listPortInfo()
|
|
125
|
+
: undefined,
|
|
126
|
+
});
|
|
127
|
+
if (!portPreflight.ok) {
|
|
128
|
+
return { ok: false, code: portPreflight.code, message: portPreflight.message };
|
|
129
|
+
}
|
|
146
130
|
const extraValues = {
|
|
147
131
|
...paramResolved.extraValues,
|
|
148
132
|
...homesResolved.extraValues,
|
|
149
133
|
...shareResolved.extraValues,
|
|
150
134
|
...runAsResolved.extraValues,
|
|
151
135
|
};
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
return {
|
|
160
|
-
ok: false,
|
|
161
|
-
code: "CATALOG_INSTALL_FAILED",
|
|
162
|
-
message: "此 NAS Host 不支援保留 App Data 的 Curated replacement uninstall。",
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
try {
|
|
166
|
-
await options.host.uninstallPackage(identity.packageId);
|
|
136
|
+
// The upgrade wizard deliberately ignores auto-filled Share controls unless
|
|
137
|
+
// the administrator opted into a change. Keep the CLI's explicit upgrade
|
|
138
|
+
// flags equivalent to checking those controls.
|
|
139
|
+
if (packageInstalled) {
|
|
140
|
+
for (const name of sharedFolderParameterNames(contract)) {
|
|
141
|
+
if ((options.shareValues?.[name] ?? "").trim() !== "") {
|
|
142
|
+
extraValues[`CHANGE_${name}`] = "true";
|
|
167
143
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
message: error instanceof Error ? error.message : String(error),
|
|
173
|
-
};
|
|
144
|
+
}
|
|
145
|
+
for (const name of posixAllSharesNames) {
|
|
146
|
+
if (explicitParamNames.has(name) && (paramValues[name] ?? "").trim() !== "") {
|
|
147
|
+
extraValues[`CHANGE_${name}`] = "true";
|
|
174
148
|
}
|
|
175
149
|
}
|
|
176
150
|
}
|
|
177
|
-
let attestationResult;
|
|
178
|
-
try {
|
|
179
|
-
attestationResult = await options.host.requestAttestation({
|
|
180
|
-
packageIdentity: identity.packageId,
|
|
181
|
-
contractDigest,
|
|
182
|
-
bundleDigest,
|
|
183
|
-
capabilities: brokerCapabilitiesFromContract(options.project),
|
|
184
|
-
allowCuratedDowngrade: options.replaceCurated,
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
catch (error) {
|
|
188
|
-
const code = error.code ?? "ATTESTATION_FAILED";
|
|
189
|
-
return {
|
|
190
|
-
ok: false,
|
|
191
|
-
code,
|
|
192
|
-
message: code === "CURATED_DEPLOY_REFUSED"
|
|
193
|
-
? CURATED_DEPLOY_REFUSED_MESSAGE
|
|
194
|
-
: error instanceof Error
|
|
195
|
-
? error.message
|
|
196
|
-
: String(error),
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
151
|
const resolveSpkVersion = options.spkVersionResolver ?? resolveDeploySpkVersion;
|
|
200
152
|
let spkVersion;
|
|
201
153
|
try {
|
|
202
154
|
spkVersion = await resolveSpkVersion(options.project, options.host, identity.packageId);
|
|
203
155
|
}
|
|
204
156
|
catch (error) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
code === "PERSONAL_BUILD_EXHAUSTED" ||
|
|
208
|
-
code === "DEPLOY_VERSION_NOT_ASCENDING") {
|
|
209
|
-
return {
|
|
210
|
-
ok: false,
|
|
211
|
-
code: code === "DEPLOY_VERSION_NOT_ASCENDING" ? code : "DEPLOY_PERSONAL_BUILD_EXHAUSTED",
|
|
212
|
-
message: error instanceof Error ? error.message : String(error),
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
throw error;
|
|
157
|
+
return { ok: false, code: "DEPLOY_VERSION_INVALID",
|
|
158
|
+
message: error instanceof Error ? error.message : String(error) };
|
|
216
159
|
}
|
|
217
160
|
let archFamily = options.archFamily;
|
|
218
161
|
if (archFamily === undefined && options.project.nativePath && existsSync(options.project.nativePath)) {
|
|
@@ -231,67 +174,25 @@ export async function deployProject(options) {
|
|
|
231
174
|
displayname: identity.displayname,
|
|
232
175
|
spkVersion,
|
|
233
176
|
archFamily,
|
|
234
|
-
embeddedAttestationToken: attestationResult.token,
|
|
235
177
|
});
|
|
236
178
|
if (!build.ok || !build.outputPath) {
|
|
237
179
|
return { ok: false, code: "BUILD_SPK_FAILED", message: build.errors.join(", ") };
|
|
238
180
|
}
|
|
239
|
-
let catalogVersion = spkVersion;
|
|
240
|
-
const publishToCatalog = options.catalogPublish ?? publishBuiltSpk;
|
|
241
|
-
if (options.host.installationId === "live-nas" && !identity.developmentInstance) {
|
|
242
|
-
try {
|
|
243
|
-
const published = await publishToCatalog(build.outputPath);
|
|
244
|
-
catalogVersion = published.version;
|
|
245
|
-
}
|
|
246
|
-
catch (error) {
|
|
247
|
-
return {
|
|
248
|
-
ok: false,
|
|
249
|
-
code: "CATALOG_PUBLISH_FAILED",
|
|
250
|
-
message: error instanceof Error ? error.message : String(error),
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
181
|
try {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
await options.host.installFromLocalSpk({
|
|
264
|
-
packageId: identity.packageId,
|
|
265
|
-
version: catalogVersion,
|
|
266
|
-
spkPath: build.outputPath,
|
|
267
|
-
extraValues,
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
else {
|
|
271
|
-
if (options.host.ensurePackageSource) {
|
|
272
|
-
await options.host.ensurePackageSource(packageSourceOrigin());
|
|
273
|
-
}
|
|
274
|
-
await options.host.installFromCatalog({
|
|
275
|
-
packageId: identity.packageId,
|
|
276
|
-
version: catalogVersion,
|
|
277
|
-
extraValues,
|
|
278
|
-
});
|
|
279
|
-
}
|
|
182
|
+
await options.host.installFromLocalSpk({
|
|
183
|
+
packageId: identity.packageId,
|
|
184
|
+
version: spkVersion,
|
|
185
|
+
spkPath: build.outputPath,
|
|
186
|
+
extraValues,
|
|
187
|
+
});
|
|
280
188
|
}
|
|
281
189
|
catch (error) {
|
|
282
|
-
return {
|
|
283
|
-
|
|
284
|
-
code: "CATALOG_INSTALL_FAILED",
|
|
285
|
-
message: error instanceof Error ? error.message : String(error),
|
|
286
|
-
};
|
|
190
|
+
return { ok: false, code: "LOCAL_SPK_INSTALL_FAILED",
|
|
191
|
+
message: error instanceof Error ? error.message : String(error) };
|
|
287
192
|
}
|
|
288
|
-
const info = await options.host.getAttestationInfo(identity.packageId);
|
|
289
193
|
return {
|
|
290
|
-
ok: true,
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
openUrl: info.openUrl ?? appOriginFromContract(options.host.dsmOrigin, contract),
|
|
294
|
-
catalogVersion,
|
|
295
|
-
notes: sharePermission.notes.length > 0 ? sharePermission.notes : undefined,
|
|
194
|
+
ok: true, packageId: identity.packageId, spkPath: build.outputPath,
|
|
195
|
+
openUrl: appOriginFromContract(options.host.dsmOrigin, contract),
|
|
196
|
+
version: spkVersion, notes: sharePermission.notes,
|
|
296
197
|
};
|
|
297
198
|
}
|
|
@@ -19,7 +19,7 @@ export async function resolveHomesExtraValues(input) {
|
|
|
19
19
|
return {
|
|
20
20
|
ok: false,
|
|
21
21
|
code: "HOMES_NOT_ENABLED",
|
|
22
|
-
message: "這台 NAS 未啟用 DSM User Home,或
|
|
22
|
+
message: "這台 NAS 未啟用 DSM User Home,或 DSM API 無法解析 user home 路徑;無法安裝需要 homes 的 App。請在 DSM 啟用 User Home 後重試。",
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
25
|
return {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { NativeArchFamily } from "../build-spk/dsm-arch.js";
|
|
2
|
+
import type { NasHost } from "../host/types.js";
|
|
3
|
+
export interface DeploymentPlan {
|
|
4
|
+
root: string;
|
|
5
|
+
kind: "native-go" | "binary" | "container";
|
|
6
|
+
packageId: string;
|
|
7
|
+
version: string;
|
|
8
|
+
arch?: NativeArchFamily;
|
|
9
|
+
privilege: "none" | "root";
|
|
10
|
+
transport: "dsm-local-spk";
|
|
11
|
+
notes: string[];
|
|
12
|
+
}
|
|
13
|
+
/** Purely local planning: no credentials, network, build or installation. */
|
|
14
|
+
export declare function planDeployment(root: string, options?: {
|
|
15
|
+
arch?: NativeArchFamily;
|
|
16
|
+
dev?: boolean;
|
|
17
|
+
}): DeploymentPlan;
|
|
18
|
+
export declare function deployStandalone(plan: DeploymentPlan, host: NasHost): Promise<string>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { findProjectRoot } from "../discovery.js";
|
|
4
|
+
import { loadContract } from "../contract/load.js";
|
|
5
|
+
import { validateProject } from "../validate.js";
|
|
6
|
+
import { findStandaloneRoot, loadStandaloneManifest } from "../standalone/manifest.js";
|
|
7
|
+
import { buildStandalone } from "../standalone/build.js";
|
|
8
|
+
import { resolveDeployIdentity } from "./identity.js";
|
|
9
|
+
import { nextDeployVersion } from "./spk-version.js";
|
|
10
|
+
/** Purely local planning: no credentials, network, build or installation. */
|
|
11
|
+
export function planDeployment(root, options = {}) {
|
|
12
|
+
const standalone = findStandaloneRoot(root);
|
|
13
|
+
if (standalone) {
|
|
14
|
+
const loaded = loadStandaloneManifest(standalone);
|
|
15
|
+
if (!loaded.ok)
|
|
16
|
+
throw new Error(loaded.errors.map(e => `${e.code}: ${e.message}`).join("\n"));
|
|
17
|
+
const m = loaded.manifest;
|
|
18
|
+
if (options.dev)
|
|
19
|
+
throw new Error('--dev is not supported for native-go; choose a distinct package name at init');
|
|
20
|
+
if (options.arch && !m.arches.includes(options.arch))
|
|
21
|
+
throw new Error(`Architecture not declared: ${options.arch}`);
|
|
22
|
+
return { root: standalone, kind: "native-go", packageId: m.package, version: m.spkVersion,
|
|
23
|
+
arch: options.arch, privilege: m.privilege, transport: "dsm-local-spk",
|
|
24
|
+
notes: m.privilege === "root" ? ['Approve root tasks in DSM after installation; changed task text requires fresh approval. Remove tasks before uninstall.'] : [] };
|
|
25
|
+
}
|
|
26
|
+
const project = findProjectRoot(root);
|
|
27
|
+
if (!project)
|
|
28
|
+
throw new Error('No dsm-package.yaml or synology-app.yaml found');
|
|
29
|
+
const validation = validateProject(project);
|
|
30
|
+
if (!validation.ok)
|
|
31
|
+
throw new Error(validation.errors.map(e => `${e.code}: ${e.message}`).join("\n"));
|
|
32
|
+
const contract = loadContract(project.contractPath).contract;
|
|
33
|
+
const identity = resolveDeployIdentity(project, { dev: options.dev });
|
|
34
|
+
return { root: project.root, kind: existsSync(join(project.root, 'native.yaml')) ? 'binary' : 'container',
|
|
35
|
+
packageId: identity.packageId, version: nextDeployVersion(String(contract.version)),
|
|
36
|
+
arch: options.arch, privilege: 'none', transport: 'dsm-local-spk',
|
|
37
|
+
notes: ['DSM resolves target architecture and installed version during deployment. Container bind access uses the configured process user.'] };
|
|
38
|
+
}
|
|
39
|
+
export async function deployStandalone(plan, host) {
|
|
40
|
+
if (plan.kind !== 'native-go')
|
|
41
|
+
throw new Error('Expected native-go deployment plan');
|
|
42
|
+
const arch = plan.arch ?? await host.getArchFamily?.();
|
|
43
|
+
if (!arch)
|
|
44
|
+
throw new Error('Cannot determine NAS architecture; specify --arch');
|
|
45
|
+
const installed = await host.getInstalledPackageVersion?.(plan.packageId);
|
|
46
|
+
if (installed && nextDeployVersion(plan.version, installed) !== plan.version) {
|
|
47
|
+
throw new Error(`Increase dsm-package.yaml version/build above installed ${installed}`);
|
|
48
|
+
}
|
|
49
|
+
const { outputs } = await buildStandalone({ root: plan.root, arches: [arch] });
|
|
50
|
+
if (outputs.length !== 1)
|
|
51
|
+
throw new Error('Expected one SPK');
|
|
52
|
+
await host.installFromLocalSpk({ packageId: plan.packageId, version: plan.version, spkPath: outputs[0] });
|
|
53
|
+
return outputs[0];
|
|
54
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ComposeDocument } from "../types.js";
|
|
2
|
+
import { type PortInfoEntry } from "../host/port-info.js";
|
|
3
|
+
export declare const PORT_ALREADY_IN_USE = "PORT_ALREADY_IN_USE";
|
|
4
|
+
export interface PortPreflightInput {
|
|
5
|
+
compose?: ComposeDocument;
|
|
6
|
+
packageIdentity: string;
|
|
7
|
+
listPortInfo?: () => Promise<PortInfoEntry[] | undefined>;
|
|
8
|
+
}
|
|
9
|
+
export type PortPreflightResult = {
|
|
10
|
+
ok: true;
|
|
11
|
+
} | {
|
|
12
|
+
ok: false;
|
|
13
|
+
code: typeof PORT_ALREADY_IN_USE;
|
|
14
|
+
message: string;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Checks Compose's published TCP host ports against DSM's administrator-only
|
|
18
|
+
* PortInfo listing. Missing capability, transport failure, malformed data,
|
|
19
|
+
* and an empty listing all mean "cannot tell" and therefore pass through.
|
|
20
|
+
*/
|
|
21
|
+
export declare function runPortPreflight(input: PortPreflightInput): Promise<PortPreflightResult>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { collectPublishedTcpPorts } from "../compose/analyze.js";
|
|
2
|
+
import { findPortConflicts, formatPortOccupant, } from "../host/port-info.js";
|
|
3
|
+
export const PORT_ALREADY_IN_USE = "PORT_ALREADY_IN_USE";
|
|
4
|
+
/**
|
|
5
|
+
* Checks Compose's published TCP host ports against DSM's administrator-only
|
|
6
|
+
* PortInfo listing. Missing capability, transport failure, malformed data,
|
|
7
|
+
* and an empty listing all mean "cannot tell" and therefore pass through.
|
|
8
|
+
*/
|
|
9
|
+
export async function runPortPreflight(input) {
|
|
10
|
+
const publishedPorts = input.compose
|
|
11
|
+
? collectPublishedTcpPorts(input.compose)
|
|
12
|
+
: new Map();
|
|
13
|
+
if (publishedPorts.size === 0 || !input.listPortInfo) {
|
|
14
|
+
return { ok: true };
|
|
15
|
+
}
|
|
16
|
+
let portInfo;
|
|
17
|
+
try {
|
|
18
|
+
portInfo = await input.listPortInfo();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
portInfo = undefined;
|
|
22
|
+
}
|
|
23
|
+
if (!portInfo || portInfo.length === 0) {
|
|
24
|
+
return { ok: true };
|
|
25
|
+
}
|
|
26
|
+
const conflicts = findPortConflicts({
|
|
27
|
+
publishedPorts,
|
|
28
|
+
packageIdentity: input.packageIdentity,
|
|
29
|
+
portInfo,
|
|
30
|
+
});
|
|
31
|
+
const conflict = conflicts[0];
|
|
32
|
+
if (!conflict) {
|
|
33
|
+
return { ok: true };
|
|
34
|
+
}
|
|
35
|
+
const occupants = conflict.occupants.map(formatPortOccupant).join("、");
|
|
36
|
+
return {
|
|
37
|
+
ok: false,
|
|
38
|
+
code: PORT_ALREADY_IN_USE,
|
|
39
|
+
message: `Published port ${conflict.port} 已被 ${occupants} 佔用(Compose service:${conflict.services.join("、")})。` +
|
|
40
|
+
"請改用未被其他服務使用的 host port;同一 Package Identity 的既有容器會在升級/重裝時放行。",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -1,17 +1,5 @@
|
|
|
1
1
|
import type { ProjectRoot } from "../discovery.js";
|
|
2
2
|
import type { NasHost } from "../host/types.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
constructor();
|
|
6
|
-
}
|
|
7
|
-
export declare class DeployVersionNotAscendingError extends Error {
|
|
8
|
-
readonly code = "DEPLOY_VERSION_NOT_ASCENDING";
|
|
9
|
-
constructor(contractVersion: string, proposedVersion: string, existingVersion: string);
|
|
10
|
-
}
|
|
11
|
-
export interface DeployVersionCandidates {
|
|
12
|
-
installed?: string;
|
|
13
|
-
feed?: string;
|
|
14
|
-
}
|
|
15
|
-
export declare function resolvePersonalDeploySpkVersion(contractVersion: string, candidates?: DeployVersionCandidates): string;
|
|
3
|
+
/** Increment the installed build only for the same application version. */
|
|
4
|
+
export declare function nextDeployVersion(version: string, installed?: string): string;
|
|
16
5
|
export declare function resolveDeploySpkVersion(project: ProjectRoot, host: NasHost, packageId: string): Promise<string>;
|
|
17
|
-
export declare function publishDeploySpk(spkPath: string): Promise<string>;
|