@synopackageland/cli 0.3.4 → 0.3.5
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/dist/build-spk/generator.js +28 -8
- package/dist/build-spk/resource-worker.d.ts +2 -1
- package/dist/build-spk/resource-worker.js +61 -1
- package/dist/compose/analyze.d.ts +2 -1
- package/dist/compose/analyze.js +31 -8
- package/dist/doctor.js +30 -8
- package/dist/error-registry.js +5 -0
- package/dist/host/fake-nas.d.ts +2 -0
- package/dist/host/types.d.ts +2 -0
- package/package.json +1 -1
|
@@ -17,7 +17,7 @@ import { BROKER_INSTALLER_VARS, BROKER_SOCKET_REQUIRED_SCRIPT, renderBrokerStatu
|
|
|
17
17
|
import { loadNative } from "../native/load.js";
|
|
18
18
|
import { materializeNativePayload } from "./native-payload.js";
|
|
19
19
|
import { renderNativePrivilege, renderNativeStartStopStatus, renderNativePostuninst, } from "./native-lifecycle.js";
|
|
20
|
-
import { renderResourceConfig, renderResourceWorkerStartStopStatus } from "./resource-worker.js";
|
|
20
|
+
import { renderHeadlessResourceMarker, renderResourceConfig, renderResourceWorkerStartStopStatus, } from "./resource-worker.js";
|
|
21
21
|
const FILE_MODE = 0o644;
|
|
22
22
|
const SCRIPT_MODE = 0o755;
|
|
23
23
|
const INSTALL_UIFILE = "install_uifile";
|
|
@@ -161,7 +161,7 @@ export async function buildSpk(options) {
|
|
|
161
161
|
mkdirSync(join(staging, "scripts"), { recursive: true });
|
|
162
162
|
for (const name of SCRIPT_NAMES) {
|
|
163
163
|
const scriptPath = join(staging, "scripts", name);
|
|
164
|
-
writeFileSync(scriptPath, renderManagedScript(name, contract, undefined, isResourceWorkerRoute, rwEligibility?.adminport, isDynamicRoute), "utf8");
|
|
164
|
+
writeFileSync(scriptPath, renderManagedScript(name, contract, undefined, isResourceWorkerRoute, rwEligibility?.adminport, isDynamicRoute, rwEligibility?.headless === true), "utf8");
|
|
165
165
|
chmodSync(scriptPath, SCRIPT_MODE);
|
|
166
166
|
}
|
|
167
167
|
mkdirSync(join(staging, "package"), { recursive: true });
|
|
@@ -580,6 +580,7 @@ function renderUninstallWizard() {
|
|
|
580
580
|
],
|
|
581
581
|
}];
|
|
582
582
|
}
|
|
583
|
+
const LEGACY_BROKER_STATE_REGISTRY_CODE = "COMPOSE_LEGACY_BROKER_STATE_PRESENT";
|
|
583
584
|
function renderDynamicPreinstMaterializer(contract) {
|
|
584
585
|
const allParams = collectInstallParameters(contract);
|
|
585
586
|
const scalarParams = allParams.filter(({ param }) => param.type === "string" || param.type === "boolean");
|
|
@@ -591,6 +592,18 @@ function renderDynamicPreinstMaterializer(contract) {
|
|
|
591
592
|
"umask 077",
|
|
592
593
|
"# D3/D5.4 package-only carrier; values are wizard answers, canonical values, or defaults.",
|
|
593
594
|
"die() { echo \"dynamic route materialization failed: $1\" >&2; exit 1; }",
|
|
595
|
+
`# registry-code="${LEGACY_BROKER_STATE_REGISTRY_CODE}"`,
|
|
596
|
+
'APPDATA="${SYNOPKG_PKGDEST_VOL:?}/@appdata/${SYNOPKG_PKGNAME:?}"',
|
|
597
|
+
'if [ -d "$APPDATA" ]; then',
|
|
598
|
+
' broker_uid="$(id -u SynoPkgLandService 2>/dev/null || true)"',
|
|
599
|
+
' if [ -n "$broker_uid" ]; then',
|
|
600
|
+
' for item in "$APPDATA"/* "$APPDATA"/.[!.]* "$APPDATA"/..?*; do',
|
|
601
|
+
' [ -e "$item" ] || continue',
|
|
602
|
+
' item_uid="$(stat -c %u "$item" 2>/dev/null || true)"',
|
|
603
|
+
' [ "$item_uid" = "$broker_uid" ] && die "MIGRATION_UNSUPPORTED: ${SYNOPKG_PKGNAME} 曾由 Broker installer 安裝;dynamic 路線不支援就地遷移,請以新的 Package Identity 安裝或先完整卸載並清除 App Data"',
|
|
604
|
+
" done",
|
|
605
|
+
" fi",
|
|
606
|
+
"fi",
|
|
594
607
|
'T="${SYNOPKG_PKGINST_TEMP_DIR:?}"',
|
|
595
608
|
'STATUS="${SYNOPKG_PKG_STATUS:?}"',
|
|
596
609
|
'PROJECT="$T/compose"',
|
|
@@ -677,12 +690,13 @@ function renderDynamicPreinstMaterializer(contract) {
|
|
|
677
690
|
}
|
|
678
691
|
if (dsmUserParam) {
|
|
679
692
|
const name = dsmUserParam.name;
|
|
680
|
-
|
|
693
|
+
const rawUserVar = "D54_RAW_USER_" + name;
|
|
694
|
+
lines.push(` append_env "${name}" "$${rawUserVar}" "string"`, ` append_env "${name}_UID" "$${name}_UID" "string"`, ` append_env "${name}_GID" "$${name}_GID" "string"`, ` idx=1`, ` for g in $${name}_GROUPS; do`, ` append_env "${name}_GROUP_\${idx}" "$g" "string"`, ` idx=$((idx + 1))`, ` done`);
|
|
681
695
|
}
|
|
682
696
|
lines.push(" } > \"$tmp\"", " mv \"$tmp\" \"$RUNTIME_ENV\"", " trap - EXIT HUP INT TERM", " [ ! -L \"$RUNTIME_ENV\" ] || die \"runtime file became a symlink\"", " [ \"$(stat -c %a \"$RUNTIME_ENV\")\" = \"600\" ] || die \"runtime env mode is not 0600\"", "exit 0");
|
|
683
697
|
return lines.join("\n") + "\n";
|
|
684
698
|
}
|
|
685
|
-
function renderDynamicCanonicalMirror() {
|
|
699
|
+
function renderDynamicCanonicalMirror(headless = false) {
|
|
686
700
|
const lines = [
|
|
687
701
|
"#!/bin/sh",
|
|
688
702
|
"set -eu",
|
|
@@ -719,14 +733,17 @@ function renderDynamicCanonicalMirror() {
|
|
|
719
733
|
'trap - EXIT HUP INT TERM',
|
|
720
734
|
'[ ! -L "$CANONICAL_ENV" ] || die "canonical file became a symlink"',
|
|
721
735
|
'[ "$(stat -c %a "$CANONICAL_ENV")" = "600" ] || die "canonical env mode is not 0600"',
|
|
722
|
-
"exit 0",
|
|
723
736
|
];
|
|
737
|
+
if (headless) {
|
|
738
|
+
lines.push('MARKER="${PKGVAR}/.docker-project-managed"', '[ ! -L "$MARKER" ] || die "docker-project marker is a symlink"', 'marker_tmp="$(mktemp "$PKGVAR/.docker-project-managed.tmp.XXXXXX")"', 'printf \'%s\\n\' managed > "$marker_tmp"', 'chmod 600 "$marker_tmp"', 'mv "$marker_tmp" "$MARKER"');
|
|
739
|
+
}
|
|
740
|
+
lines.push("exit 0");
|
|
724
741
|
return lines.join("\n") + "\n";
|
|
725
742
|
}
|
|
726
743
|
function shellQuote(value) {
|
|
727
744
|
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
728
745
|
}
|
|
729
|
-
function renderManagedScript(name, contract, nativeContext, isResourceWorker = false, resourceWorkerAdminport, isDynamicRoute = false) {
|
|
746
|
+
function renderManagedScript(name, contract, nativeContext, isResourceWorker = false, resourceWorkerAdminport, isDynamicRoute = false, isHeadlessRoute = false) {
|
|
730
747
|
if (nativeContext) {
|
|
731
748
|
const lifecycleInput = {
|
|
732
749
|
packageIdentity: String(contract.package),
|
|
@@ -766,13 +783,16 @@ fi
|
|
|
766
783
|
}
|
|
767
784
|
if (isResourceWorker) {
|
|
768
785
|
if (name === "start-stop-status") {
|
|
769
|
-
return renderResourceWorkerStartStopStatus(resourceWorkerAdminport);
|
|
786
|
+
return renderResourceWorkerStartStopStatus(String(contract.package), resourceWorkerAdminport, isHeadlessRoute);
|
|
770
787
|
}
|
|
771
788
|
if (isDynamicRoute && name === "preinst") {
|
|
772
789
|
return renderDynamicPreinstMaterializer(contract);
|
|
773
790
|
}
|
|
774
791
|
if (isDynamicRoute && (name === "postinst" || name === "postupgrade")) {
|
|
775
|
-
return renderDynamicCanonicalMirror();
|
|
792
|
+
return renderDynamicCanonicalMirror(isHeadlessRoute);
|
|
793
|
+
}
|
|
794
|
+
if (isHeadlessRoute && (name === "postinst" || name === "postupgrade")) {
|
|
795
|
+
return renderHeadlessResourceMarker();
|
|
776
796
|
}
|
|
777
797
|
if (name === "postuninst") {
|
|
778
798
|
const projectRoot = isDynamicRoute ? 'PROJECT="${SYNOPKG_PKGDEST}/compose"\n' : "";
|
|
@@ -11,4 +11,5 @@ export declare function renderResourceConfig(contract: AppContract, eligibility:
|
|
|
11
11
|
* the worker timing hooks; the package script must wait for that operation to
|
|
12
12
|
* leave a reachable service and must not claim running unconditionally.
|
|
13
13
|
*/
|
|
14
|
-
export declare function renderResourceWorkerStartStopStatus(adminport?: number): string;
|
|
14
|
+
export declare function renderResourceWorkerStartStopStatus(packageId: string, adminport?: number, headless?: boolean): string;
|
|
15
|
+
export declare function renderHeadlessResourceMarker(): string;
|
|
@@ -77,7 +77,50 @@ export function renderResourceConfig(contract, eligibility) {
|
|
|
77
77
|
* the worker timing hooks; the package script must wait for that operation to
|
|
78
78
|
* leave a reachable service and must not claim running unconditionally.
|
|
79
79
|
*/
|
|
80
|
-
export function renderResourceWorkerStartStopStatus(adminport) {
|
|
80
|
+
export function renderResourceWorkerStartStopStatus(packageId, adminport, headless = false) {
|
|
81
|
+
if (headless) {
|
|
82
|
+
const projectName = composeProjectName(packageId);
|
|
83
|
+
return `#!/bin/sh
|
|
84
|
+
PKGVAR="\${SYNOPKG_PKGVAR:-/var/packages/${packageId}/var}"
|
|
85
|
+
MARKER="$PKGVAR/.docker-project-managed"
|
|
86
|
+
PROJECT_NAME="${projectName}"
|
|
87
|
+
|
|
88
|
+
project_containers_running() {
|
|
89
|
+
command -v docker >/dev/null 2>&1 || return 2
|
|
90
|
+
containers="$(docker ps -aq --filter "label=com.docker.compose.project=$PROJECT_NAME" 2>/dev/null)" || return 2
|
|
91
|
+
[ -n "$containers" ] || return 1
|
|
92
|
+
for container in $containers; do
|
|
93
|
+
state="$(docker inspect --format '{{.State.Running}}' "$container" 2>/dev/null)" || return 2
|
|
94
|
+
[ "$state" = "true" ] || return 1
|
|
95
|
+
done
|
|
96
|
+
return 0
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
marker_managed() {
|
|
100
|
+
[ -f "$MARKER" ] || return 1
|
|
101
|
+
[ ! -L "$MARKER" ] || return 1
|
|
102
|
+
[ "$(cat "$MARKER" 2>/dev/null || true)" = "managed" ]
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
case "$1" in
|
|
106
|
+
start|status)
|
|
107
|
+
project_containers_running
|
|
108
|
+
probe_status=$?
|
|
109
|
+
if [ "$probe_status" -eq 0 ]; then
|
|
110
|
+
exit 0
|
|
111
|
+
fi
|
|
112
|
+
# Package user may not have a usable Docker inspection path. DSM owns the
|
|
113
|
+
# docker-project lifecycle; the postinst marker records that ownership.
|
|
114
|
+
if [ "$probe_status" -eq 2 ] && marker_managed; then
|
|
115
|
+
exit 0
|
|
116
|
+
fi
|
|
117
|
+
exit 3
|
|
118
|
+
;;
|
|
119
|
+
stop) exit 0 ;;
|
|
120
|
+
*) exit 1 ;;
|
|
121
|
+
esac
|
|
122
|
+
`;
|
|
123
|
+
}
|
|
81
124
|
if (adminport === undefined) {
|
|
82
125
|
return `#!/bin/sh
|
|
83
126
|
case "$1" in
|
|
@@ -138,3 +181,20 @@ case "$1" in
|
|
|
138
181
|
esac
|
|
139
182
|
`;
|
|
140
183
|
}
|
|
184
|
+
export function renderHeadlessResourceMarker() {
|
|
185
|
+
return `#!/bin/sh
|
|
186
|
+
set -eu
|
|
187
|
+
PKGVAR="\${SYNOPKG_PKGVAR:?}"
|
|
188
|
+
[ -d "$PKGVAR" ] || mkdir -p "$PKGVAR"
|
|
189
|
+
[ ! -L "$PKGVAR" ] || { echo "package var is a symlink" >&2; exit 1; }
|
|
190
|
+
MARKER="$PKGVAR/.docker-project-managed"
|
|
191
|
+
[ ! -L "$MARKER" ] || { echo "docker-project marker is a symlink" >&2; exit 1; }
|
|
192
|
+
tmp="$(mktemp "$PKGVAR/.docker-project-managed.tmp.XXXXXX")"
|
|
193
|
+
trap 'rm -f "$tmp"' EXIT HUP INT TERM
|
|
194
|
+
printf '%s\n' managed > "$tmp"
|
|
195
|
+
chmod 600 "$tmp"
|
|
196
|
+
mv "$tmp" "$MARKER"
|
|
197
|
+
trap - EXIT HUP INT TERM
|
|
198
|
+
exit 0
|
|
199
|
+
`;
|
|
200
|
+
}
|
|
@@ -14,6 +14,7 @@ export type ResourceWorkerIneligibleReason = "HAS_INSTALL_PARAMETERS" | "HAS_COM
|
|
|
14
14
|
export interface ResourceWorkerEligibility {
|
|
15
15
|
eligible: boolean;
|
|
16
16
|
reasons: ResourceWorkerIneligibleReason[];
|
|
17
|
+
headless: boolean;
|
|
17
18
|
adminport?: number;
|
|
18
19
|
}
|
|
19
20
|
/**
|
|
@@ -24,7 +25,7 @@ export interface ResourceWorkerEligibility {
|
|
|
24
25
|
* - compose secrets or configs
|
|
25
26
|
* - build: stanzas
|
|
26
27
|
* - privileged / host network / device / root-like capabilities
|
|
27
|
-
* - any App without a unique literal published adminport status probe
|
|
28
|
+
* - any non-headless App without a unique literal published adminport status probe
|
|
28
29
|
*/
|
|
29
30
|
export declare function resourceWorkerEligibility(contract: AppContract, compose: ComposeDocument): ResourceWorkerEligibility;
|
|
30
31
|
export type ComposeRoute = "static-zero-config" | "dynamic" | "broker-installer" | "reject";
|
package/dist/compose/analyze.js
CHANGED
|
@@ -112,10 +112,11 @@ export function collectPublishedTcpPorts(compose) {
|
|
|
112
112
|
* - compose secrets or configs
|
|
113
113
|
* - build: stanzas
|
|
114
114
|
* - privileged / host network / device / root-like capabilities
|
|
115
|
-
* - any App without a unique literal published adminport status probe
|
|
115
|
+
* - any non-headless App without a unique literal published adminport status probe
|
|
116
116
|
*/
|
|
117
117
|
export function resourceWorkerEligibility(contract, compose) {
|
|
118
118
|
const reasons = [];
|
|
119
|
+
const headless = contract.adminport === undefined && collectPublishedTcpPorts(compose).size === 0;
|
|
119
120
|
if (contract.install && Object.keys(contract.install).length > 0) {
|
|
120
121
|
reasons.push("HAS_INSTALL_PARAMETERS");
|
|
121
122
|
}
|
|
@@ -134,7 +135,9 @@ export function resourceWorkerEligibility(contract, compose) {
|
|
|
134
135
|
}
|
|
135
136
|
let resolvedAdminport;
|
|
136
137
|
if (contract.adminport === undefined) {
|
|
137
|
-
|
|
138
|
+
if (!headless) {
|
|
139
|
+
reasons.push("WEB_APP_MISSING_OR_AMBIGUOUS_ADMINPORT");
|
|
140
|
+
}
|
|
138
141
|
}
|
|
139
142
|
else {
|
|
140
143
|
const rawPort = Number(contract.adminport);
|
|
@@ -155,6 +158,7 @@ export function resourceWorkerEligibility(contract, compose) {
|
|
|
155
158
|
return {
|
|
156
159
|
eligible: reasons.length === 0,
|
|
157
160
|
reasons,
|
|
161
|
+
headless,
|
|
158
162
|
adminport: resolvedAdminport,
|
|
159
163
|
};
|
|
160
164
|
}
|
|
@@ -176,6 +180,17 @@ function hasBuildStanza(compose) {
|
|
|
176
180
|
}
|
|
177
181
|
return Object.values(compose.services).some((service) => "build" in service && service.build !== undefined);
|
|
178
182
|
}
|
|
183
|
+
const HEADLESS_BROKER_FALLBACK_REASONS = new Set([
|
|
184
|
+
"HAS_COMPOSE_SECRETS_OR_CONFIGS",
|
|
185
|
+
"HAS_BUILD_STANZA",
|
|
186
|
+
"HAS_PRIVILEGED_CAPABILITY",
|
|
187
|
+
]);
|
|
188
|
+
const HEADLESS_PACKAGE_ONLY_PARAMETER_TYPES = new Set([
|
|
189
|
+
"string",
|
|
190
|
+
"boolean",
|
|
191
|
+
"shared_folder",
|
|
192
|
+
"dsm_user",
|
|
193
|
+
]);
|
|
179
194
|
/**
|
|
180
195
|
* Chooses the local route D0 can safely emit. The first dynamic slice is
|
|
181
196
|
* deliberately small: only string/boolean wizard answers interpolated as
|
|
@@ -185,13 +200,18 @@ function hasBuildStanza(compose) {
|
|
|
185
200
|
*/
|
|
186
201
|
export function classifyComposeRoute(contract, parsed, composeFile = "compose.yaml") {
|
|
187
202
|
const eligibility = resourceWorkerEligibility(contract, parsed.compose);
|
|
188
|
-
const
|
|
203
|
+
const hasHeadlessUnsupportedParameter = collectInstallParameters(contract).some(({ param }) => !HEADLESS_PACKAGE_ONLY_PARAMETER_TYPES.has(String(param.type)));
|
|
204
|
+
const headlessPackageOnly = eligibility.headless &&
|
|
205
|
+
!hasHeadlessUnsupportedParameter &&
|
|
206
|
+
!eligibility.reasons.some((reason) => HEADLESS_BROKER_FALLBACK_REASONS.has(reason));
|
|
207
|
+
const localRoute = contract.adminport !== undefined || headlessPackageOnly;
|
|
208
|
+
const rejectErrors = localRoute
|
|
189
209
|
? collectRouteRejectErrors(contract, parsed, composeFile)
|
|
190
210
|
: [];
|
|
191
211
|
if (rejectErrors.length > 0) {
|
|
192
212
|
return { route: "reject", eligibility, errors: rejectErrors };
|
|
193
213
|
}
|
|
194
|
-
const dynamicError =
|
|
214
|
+
const dynamicError = localRoute
|
|
195
215
|
? collectDynamicRouteError(contract, parsed, composeFile)
|
|
196
216
|
: undefined;
|
|
197
217
|
if (dynamicError) {
|
|
@@ -206,9 +226,11 @@ export function classifyComposeRoute(contract, parsed, composeFile = "compose.ya
|
|
|
206
226
|
}
|
|
207
227
|
// Keep the route decision in one place. build-spk must not independently
|
|
208
228
|
// reinterpret an ineligible Compose as D1: only a route with a resolved
|
|
209
|
-
// published adminport
|
|
210
|
-
// package-only materializer. Every other
|
|
211
|
-
|
|
229
|
+
// published adminport or an eligible headless Compose, plus at least one
|
|
230
|
+
// declared install parameter, has a package-only materializer. Every other
|
|
231
|
+
// buildable fallback is Broker.
|
|
232
|
+
if ((eligibility.adminport !== undefined || headlessPackageOnly) &&
|
|
233
|
+
collectInstallParameters(contract).length > 0) {
|
|
212
234
|
return { route: "dynamic", eligibility, errors: [] };
|
|
213
235
|
}
|
|
214
236
|
return { route: "broker-installer", eligibility, errors: [] };
|
|
@@ -239,7 +261,8 @@ function collectRouteRejectErrors(contract, parsed, composeFile) {
|
|
|
239
261
|
return dedupeValidationErrors(errors);
|
|
240
262
|
}
|
|
241
263
|
function collectPermanentRouteErrors(contract, parsed, composeFile) {
|
|
242
|
-
|
|
264
|
+
const headless = contract.adminport === undefined && collectPublishedTcpPorts(parsed.compose).size === 0;
|
|
265
|
+
if (contract.adminport === undefined && !headless) {
|
|
243
266
|
return [];
|
|
244
267
|
}
|
|
245
268
|
const { compose } = parsed;
|
package/dist/doctor.js
CHANGED
|
@@ -472,14 +472,36 @@ export async function runDoctor(options) {
|
|
|
472
472
|
else {
|
|
473
473
|
const info = await host.getAttestationInfo(targetPackageId);
|
|
474
474
|
if (info.readiness === "NOT_INSTALLED") {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
475
|
+
// Check if package is package-only route via Broker diagnostics
|
|
476
|
+
let isPackageOnly = false;
|
|
477
|
+
if (host.getDsmDiagnostics) {
|
|
478
|
+
try {
|
|
479
|
+
const diag = await host.getDsmDiagnostics(targetPackageId);
|
|
480
|
+
if (diag?.isPackageOnly === true || diag?.packageRoute === "package-only") {
|
|
481
|
+
isPackageOnly = true;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
// ignore
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (isPackageOnly) {
|
|
489
|
+
checks.push({
|
|
490
|
+
id: "host.package_binding",
|
|
491
|
+
status: "skip",
|
|
492
|
+
message: "package-only 路線,無 Broker binding(略過)",
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
const errorEntry = getErrorCode("PROJECT_BINDING_NOT_FOUND");
|
|
497
|
+
checks.push({
|
|
498
|
+
id: "host.package_binding",
|
|
499
|
+
status: "fail",
|
|
500
|
+
code: "PROJECT_BINDING_NOT_FOUND",
|
|
501
|
+
message: `套件 ${targetPackageId} 在 Broker 中無有效 Project Binding`,
|
|
502
|
+
suggestion: errorEntry?.suggestion ?? "請執行 synopkgland deploy 重新部署套件。",
|
|
503
|
+
});
|
|
504
|
+
}
|
|
483
505
|
}
|
|
484
506
|
else if (info.readiness === "PROJECT_BINDING_DRIFTED") {
|
|
485
507
|
const errorEntry = getErrorCode("PROJECT_BINDING_DRIFTED");
|
package/dist/error-registry.js
CHANGED
|
@@ -116,6 +116,11 @@ export const ERROR_REGISTRY = {
|
|
|
116
116
|
description: "Compose dynamic route 超出 D1/D2 白名單,或使用 DSM worker 不會載入的 secret/config 來源。",
|
|
117
117
|
suggestion: "請只使用 string/boolean wizard answer 的 ${NAME} interpolation;secret/config 因 DSM worker 不讀 docker-compose.override.yml 而拒收。",
|
|
118
118
|
},
|
|
119
|
+
COMPOSE_LEGACY_BROKER_STATE_PRESENT: {
|
|
120
|
+
code: "COMPOSE_LEGACY_BROKER_STATE_PRESENT",
|
|
121
|
+
description: "偵測到既有 Broker installer App Data,dynamic package-only route 不支援同一 Package Identity 的就地遷移。",
|
|
122
|
+
suggestion: "請改用新的 Package Identity 安裝,或先完整卸載舊套件並清除 App Data。",
|
|
123
|
+
},
|
|
119
124
|
COMPOSE_BUILD_FORBIDDEN: {
|
|
120
125
|
code: "COMPOSE_BUILD_FORBIDDEN",
|
|
121
126
|
description: "D0 Compose route 禁止在 NAS install-time 使用 build:;build capability 永久拒收。",
|
package/dist/host/fake-nas.d.ts
CHANGED
package/dist/host/types.d.ts
CHANGED
|
@@ -74,6 +74,8 @@ export interface NasHost {
|
|
|
74
74
|
fetchContainerLogs(packageIdentity: string): Promise<string[]>;
|
|
75
75
|
getDsmDiagnostics?(packageIdentity?: string): Promise<{
|
|
76
76
|
dsmResidue?: DsmResidueInventory;
|
|
77
|
+
packageRoute?: "package-only" | "broker-installer" | string;
|
|
78
|
+
isPackageOnly?: boolean;
|
|
77
79
|
[key: string]: unknown;
|
|
78
80
|
} | undefined>;
|
|
79
81
|
smokeRequireLogin(input: {
|