@synopackageland/cli 0.3.4 → 0.3.6
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 +33 -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 +51 -8
- package/dist/error-registry.js +5 -0
- package/dist/host/fake-nas.d.ts +3 -1
- package/dist/host/types.d.ts +3 -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 });
|
|
@@ -192,6 +192,11 @@ export async function buildSpk(options) {
|
|
|
192
192
|
copyFileSync(options.project.composePath, join(resourceProjectDir, "docker-compose.yml"));
|
|
193
193
|
}
|
|
194
194
|
chmodSync(join(resourceProjectDir, "docker-compose.yml"), FILE_MODE);
|
|
195
|
+
const routeMarkerDir = join(staging, "package", ".synopkgland");
|
|
196
|
+
mkdirSync(routeMarkerDir, { recursive: true });
|
|
197
|
+
const routeMarker = join(routeMarkerDir, "route");
|
|
198
|
+
writeFileSync(routeMarker, "package-only", "utf8");
|
|
199
|
+
chmodSync(routeMarker, FILE_MODE);
|
|
195
200
|
}
|
|
196
201
|
const htmlDir = join(options.project.root, "html");
|
|
197
202
|
if (existsSync(htmlDir)) {
|
|
@@ -580,6 +585,7 @@ function renderUninstallWizard() {
|
|
|
580
585
|
],
|
|
581
586
|
}];
|
|
582
587
|
}
|
|
588
|
+
const LEGACY_BROKER_STATE_REGISTRY_CODE = "COMPOSE_LEGACY_BROKER_STATE_PRESENT";
|
|
583
589
|
function renderDynamicPreinstMaterializer(contract) {
|
|
584
590
|
const allParams = collectInstallParameters(contract);
|
|
585
591
|
const scalarParams = allParams.filter(({ param }) => param.type === "string" || param.type === "boolean");
|
|
@@ -591,6 +597,18 @@ function renderDynamicPreinstMaterializer(contract) {
|
|
|
591
597
|
"umask 077",
|
|
592
598
|
"# D3/D5.4 package-only carrier; values are wizard answers, canonical values, or defaults.",
|
|
593
599
|
"die() { echo \"dynamic route materialization failed: $1\" >&2; exit 1; }",
|
|
600
|
+
`# registry-code="${LEGACY_BROKER_STATE_REGISTRY_CODE}"`,
|
|
601
|
+
'APPDATA="${SYNOPKG_PKGDEST_VOL:?}/@appdata/${SYNOPKG_PKGNAME:?}"',
|
|
602
|
+
'if [ -d "$APPDATA" ]; then',
|
|
603
|
+
' broker_uid="$(id -u SynoPkgLandService 2>/dev/null || true)"',
|
|
604
|
+
' if [ -n "$broker_uid" ]; then',
|
|
605
|
+
' for item in "$APPDATA"/* "$APPDATA"/.[!.]* "$APPDATA"/..?*; do',
|
|
606
|
+
' [ -e "$item" ] || continue',
|
|
607
|
+
' item_uid="$(stat -c %u "$item" 2>/dev/null || true)"',
|
|
608
|
+
' [ "$item_uid" = "$broker_uid" ] && die "MIGRATION_UNSUPPORTED: ${SYNOPKG_PKGNAME} 曾由 Broker installer 安裝;dynamic 路線不支援就地遷移,請以新的 Package Identity 安裝或先完整卸載並清除 App Data"',
|
|
609
|
+
" done",
|
|
610
|
+
" fi",
|
|
611
|
+
"fi",
|
|
594
612
|
'T="${SYNOPKG_PKGINST_TEMP_DIR:?}"',
|
|
595
613
|
'STATUS="${SYNOPKG_PKG_STATUS:?}"',
|
|
596
614
|
'PROJECT="$T/compose"',
|
|
@@ -677,12 +695,13 @@ function renderDynamicPreinstMaterializer(contract) {
|
|
|
677
695
|
}
|
|
678
696
|
if (dsmUserParam) {
|
|
679
697
|
const name = dsmUserParam.name;
|
|
680
|
-
|
|
698
|
+
const rawUserVar = "D54_RAW_USER_" + name;
|
|
699
|
+
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
700
|
}
|
|
682
701
|
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
702
|
return lines.join("\n") + "\n";
|
|
684
703
|
}
|
|
685
|
-
function renderDynamicCanonicalMirror() {
|
|
704
|
+
function renderDynamicCanonicalMirror(headless = false) {
|
|
686
705
|
const lines = [
|
|
687
706
|
"#!/bin/sh",
|
|
688
707
|
"set -eu",
|
|
@@ -719,14 +738,17 @@ function renderDynamicCanonicalMirror() {
|
|
|
719
738
|
'trap - EXIT HUP INT TERM',
|
|
720
739
|
'[ ! -L "$CANONICAL_ENV" ] || die "canonical file became a symlink"',
|
|
721
740
|
'[ "$(stat -c %a "$CANONICAL_ENV")" = "600" ] || die "canonical env mode is not 0600"',
|
|
722
|
-
"exit 0",
|
|
723
741
|
];
|
|
742
|
+
if (headless) {
|
|
743
|
+
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"');
|
|
744
|
+
}
|
|
745
|
+
lines.push("exit 0");
|
|
724
746
|
return lines.join("\n") + "\n";
|
|
725
747
|
}
|
|
726
748
|
function shellQuote(value) {
|
|
727
749
|
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
728
750
|
}
|
|
729
|
-
function renderManagedScript(name, contract, nativeContext, isResourceWorker = false, resourceWorkerAdminport, isDynamicRoute = false) {
|
|
751
|
+
function renderManagedScript(name, contract, nativeContext, isResourceWorker = false, resourceWorkerAdminport, isDynamicRoute = false, isHeadlessRoute = false) {
|
|
730
752
|
if (nativeContext) {
|
|
731
753
|
const lifecycleInput = {
|
|
732
754
|
packageIdentity: String(contract.package),
|
|
@@ -766,13 +788,16 @@ fi
|
|
|
766
788
|
}
|
|
767
789
|
if (isResourceWorker) {
|
|
768
790
|
if (name === "start-stop-status") {
|
|
769
|
-
return renderResourceWorkerStartStopStatus(resourceWorkerAdminport);
|
|
791
|
+
return renderResourceWorkerStartStopStatus(String(contract.package), resourceWorkerAdminport, isHeadlessRoute);
|
|
770
792
|
}
|
|
771
793
|
if (isDynamicRoute && name === "preinst") {
|
|
772
794
|
return renderDynamicPreinstMaterializer(contract);
|
|
773
795
|
}
|
|
774
796
|
if (isDynamicRoute && (name === "postinst" || name === "postupgrade")) {
|
|
775
|
-
return renderDynamicCanonicalMirror();
|
|
797
|
+
return renderDynamicCanonicalMirror(isHeadlessRoute);
|
|
798
|
+
}
|
|
799
|
+
if (isHeadlessRoute && (name === "postinst" || name === "postupgrade")) {
|
|
800
|
+
return renderHeadlessResourceMarker();
|
|
776
801
|
}
|
|
777
802
|
if (name === "postuninst") {
|
|
778
803
|
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
|
@@ -6,6 +6,19 @@ import { loadContract } from "./contract/load.js";
|
|
|
6
6
|
import { getErrorCode } from "./error-registry.js";
|
|
7
7
|
import { dsmFetch } from "./host/dsm-http.js";
|
|
8
8
|
import { appOriginFromContract } from "./host/open-url.js";
|
|
9
|
+
function packageRouteFromDiagnostics(diagnostics) {
|
|
10
|
+
switch (diagnostics?.packageRoute) {
|
|
11
|
+
case "package-only":
|
|
12
|
+
case "broker-installer":
|
|
13
|
+
case "unknown":
|
|
14
|
+
return diagnostics.packageRoute;
|
|
15
|
+
default:
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
// Keep compatibility with the boolean-only response emitted by older
|
|
19
|
+
// Brokers, but never infer broker-installer from missing evidence.
|
|
20
|
+
return diagnostics?.isPackageOnly === true ? "package-only" : "unknown";
|
|
21
|
+
}
|
|
9
22
|
const PROJECT_FOUR_FILES = ["synology-app.yaml", "README.md", "icon.png"];
|
|
10
23
|
function packageFailureSummary(state) {
|
|
11
24
|
const fields = [];
|
|
@@ -472,14 +485,44 @@ export async function runDoctor(options) {
|
|
|
472
485
|
else {
|
|
473
486
|
const info = await host.getAttestationInfo(targetPackageId);
|
|
474
487
|
if (info.readiness === "NOT_INSTALLED") {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
488
|
+
// Check the diagnosed route before treating a missing binding
|
|
489
|
+
// as a Broker-installer failure. Unknown is deliberately
|
|
490
|
+
// non-failing because the Broker may not be able to read the
|
|
491
|
+
// DSM package resource.
|
|
492
|
+
let packageRoute = "unknown";
|
|
493
|
+
if (host.getDsmDiagnostics) {
|
|
494
|
+
try {
|
|
495
|
+
const diag = await host.getDsmDiagnostics(targetPackageId);
|
|
496
|
+
packageRoute = packageRouteFromDiagnostics(diag);
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
// ignore
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (packageRoute === "package-only") {
|
|
503
|
+
checks.push({
|
|
504
|
+
id: "host.package_binding",
|
|
505
|
+
status: "skip",
|
|
506
|
+
message: "package-only 路線,無 Broker binding(略過)",
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
else if (packageRoute === "unknown") {
|
|
510
|
+
checks.push({
|
|
511
|
+
id: "host.package_binding",
|
|
512
|
+
status: "skip",
|
|
513
|
+
message: "Broker 無法判定套件路線,略過 Package Binding 檢查",
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
const errorEntry = getErrorCode("PROJECT_BINDING_NOT_FOUND");
|
|
518
|
+
checks.push({
|
|
519
|
+
id: "host.package_binding",
|
|
520
|
+
status: "fail",
|
|
521
|
+
code: "PROJECT_BINDING_NOT_FOUND",
|
|
522
|
+
message: `套件 ${targetPackageId} 在 Broker 中無有效 Project Binding`,
|
|
523
|
+
suggestion: errorEntry?.suggestion ?? "請執行 synopkgland deploy 重新部署套件。",
|
|
524
|
+
});
|
|
525
|
+
}
|
|
483
526
|
}
|
|
484
527
|
else if (info.readiness === "PROJECT_BINDING_DRIFTED") {
|
|
485
528
|
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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AttestationKind, HostInstallRecord, NasHost } from "./types.js";
|
|
1
|
+
import type { AttestationKind, HostInstallRecord, NasHost, PackageRoute } from "./types.js";
|
|
2
2
|
export interface FakeNasHostOptions {
|
|
3
3
|
curated?: string[];
|
|
4
4
|
dsmOrigin?: string;
|
|
@@ -8,6 +8,8 @@ export interface FakeNasHostOptions {
|
|
|
8
8
|
installedEnv?: Record<string, Record<string, string>>;
|
|
9
9
|
diagnostics?: {
|
|
10
10
|
dsmResidue?: import("./types.js").DsmResidueInventory;
|
|
11
|
+
packageRoute?: PackageRoute;
|
|
12
|
+
isPackageOnly?: boolean;
|
|
11
13
|
[key: string]: unknown;
|
|
12
14
|
};
|
|
13
15
|
}
|
package/dist/host/types.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { InstalledPackageState } from "./catalog-install.js";
|
|
2
2
|
export type AttestationKind = "local" | "app-package" | "none";
|
|
3
|
+
export type PackageRoute = "package-only" | "broker-installer" | "unknown";
|
|
3
4
|
export interface DeployIdentity {
|
|
4
5
|
packageId: string;
|
|
5
6
|
displayname: string;
|
|
@@ -74,6 +75,8 @@ export interface NasHost {
|
|
|
74
75
|
fetchContainerLogs(packageIdentity: string): Promise<string[]>;
|
|
75
76
|
getDsmDiagnostics?(packageIdentity?: string): Promise<{
|
|
76
77
|
dsmResidue?: DsmResidueInventory;
|
|
78
|
+
packageRoute?: PackageRoute;
|
|
79
|
+
isPackageOnly?: boolean;
|
|
77
80
|
[key: string]: unknown;
|
|
78
81
|
} | undefined>;
|
|
79
82
|
smokeRequireLogin(input: {
|