@synopackageland/cli 0.15.3 → 0.16.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 +22 -8
- package/README.md +3 -0
- package/assets/init-compose.md +33 -0
- package/assets/init-upstream/build-source.sh +10 -0
- package/assets/init-upstream/update-image.mjs +26 -0
- package/assets/init-upstream/upgrade.sh +47 -0
- package/assets/init-upstream/version.mjs +12 -0
- package/dist/cli.js +16 -2
- package/dist/e2e/dsm.d.ts +2 -2
- package/dist/e2e/dsm.js +3 -3
- package/dist/error-registry.js +4 -0
- package/dist/host/share-permission.d.ts +1 -1
- package/dist/host/share-permission.js +1 -1
- package/dist/init-upstream.d.ts +7 -0
- package/dist/init-upstream.js +64 -0
- package/dist/init.d.ts +4 -0
- package/dist/init.js +37 -1
- package/dist/runtime/dsm-version.d.ts +1 -1
- package/dist/runtime/dsm-version.js +1 -1
- package/dist/runtime/executor-digests.json +1 -23
- package/dist/skill.js +1 -0
- package/dist/standalone/build.js +3 -1
- package/dist/standalone/compose-errors.js +15 -0
- package/dist/standalone/compose.d.ts +8 -0
- package/dist/standalone/compose.js +16 -6
- package/dist/standalone/daemon.js +2 -2
- package/dist/standalone/dsm-window.js +2 -2
- package/dist/standalone/identity-gateway.js +1 -0
- package/dist/standalone/inspect-spk.js +11 -0
- package/dist/standalone/lifecycle.js +4 -1
- package/dist/standalone/manifest.js +6 -1
- package/dist/standalone/portal-window.js +1 -1
- package/dist/standalone/spk.d.ts +1 -0
- package/dist/standalone/spk.js +6 -0
- package/dist/template-paths.d.ts +4 -0
- package/dist/template-paths.js +11 -0
- package/package.json +3 -3
- package/skills/SKILL.md +7 -13
- package/skills/reference/compose.md +25 -8
- package/skills/reference/create.md +2 -2
- package/skills/reference/upstream-fork.md +165 -0
- package/skills/reference/wrap.md +3 -1
|
@@ -23,6 +23,12 @@ export interface ComposePlan {
|
|
|
23
23
|
mounts_blocked: boolean;
|
|
24
24
|
acceptance_blockers: string[];
|
|
25
25
|
services: Array<{
|
|
26
|
+
storage_init?: {
|
|
27
|
+
directories: Array<{
|
|
28
|
+
base: string;
|
|
29
|
+
path: string;
|
|
30
|
+
}>;
|
|
31
|
+
};
|
|
26
32
|
service: string;
|
|
27
33
|
user: string;
|
|
28
34
|
container_root: boolean;
|
|
@@ -96,6 +102,8 @@ export declare function writeComposePayload(c: ComposeApp, root: string, target:
|
|
|
96
102
|
export declare function renderComposeInstall(pkg: string): string;
|
|
97
103
|
/** The same SDK entry point supplies the package-account install materializer. */
|
|
98
104
|
export declare function buildComposeControl(m: StandaloneManifest, arch: NativeArchFamily, work: string, run: import("./build.js").CommandRunner): Promise<string | undefined>;
|
|
105
|
+
/** The SPK supplies this read-only binary; upstream registry images stay unchanged. */
|
|
106
|
+
export declare function buildComposeStorageInit(m: StandaloneManifest, arch: NativeArchFamily, work: string, run: import("./build.js").CommandRunner): Promise<string | undefined>;
|
|
99
107
|
/** Only the build resolves registry metadata; check and inspect remain offline. */
|
|
100
108
|
export declare function lockComposeImages(m: StandaloneManifest, arch: NativeArchFamily, resolveImages: boolean, imageProofs?: unknown): StandaloneManifest;
|
|
101
109
|
export declare function composeInputs(root: string, file: string, envFile?: string): ComposePlan["inputs"];
|
|
@@ -5,7 +5,7 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync
|
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
import { parse } from "yaml";
|
|
8
|
-
import {
|
|
8
|
+
import { templateSdkVersion } from "../template-paths.js";
|
|
9
9
|
import { goChildEnv } from "../runtime/go-env.js";
|
|
10
10
|
import { sdkSource } from "../runtime/sdk-source.js";
|
|
11
11
|
import { readDaemon } from "./daemon.js";
|
|
@@ -69,7 +69,7 @@ export function readComposeApp(value, root, arches, fail, context = {}) {
|
|
|
69
69
|
if (value.sdk !== undefined &&
|
|
70
70
|
(typeof value.sdk !== "string" || !/^\d+\.\d+\.\d+$/.test(value.sdk)))
|
|
71
71
|
return invalid("compose.sdk must name the matching SDK release.", "SDK_COMPOSE_UNSUPPORTED");
|
|
72
|
-
const sdk = typeof value.sdk === "string" ? value.sdk : (context.sdk ??
|
|
72
|
+
const sdk = typeof value.sdk === "string" ? value.sdk : (context.sdk ?? templateSdkVersion());
|
|
73
73
|
let targets = {};
|
|
74
74
|
if (value.helpers !== undefined) {
|
|
75
75
|
const d = readDaemon({ exec: "_sso/gateway", targets: value.helpers }, root, arches, fail);
|
|
@@ -194,6 +194,7 @@ export function writeComposePayload(c, root, target, gateway) {
|
|
|
194
194
|
export function renderComposeInstall(pkg) {
|
|
195
195
|
return `#!/bin/sh
|
|
196
196
|
set -eu
|
|
197
|
+
"\${SYNOPKG_PKGINST_TEMP_DIR:?}/compose/control" preflight || exit $?
|
|
197
198
|
exec "\${SYNOPKG_PKGINST_TEMP_DIR:?}/compose/control" prepare --project "\${SYNOPKG_PKGINST_TEMP_DIR:?}/compose" --package '${pkg}'
|
|
198
199
|
`;
|
|
199
200
|
}
|
|
@@ -201,10 +202,19 @@ exec "\${SYNOPKG_PKGINST_TEMP_DIR:?}/compose/control" prepare --project "\${SYNO
|
|
|
201
202
|
export async function buildComposeControl(m, arch, work, run) {
|
|
202
203
|
if (!m.compose)
|
|
203
204
|
return;
|
|
205
|
+
return buildComposeTool(m, arch, work, run, "plan");
|
|
206
|
+
}
|
|
207
|
+
/** The SPK supplies this read-only binary; upstream registry images stay unchanged. */
|
|
208
|
+
export async function buildComposeStorageInit(m, arch, work, run) {
|
|
209
|
+
if (!m.compose?.plan.services.some((s) => s.storage_init))
|
|
210
|
+
return;
|
|
211
|
+
return buildComposeTool(m, arch, work, run, "storage-init");
|
|
212
|
+
}
|
|
213
|
+
async function buildComposeTool(m, arch, work, run, tool) {
|
|
204
214
|
const { pinnedGoBuildEnv, pinnedGoProblem } = await import("../build-spk/go-toolchain.js");
|
|
205
215
|
const { checkCgiBinary } = await import("./binary.js");
|
|
206
216
|
const { StandaloneBuildError } = await import("./spk.js");
|
|
207
|
-
const out = join(work, `compose
|
|
217
|
+
const out = join(work, `compose-${tool === "plan" ? "control" : tool}-${arch}`);
|
|
208
218
|
const source = sdkSource(m.compose.sdk);
|
|
209
219
|
const result = await run("go", [
|
|
210
220
|
"build",
|
|
@@ -214,13 +224,13 @@ export async function buildComposeControl(m, arch, work, run) {
|
|
|
214
224
|
"-s -w -buildid=",
|
|
215
225
|
"-o",
|
|
216
226
|
out,
|
|
217
|
-
|
|
227
|
+
`./compose/cmd/${tool}`,
|
|
218
228
|
], {
|
|
219
229
|
cwd: source.goDir,
|
|
220
230
|
env: goChildEnv({ ...process.env, ...pinnedGoBuildEnv(arch) }),
|
|
221
231
|
});
|
|
222
232
|
if (result.status !== 0 || result.missing)
|
|
223
|
-
throw new StandaloneBuildError("SDK_COMPOSE_BUILD_FAILED", `Cannot build the SDK Compose
|
|
233
|
+
throw new StandaloneBuildError("SDK_COMPOSE_BUILD_FAILED", `Cannot build the SDK Compose ${tool}: ${result.stderr}`);
|
|
224
234
|
const error = checkCgiBinary(work, out, arch);
|
|
225
235
|
if (error)
|
|
226
236
|
throw new StandaloneBuildError(error.code, error.message);
|
|
@@ -250,7 +260,7 @@ export function lockComposeImages(m, arch, resolveImages, imageProofs) {
|
|
|
250
260
|
return { ...m, compose };
|
|
251
261
|
}
|
|
252
262
|
export function composeInputs(root, file, envFile) {
|
|
253
|
-
const result = spawnSync(planner(
|
|
263
|
+
const result = spawnSync(planner(templateSdkVersion()), [
|
|
254
264
|
"sources",
|
|
255
265
|
"--root",
|
|
256
266
|
root,
|
|
@@ -3,7 +3,7 @@ import { chmodSync, copyFileSync, lstatSync, mkdirSync, readdirSync, realpathSyn
|
|
|
3
3
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
import { PINNED_GO_HINT, pinnedGoBuildEnv, pinnedGoProblem, } from "../build-spk/go-toolchain.js";
|
|
5
5
|
import { sdkSource, appSdkVersion } from "../runtime/sdk-source.js";
|
|
6
|
-
import {
|
|
6
|
+
import { templateSdkVersion } from "../template-paths.js";
|
|
7
7
|
import { checkCgiBinary } from "./binary.js";
|
|
8
8
|
import { composeDaemon, composeNetwork } from "./compose.js";
|
|
9
9
|
import { gatewayDaemon } from "./identity-gateway.js";
|
|
@@ -141,7 +141,7 @@ export async function buildDaemonControl(manifest, arch, work, run) {
|
|
|
141
141
|
(manifest.backend.kind === "go"
|
|
142
142
|
? appSdkVersion(manifest.backend)
|
|
143
143
|
: undefined) ??
|
|
144
|
-
|
|
144
|
+
templateSdkVersion()).goDir,
|
|
145
145
|
env: goChildEnv({ ...process.env, ...pinnedGoBuildEnv(arch) }),
|
|
146
146
|
});
|
|
147
147
|
if (result.missing || result.status !== 0)
|
|
@@ -256,7 +256,7 @@ ${ns}.callOnce = function (method, params, options, requestTimeout) {
|
|
|
256
256
|
|
|
257
257
|
// APP_STOPPED, APP_STARTING and EXECUTOR_STARTING mean the executor op was not
|
|
258
258
|
// sent. A stopped package waits for the user to start it in Package Center, so
|
|
259
|
-
// APP_STOPPED rejects at once
|
|
259
|
+
// APP_STOPPED rejects at once. A start ends by itself: a call repeats
|
|
260
260
|
// half a second, then doubling, for 4 s of waiting in all, so a short start
|
|
261
261
|
// shows the page nothing and the SDK's starting copy follows a long one within
|
|
262
262
|
// its own 4 s recheck; executor.control, which the window's approval starts the
|
|
@@ -264,7 +264,7 @@ ${ns}.callOnce = function (method, params, options, requestTimeout) {
|
|
|
264
264
|
// deadline; past the wait it rejects with the last refusal, and leaving the
|
|
265
265
|
// page during a wait rejects request_canceled at once. The SDK's status reads
|
|
266
266
|
// are not repeated: its panels show the stopped or starting package and check
|
|
267
|
-
// again on their own
|
|
267
|
+
// again on their own.
|
|
268
268
|
${ns}.retryCodes = ["APP_STARTING", "EXECUTOR_STARTING"];
|
|
269
269
|
${ns}.statusReads = ["delegate.status", "authorization.status", "executor.status"];
|
|
270
270
|
${ns}.call = function (method, params, options, requestTimeout) {
|
|
@@ -313,6 +313,7 @@ export function identityGatewayConfig(m) {
|
|
|
313
313
|
access: g.access ?? "dsm-users",
|
|
314
314
|
...(g.groups ? { groups: g.groups } : {}),
|
|
315
315
|
...(m.entry ? { entry: m.entry } : {}),
|
|
316
|
+
...(m.compose ? { container_app: true } : {}),
|
|
316
317
|
...(m.compose?.plan.mounts_blocked ? { compose_blocked: true } : {}),
|
|
317
318
|
...(g.max_upload_bytes ? { max_upload_bytes: g.max_upload_bytes } : {}),
|
|
318
319
|
...(g.response_header_timeout_seconds
|
|
@@ -668,6 +668,17 @@ async function inspectUnifiedCompose(dir, payload, metadata, findings) {
|
|
|
668
668
|
throw new Error(materializer.message);
|
|
669
669
|
if (payload.find((p) => p.path === "compose/control")?.mode !== "0755")
|
|
670
670
|
throw new Error("Compose materializer must be executable");
|
|
671
|
+
const initializer = join(root, "compose/storage-init");
|
|
672
|
+
if (c.plan.services.some((s) => s.storage_init)) {
|
|
673
|
+
const error = checkCgiBinary(root, initializer, arch);
|
|
674
|
+
if (error)
|
|
675
|
+
throw new Error(error.message);
|
|
676
|
+
if (payload.find((p) => p.path === "compose/storage-init")?.mode !== "0755")
|
|
677
|
+
throw new Error("Compose storage initializer must be executable");
|
|
678
|
+
}
|
|
679
|
+
else if (existsSync(initializer)) {
|
|
680
|
+
throw new Error("unexpected Compose storage initializer");
|
|
681
|
+
}
|
|
671
682
|
const binaries = [
|
|
672
683
|
"daemon/control",
|
|
673
684
|
"daemon/payload/_sso/gateway",
|
|
@@ -21,12 +21,15 @@ export function renderStandaloneScript(name, manifest) {
|
|
|
21
21
|
: renderStandaloneScript(name, { ...native, appScripts: undefined });
|
|
22
22
|
const control = `"/var/packages/${manifest.package}/target/daemon/control"`;
|
|
23
23
|
const paths = `"\${SYNOPKG_PKGDEST:-/var/packages/${manifest.package}/target}" "\${SYNOPKG_PKGVAR:-/var/packages/${manifest.package}/var}"`;
|
|
24
|
+
const preflight = name === "start-stop-status"
|
|
25
|
+
? `case "$1" in prestart|start|restart)\n "\${SYNOPKG_PKGDEST:-/var/packages/${manifest.package}/target}/compose/control" preflight || exit $? ;;\nesac\n`
|
|
26
|
+
: "";
|
|
24
27
|
const remember = name === "postinst" &&
|
|
25
28
|
manifest.compose.plan.ingress &&
|
|
26
29
|
!manifest.compose.plan.mounts_blocked
|
|
27
30
|
? `"/var/packages/${manifest.package}/target/compose/control" remember-ingress --project "/var/packages/${manifest.package}/target/compose" --package '${manifest.package}' || exit $?\n`
|
|
28
31
|
: "";
|
|
29
|
-
const ledger = remember +
|
|
32
|
+
const ledger = preflight + remember +
|
|
30
33
|
(manifest.identityGateway && !manifest.compose.plan.mounts_blocked
|
|
31
34
|
? name === "start-stop-status"
|
|
32
35
|
? `if [ "$1" = restart ]; then\n "$0" stop || exit $?\n exec "$0" start\nfi\nif [ "$1" = start ]; then\n ${control} acquire ${paths} || exit $?\nelif [ "$1" = stop ]; then\n ${control} release ${paths} || exit $?\nfi\n`
|
|
@@ -234,6 +234,10 @@ function readManifest(raw, c) {
|
|
|
234
234
|
const daemon = arches &&
|
|
235
235
|
readDaemon(raw.daemon, c.root, arches, (e) => c.fail(e.code, e.message));
|
|
236
236
|
const gatewayRaw = isRecord(raw.identity_gateway) ? raw.identity_gateway : {};
|
|
237
|
+
if (raw.compose !== undefined && isRecord(raw.ctl) && raw.ctl.enabled === true)
|
|
238
|
+
c.fail("CTL_UNSUPPORTED_BACKEND", "Compose does not support automatic ctl descriptions; disable ctl.");
|
|
239
|
+
if (isRecord(raw.compose) && isRecord(raw.compose.web) && !isRecord(raw.compose.web.health))
|
|
240
|
+
c.fail("COMPOSE_HEALTH_REQUIRED", "Select compose.web.health.path and expected 2xx status for the actual App. Readiness does not follow redirects; check is static and does not probe HTTP.");
|
|
237
241
|
const compose = arches &&
|
|
238
242
|
readComposeApp(raw.compose, c.root, arches, (e) => c.fail(e.code, e.message), {
|
|
239
243
|
package: packageId,
|
|
@@ -260,7 +264,8 @@ function readManifest(raw, c) {
|
|
|
260
264
|
: raw.identity_gateway;
|
|
261
265
|
if (compose?.web && gatewayRaw.upstream !== undefined)
|
|
262
266
|
c.fail("COMPOSE_WEB_INVALID", "Compose upstream is derived from the SDK Compose ingress endpoint; remove identity_gateway.upstream.");
|
|
263
|
-
|
|
267
|
+
// A failed Compose plan already explains the invalid upstream declaration.
|
|
268
|
+
const identityGateway = raw.compose !== undefined && !compose ? undefined : readIdentityGateway(gatewayValue, daemon || (compose ? { targets: compose.targets } : undefined), (e) => c.fail(e.code, e.message), false, Boolean(compose?.plan.ingress));
|
|
264
269
|
if (compose && identityGateway && compose.sdk !== identityGateway.sdk)
|
|
265
270
|
c.fail("STANDALONE_IDENTITY_GATEWAY_INVALID", "compose and identity_gateway must use the same SDK release.");
|
|
266
271
|
if (compose &&
|
|
@@ -204,7 +204,7 @@ export const portalRuntime = (catalogs) => `function createPortalAccess(options)
|
|
|
204
204
|
};
|
|
205
205
|
}
|
|
206
206
|
// The window reads the status when it opens, so a settings page shows the
|
|
207
|
-
// portal section as it is
|
|
207
|
+
// portal section as it is.
|
|
208
208
|
status();
|
|
209
209
|
return access;
|
|
210
210
|
}`;
|
package/dist/standalone/spk.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export interface AssembleOptions {
|
|
|
16
16
|
executor?: ExecutorArtifact;
|
|
17
17
|
daemonControl?: string;
|
|
18
18
|
composeControl?: string;
|
|
19
|
+
composeStorageInit?: string;
|
|
19
20
|
/** The SDK identity gateway, required exactly when the manifest declares identity_gateway. */
|
|
20
21
|
identityGateway?: string;
|
|
21
22
|
/** The compiled background worker, required exactly when capabilities.jobs is enabled. */
|
package/dist/standalone/spk.js
CHANGED
|
@@ -190,6 +190,12 @@ export async function assembleStandaloneSpk(options) {
|
|
|
190
190
|
throw new StandaloneBuildError("SDK_COMPOSE_BUILD_FAILED", "Compose runtime artifact required.");
|
|
191
191
|
copyFileSync(options.composeControl, join(target, "control"));
|
|
192
192
|
chmodSync(join(target, "control"), EXEC_MODE);
|
|
193
|
+
if (manifest.compose.plan.services.some((s) => s.storage_init)) {
|
|
194
|
+
if (!options.composeStorageInit)
|
|
195
|
+
throw new StandaloneBuildError("SDK_COMPOSE_BUILD_FAILED", "Compose storage initializer required.");
|
|
196
|
+
copyFileSync(options.composeStorageInit, join(target, "storage-init"));
|
|
197
|
+
chmodSync(join(target, "storage-init"), EXEC_MODE);
|
|
198
|
+
}
|
|
193
199
|
}
|
|
194
200
|
const packageTgz = join(staging, "package.tgz");
|
|
195
201
|
await createReproducibleTar(join(staging, "package"), packageTgz, {
|
package/dist/template-paths.d.ts
CHANGED
|
@@ -3,3 +3,7 @@
|
|
|
3
3
|
* package; SYNOPKGLAND_TEMPLATES_DIR points at a templates checkout instead.
|
|
4
4
|
*/
|
|
5
5
|
export declare function templatesRoot(env?: NodeJS.ProcessEnv, from?: string): string;
|
|
6
|
+
/** The installed templates' SDK pin also supplies CLI-owned runtime defaults.
|
|
7
|
+
* CLI-only patch releases must never turn their own version into an SDK tag.
|
|
8
|
+
*/
|
|
9
|
+
export declare function templateSdkVersion(env?: NodeJS.ProcessEnv): string;
|
package/dist/template-paths.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
+
import { sdkRequirement } from "./runtime/sdk-source.js";
|
|
3
4
|
/**
|
|
4
5
|
* The scaffold templates come from the exactly pinned @synopackageland/templates
|
|
5
6
|
* package; SYNOPKGLAND_TEMPLATES_DIR points at a templates checkout instead.
|
|
@@ -14,3 +15,13 @@ export function templatesRoot(env = process.env, from = import.meta.url) {
|
|
|
14
15
|
throw new Error("[TEMPLATES_MISSING] @synopackageland/templates is not installed; reinstall @synopackageland/cli, or set SYNOPKGLAND_TEMPLATES_DIR to a templates checkout.");
|
|
15
16
|
}
|
|
16
17
|
}
|
|
18
|
+
/** The installed templates' SDK pin also supplies CLI-owned runtime defaults.
|
|
19
|
+
* CLI-only patch releases must never turn their own version into an SDK tag.
|
|
20
|
+
*/
|
|
21
|
+
export function templateSdkVersion(env = process.env) {
|
|
22
|
+
const version = sdkRequirement(join(templatesRoot(env), "native-go/backend"));
|
|
23
|
+
if (!version || !/^\d+\.\d+\.\d+$/.test(version)) {
|
|
24
|
+
throw new Error("[TEMPLATES_MISSING] The installed templates have no valid SDK pin; reinstall @synopackageland/templates.");
|
|
25
|
+
}
|
|
26
|
+
return version;
|
|
27
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synopackageland/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Syno Package Land Developer Kit CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
],
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "rm -rf dist && tsc",
|
|
21
|
-
"test": "npm run build && node --import tsx --test test/*.test.ts test/ctl/*.test.ts",
|
|
21
|
+
"test": "npm run build && node --import tsx --test --test-concurrency=${SYNOPKGLAND_GATE_JOBS:-3} test/*.test.ts test/ctl/*.test.ts",
|
|
22
22
|
"test:glossary": "node --test scripts/test/glossary-drift.test.mjs",
|
|
23
23
|
"test:scripts": "node --test scripts/test/gate.test.mjs scripts/test/release.test.mjs scripts/test/pack-docker.test.mjs scripts/test/npm-pack.test.mjs scripts/test/executor-bump.test.mjs scripts/test/smoke-exports.test.mjs scripts/test/executor-digests.test.mjs scripts/test/parallel.test.mjs scripts/test/scaffold-matrix.test.mjs scripts/test/frontend-deps.test.mjs scripts/test/docker-gate.test.mjs scripts/test/gate-plan.test.mjs scripts/test/gates.test.mjs",
|
|
24
24
|
"gates": "bash scripts/ci/gates.sh",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@napi-rs/keyring": "^2.1.1",
|
|
30
|
-
"@synopackageland/templates": "0.
|
|
30
|
+
"@synopackageland/templates": "0.16.0",
|
|
31
31
|
"ajv": "^8.17.1",
|
|
32
32
|
"commander": "^13.1.0",
|
|
33
33
|
"json-canonicalize": "^3.0.1",
|
package/skills/SKILL.md
CHANGED
|
@@ -7,6 +7,8 @@ description: Initialize, extend, check, build, inspect, and deploy Synology appl
|
|
|
7
7
|
|
|
8
8
|
Use this guide to initialize, build and inspect own applications or upstream OSS as independent packages. 依已實作能力區分 DSM session 檔案、私有 jobs 與 App 登入;delegate 已實作並隨 CLI 提供固定操作的離線委託(預設 `files.moveFromStaging`);共用 delegate/rootOps executor 的完整裝置生命週期驗收仍待完成,不得宣稱已通過;支援邊界依 delegate guide/support matrix,禁止發明新的身分平台。
|
|
9
9
|
|
|
10
|
+
For an existing open-source App, first read [reference/upstream-fork.md](reference/upstream-fork.md): upstream fork/history + untouched upstream files + all packaging in `synology/`, official container images pinned by tag/index digest, native official release binaries pinned by SHA-256 or a source build hook, and a single tag-merge upgrade command. Use `init --upstream-fork`; never rewrite the App as a component. Integrated upstream Apps require direct DSM administrator entry without a separate App password or an OIDC/SSO server: supported trusted headers first, disabling native login second; the shipped OpenList pattern uses the existing SDK 0.16.0 token adapter (`mode: token`, `access: dsm-admins`, `entry: admin-only`) with server-side Authorization injection. All DSM admins share one upstream admin; token reset requires package restart; WebDAV retains its native credentials.
|
|
11
|
+
|
|
10
12
|
Start with `synopkgland init -t native-go`, `synopkgland init -t binary`, or `synopkgland init -t compose`. Read reference/create.md or reference/wrap.md, then reference/deploy.md. Native CGI session checks belong to the package. For wrapping upstream OSS with DSM accounts, read [reference/identity-gateway.md](reference/identity-gateway.md) first. For all Compose work, read [reference/compose.md](reference/compose.md): schema-2 upstream files, generated override, SDK planner, fixed package identity and current DSM blockers. Containers do not inherit a signed-in user's File Station ACL. Existing Shared Folder mounts are blocked until official protected ACL/project control is verified.
|
|
11
13
|
|
|
12
14
|
Never install, uninstall, publish or modify NAS data without task authorization. Prefer check, inspect and deploy --dry-run for reviewable artifacts.
|
|
@@ -149,18 +151,10 @@ Application Portal 是獨立 opt-in:不想要別名就省略 `portal`,不生
|
|
|
149
151
|
`defaultAlias`、Portal 面板/capability 或 Web Station alias;沒有 Portal
|
|
150
152
|
不是 inspect/info/doctor 問題。明確啟用 `portal` 時才將其 alias 作為 `defaultAlias`。
|
|
151
153
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
alias;這是服務入口,不是 SDK Application Portal。DSM 會將 standalone URL 改寫成 launchApp 頁面,
|
|
158
|
-
需要 AppInstance/AppWindow JS class;schema 1 web service 沒有該 class,因此使用 url。需要 DSM 視窗時明寫 `dsm-window`。
|
|
159
|
-
|
|
160
|
-
Schema 2 的 `dsm-package.yaml` + `identity_gateway` + `compose` 同樣視為
|
|
161
|
-
web service:省略 `ui.launch` 時預設 `new-page`,使用 standalone launcher,
|
|
162
|
-
不帶 Application Portal。兩條 Compose 路線共用 `contract/app-launch.ts`
|
|
163
|
-
解析開啟方式與選擇 launcher 型別;明確的 `ui.launch` 可覆寫預設。
|
|
164
|
-
只有沒有 web service 的 SDK 原生 App 省略時才預設 `dsm-window`。
|
|
154
|
+
Schema 2 Compose 使用 `dsm-package.yaml` 的 `compose.files`、`compose.web` 與
|
|
155
|
+
`identity_gateway`;不使用 file/targets/address_env 或手寫 upstream。
|
|
156
|
+
省略 `ui.launch` 時預設 `new-page`,使用 SDK standalone launcher,不帶 Application Portal。
|
|
157
|
+
Headless Compose 不產生 launcher。health 必須選實際 endpoint 與 2xx status;
|
|
158
|
+
readiness 不跟隨 redirect,check 不會發 HTTP。Compose 不支援 ctl。
|
|
165
159
|
|
|
166
160
|
For controlling installed Apps or designing task-level AI commands, read [reference/ctl.md](reference/ctl.md). Use ctl host list, register/renew explicitly, discover all commands, and always name the host when agents may operate multiple NAS machines.
|
|
@@ -23,7 +23,7 @@ compose:
|
|
|
23
23
|
network: {egress: isolated}
|
|
24
24
|
# Private package binds are rejected by DSM build; keep compatible upstream named volumes.
|
|
25
25
|
identity_gateway:
|
|
26
|
-
sdk: 0.
|
|
26
|
+
sdk: 0.16.0
|
|
27
27
|
mode: none
|
|
28
28
|
access: dsm-users
|
|
29
29
|
listen: {http: 28000}
|
|
@@ -151,7 +151,7 @@ declared availability readiness, LAN isolation/egress, protected grant lifecycle
|
|
|
151
151
|
ACLs, two-user visibility and clean install/upgrade/reboot/stop/uninstall before
|
|
152
152
|
calling the package supported on a NAS. No NAS is accessed by the build gate.
|
|
153
153
|
|
|
154
|
-
The unified Compose contract uses SDK/templates 0.
|
|
154
|
+
The unified Compose contract uses SDK/templates 0.16.0 with CLI 0.16.0. Use the published, matching versions together; the legacy packaging route and bridge publish mode are removed.
|
|
155
155
|
|
|
156
156
|
Primary override files, and each included override set, must share a directory. Cross-directory override bases are rejected before loader I/O. Included implicit .env files are confined, inventoried and preserved; symlinks are rejected.
|
|
157
157
|
|
|
@@ -187,13 +187,28 @@ bridge-unverified admission blocker is replaced by `COMPOSE_READINESS_INVALID`,
|
|
|
187
187
|
|
|
188
188
|
Compose SPKs intentionally omit `install_dep_packages=ContainerManager`: DSM can
|
|
189
189
|
report that dependency not ready even while its systemd unit and Docker daemon
|
|
190
|
-
are active. The
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
190
|
+
are active. The SDK's package-account preflight runs before install preparation
|
|
191
|
+
and before project startup. It reads fixed Container Manager package/executable
|
|
192
|
+
paths and the kernel's listening Unix-socket inventory; it never opens the Docker
|
|
193
|
+
socket, needs root, starts services or repairs Container Manager. Missing software
|
|
194
|
+
fails with `COMPOSE_CONTAINER_MANAGER_REQUIRED`; installed but unavailable or
|
|
195
|
+
unverifiable Docker fails with `COMPOSE_CONTAINER_MANAGER_NOT_READY`. Stable
|
|
196
|
+
message keys carry English defaults telling the user to install/start Container
|
|
197
|
+
Manager from Package Center. The install hook writes the DSM temporary error log
|
|
198
|
+
when provided; start returns the same error; first-open CGI displays an i18n-ready
|
|
199
|
+
503 page even when the App gateway is stopped. This is availability evidence,
|
|
200
|
+
not Docker/project identity or a successful container API call. DSM wizard display
|
|
201
|
+
and permission behavior still need device acceptance.
|
|
202
|
+
|
|
203
|
+
Exclude NAS platforms which cannot run Container Manager through the package's
|
|
204
|
+
supported `arches`/INFO arch declarations. Do not advertise an unsupported
|
|
205
|
+
architecture merely because the App image has it; review model compatibility
|
|
206
|
+
within the declared family too.
|
|
207
|
+
|
|
208
|
+
For disposable environments, optional `compose.resource_prefix: sandbox-` prefixes
|
|
194
209
|
all generated project/network/volume names while retaining the package ID as the
|
|
195
210
|
namespace suffix. Keep it stable across upgrades: changing it selects new
|
|
196
|
-
resources and does not
|
|
211
|
+
resources and does not move or delete retained data.
|
|
197
212
|
|
|
198
213
|
Bridge web ingress uses a dedicated `sdk_ingress` internal bridge. The web service
|
|
199
214
|
receives a fixed IPv4 address, and the package-account gateway connects directly
|
|
@@ -204,7 +219,7 @@ still adds the existing outbound bridge; other App services retain their interna
|
|
|
204
219
|
|
|
205
220
|
The subnet is a deterministic /29 candidate derived from package ID and
|
|
206
221
|
`compose.network.ingress_candidate` (integer 0-255, default 0). It comes from
|
|
207
|
-
|
|
222
|
+
the upper half of the RFC 1918 10/8 private address block; this pool is not globally reserved. Before acquisition, the
|
|
208
223
|
package-account preparer reads kernel IPv4 routes and all interface prefixes, and rejects overlap with
|
|
209
224
|
`COMPOSE_SUBNET_CONFLICT`. Choose another candidate and rebuild; no silent
|
|
210
225
|
renumbering occurs on upgrade. Choose a different candidate for a clean
|
|
@@ -347,3 +362,5 @@ verified loopback address setting, cannot remap ports or enforce isolated
|
|
|
347
362
|
egress, and is not a general replacement for Header/native-cookie bridge Apps.
|
|
348
363
|
Do not silently switch networking, add Docker socket access, run an App hook as
|
|
349
364
|
root or delete DSM configuration to claim compatibility.
|
|
365
|
+
|
|
366
|
+
Compose web health is required before check/build; init has no guessed endpoint or status. Select both `--health-path` and `--health-status`, or edit `compose.web.health`. Readiness does not follow redirects. Static check is not an HTTP or arbitrary-UID/storage preflight. For official-image upstream packaging and DSM login, read [upstream-fork.md](upstream-fork.md).
|
|
@@ -40,8 +40,8 @@ rootOps 與 delegate 已共用管理員 PasswordConfirm 核准的 protected exec
|
|
|
40
40
|
backend 寫 `runtime: python|node|rust`(`runtime: go` 等同 `kind: go`)。CGI 是 SDK 的 Go runtime(`bridge/cmd/runtime`),它驗證 DSM session、entry 與 `permissions.json` 後,以套件帳號(不是 root)每個請求啟動一次 App。App 放在 ui 以外的目錄,打包成 `target/app/`,build 寫入 `bridge.json`(spec/bridge/v1)並把它與 `permissions.json` 的 sha256 編進 runtime。
|
|
41
41
|
|
|
42
42
|
```yaml
|
|
43
|
-
backend: {runtime: python, dir: app, entry: main.py, sdk: 0.
|
|
44
|
-
backend: {runtime: node, dir: app, entry: main.mjs, sdk: 0.
|
|
43
|
+
backend: {runtime: python, dir: app, entry: main.py, sdk: 0.16.0}
|
|
44
|
+
backend: {runtime: node, dir: app, entry: main.mjs, sdk: 0.16.0, node: {package: Node.js_v20}}
|
|
45
45
|
backend: {runtime: rust, dir: app, entry: hello, methods: [hello.greet]}
|
|
46
46
|
```
|
|
47
47
|
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Integrating an existing open-source App
|
|
2
|
+
|
|
3
|
+
Preserve the upstream fork, full history and `upstream` remote. Keep **every**
|
|
4
|
+
wrapper-owned manifest, adapter, script, notice and document in `synology/`.
|
|
5
|
+
Never edit upstream README, Dockerfile, source, config, workflows or ignore files.
|
|
6
|
+
Do not rewrite the App or treat it as an SDK component. Use `.git/info/exclude`
|
|
7
|
+
for local output when needed.
|
|
8
|
+
|
|
9
|
+
## Container: official image and a matching source tag
|
|
10
|
+
|
|
11
|
+
Use the upstream **official image**, pinned as `repository:tag@sha256:digest`.
|
|
12
|
+
For a multi-architecture image, pin its index digest so Container Manager can
|
|
13
|
+
select the NAS architecture. Container Manager downloads the pinned image at
|
|
14
|
+
install. Retain the upstream source fork to match releases and support required
|
|
15
|
+
source/license delivery, including Corresponding Source where applicable. A
|
|
16
|
+
matching source tag alone does not prove a complete license/source inventory.
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
git clone <your-fork-url> app
|
|
20
|
+
cd app
|
|
21
|
+
git remote add upstream <upstream-url>
|
|
22
|
+
synopkgland init -t compose --upstream-fork --package MyApp \
|
|
23
|
+
--upstream-image owner/app --health-path /actual-readiness --health-status 200 .
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Replace `owner/app` with the verified official repository. `init` does not guess
|
|
27
|
+
its publisher or make network requests. Configure `synology/upstream-recipe.json`:
|
|
28
|
+
`repository`, the actual Compose `service`, and `imageTag: tag` (exact upstream
|
|
29
|
+
tag) or `version` (strip the leading `v` only if the official image uses that
|
|
30
|
+
spelling). Adjust the App's port, storage and DSM login adapter before committing.
|
|
31
|
+
The initial `SELECT_UPSTREAM_TAG` marker must be replaced by a successful upgrade.
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
bash synology/upgrade.sh v1.2.3 2
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The script requires a clean tree, fetches the exact upstream tag into a private
|
|
38
|
+
ref, verifies ancestry and untouched upstream files, then merges. It updates the
|
|
39
|
+
official image tag and calls the existing `synopkgland images pin` registry
|
|
40
|
+
resolver, recording `tag@digest` in `synology/compose.image.yaml`. That wrapper
|
|
41
|
+
Compose override is listed after the base file; upstream files remain unchanged.
|
|
42
|
+
The resolver requests OCI index/manifest-list media types. Verify the official
|
|
43
|
+
image supports every declared architecture; a single-platform manifest does not
|
|
44
|
+
become multi-architecture merely because it has a digest. A failed lookup leaves
|
|
45
|
+
the previous override intact and stops the upgrade.
|
|
46
|
+
|
|
47
|
+
Next, the script derives `version: 1.2.3` and explicit increasing packaging build
|
|
48
|
+
`0002`, records source tag/commit/build in `upstream-source.json`, then runs
|
|
49
|
+
`check --json` and `build --json`. It performs no image compilation or publication.
|
|
50
|
+
Stable `[v]X.Y.Z` only. Build numbers increase globally (1–9999), even when upstream
|
|
51
|
+
versions change. Review/inspect the SPK and commit `synology/` before another
|
|
52
|
+
upgrade. Resolve or revert a failed merge explicitly; rehearse retained-data
|
|
53
|
+
upgrades separately. No Git push, Release, NAS installation or deployment occurs.
|
|
54
|
+
|
|
55
|
+
For fixed-UID image data directories, currently supply a startup adapter or an
|
|
56
|
+
upstream-supported writable path under the control of `synology/`. Verify the
|
|
57
|
+
package UID can create first-run config/cache/data and retain it across upgrades.
|
|
58
|
+
Do not use root/chown lifecycle hooks or substitute a fixed UID. Replace this
|
|
59
|
+
workaround with the framework ownership mechanism once available and verified.
|
|
60
|
+
|
|
61
|
+
## Native: official release binaries or a source build hook
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
synopkgland init -t native-go --ui plain --upstream-fork --package MyApp .
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Native Apps can stage official release binaries pinned by **SHA-256**, or compile
|
|
68
|
+
the merged tree with a source build hook. OpenList uses the official release
|
|
69
|
+
binary route: select the binary for the matching upstream tag and architecture,
|
|
70
|
+
verify its pinned SHA-256 before staging it, and retain the source fork for
|
|
71
|
+
version/source correspondence. Do not execute an unverified download.
|
|
72
|
+
|
|
73
|
+
Implement `synology/build-source.sh` as the native payload hook; it deliberately
|
|
74
|
+
fails until implemented. For official release binaries, use that hook to acquire,
|
|
75
|
+
verify and stage the pinned release payload. For a source build, compile the
|
|
76
|
+
merged source and its frontend with pinned toolchains/dependencies. The hook
|
|
77
|
+
receives `SPL_UPSTREAM_SOURCE`, `SPL_UPSTREAM_TAG`, `SPL_UPSTREAM_COMMIT`; stage
|
|
78
|
+
architecture-specific `daemon.targets` below `synology/` and keep the small SDK
|
|
79
|
+
CGI as a separate management shell. The upgrade command fetches/merges the tag,
|
|
80
|
+
derives the version, runs the selected native payload hook, checks and builds.
|
|
81
|
+
|
|
82
|
+
## Use DSM login
|
|
83
|
+
|
|
84
|
+
Every integrated App must admit **DSM administrators directly**, without a separate
|
|
85
|
+
App password, an OIDC/SSO server, or upstream source changes. The scaffold declares
|
|
86
|
+
`entry: admin-only`; Compose also declares gateway `access: dsm-admins`. This gates
|
|
87
|
+
the entry but does not itself create an upstream session. Choose these supported
|
|
88
|
+
upstream mechanisms in order:
|
|
89
|
+
|
|
90
|
+
1. **Trusted header / reverse-proxy authentication**: configure gateway header
|
|
91
|
+
mode and the upstream's documented proxy-auth setting. FreshRSS supports this:
|
|
92
|
+
use its supported server-side `REMOTE_USER` configuration; the `synology/`
|
|
93
|
+
adapter may map admitted DSM administrators to a fixed shared upstream admin.
|
|
94
|
+
Clear incoming competing identity headers and admit upstream traffic only
|
|
95
|
+
through the gateway's dedicated bridge. Document shared data/audit identity.
|
|
96
|
+
2. **Disable native login / allow guests**: configure the supported option and
|
|
97
|
+
let the DSM administrator gateway protect the sole reachable entry.
|
|
98
|
+
Uptime Kuma's `disableAuth` is the example. First bootstrap must complete native setup,
|
|
99
|
+
enable `disableAuth` via the upstream UI/API and read the state back before
|
|
100
|
+
declaring ready. An app-specific `synology/` adapter should generate any
|
|
101
|
+
required one-time setup secret, use it only for bootstrap and clear it; do not
|
|
102
|
+
ask users to remember/type an additional App password or edit the DB. The
|
|
103
|
+
existing prototype's setup rehearsal is not a generic shipped bootstrap SDK.
|
|
104
|
+
If manual App login is still necessary, label bootstrap incomplete. Verify
|
|
105
|
+
subsequent DSM entry, restart and upgrade all retain password-free access,
|
|
106
|
+
while anonymous/nonadmin requests and direct upstream ports remain blocked.
|
|
107
|
+
3. **Existing SDK token adapter**: SDK **0.16.0** supports `mode: token`, with
|
|
108
|
+
`access: dsm-admins` and `entry: admin-only`. OpenList uses its unmodified
|
|
109
|
+
upstream `admin token` CLI while the server is stopped to obtain the native
|
|
110
|
+
administrator API token. The package stores it in a package-private **0600**
|
|
111
|
+
file under a 0700 directory. After DSM administrator admission, the gateway
|
|
112
|
+
runs the package's token adapter and injects `Authorization` server-side,
|
|
113
|
+
replacing client credentials. Packaging never delivers the token to the
|
|
114
|
+
browser, URL, localStorage or page script. No new SDK authentication API,
|
|
115
|
+
OIDC/SSO server, upstream source change or direct database edit is required.
|
|
116
|
+
All admitted DSM administrators share one upstream admin account; this does
|
|
117
|
+
not provide per-user isolation. After OpenList **Reset Token**, restart the
|
|
118
|
+
package to refresh the cached credential; failure refuses startup/requests
|
|
119
|
+
rather than falling back to anonymous access. WebDAV retains its own native
|
|
120
|
+
credentials: API-token injection does not make WebDAV password-free. The
|
|
121
|
+
upstream admin's own token/settings APIs remain unchanged, so an admitted
|
|
122
|
+
administrator may still inspect or rotate the native token through them.
|
|
123
|
+
Recheck `admin token`, API header and frontend `/api/me` behavior on upgrades;
|
|
124
|
+
verify actual DSM admission/revocation separately. The OpenList authentication
|
|
125
|
+
adapter is implemented and tested with the existing SDK; generic provisioning
|
|
126
|
+
automation and complete device acceptance are separate work.
|
|
127
|
+
|
|
128
|
+
## Health, prerequisites and prototype boundaries
|
|
129
|
+
|
|
130
|
+
Choose the actual unauthenticated health endpoint and its expected 2xx status
|
|
131
|
+
with both init flags, or fill `compose.web.health` before check/build. No guessed
|
|
132
|
+
`/` + 200. Readiness **does not follow redirects**: a 302 to a dashboard/login is
|
|
133
|
+
unhealthy even if its destination is 200. A favicon/robots probe proves HTTP
|
|
134
|
+
availability only; verify database setup and full readiness separately. `check`
|
|
135
|
+
is static, not an HTTP or UID/storage probe. Compose ctl is rejected at check;
|
|
136
|
+
dedicated origin `/` is supported, automatic subpath rewriting is not.
|
|
137
|
+
|
|
138
|
+
Public npm availability does not grant anonymous Go SDK access. Obtain authorised
|
|
139
|
+
source access or an exact matching module cache with original checksums, or use
|
|
140
|
+
`SYNOPKGLAND_SDK_DIR` with an authorised matching checkout. Go source distribution
|
|
141
|
+
is undecided. Never copy credentials into generated projects.
|
|
142
|
+
|
|
143
|
+
| Prototype | Reusable lesson and remaining boundary |
|
|
144
|
+
| --- | --- |
|
|
145
|
+
| OpenList | Official release binary pinned by SHA-256, `/ping` and SDK 0.16.0 server-side token adapter; shared upstream admin, restart after token reset, native WebDAV credentials. Share selection does not grant package ACL/register Local storage. |
|
|
146
|
+
| FreshRSS | Official image, package-UID Apache/config adapter in `synology/`, explicit health; supported proxy authentication with fixed admin mapping and header clearing. |
|
|
147
|
+
| Uptime Kuma | Official image, writable-path adapter, nonredirecting health and `disableAuth` after first bootstrap; WebSocket expiry/revocation need separate tests. |
|
|
148
|
+
|
|
149
|
+
Historical bounded Linux/prototype proofs are not DSM acceptance. Recheck the
|
|
150
|
+
selected official image, arbitrary UID, persistent data, bootstrap and login
|
|
151
|
+
boundaries; preserve complete third-party notices and source correspondence.
|
|
152
|
+
|
|
153
|
+
## Container Manager prerequisite
|
|
154
|
+
|
|
155
|
+
Container SPKs omit `install_dep_packages=ContainerManager` because DSM dependency
|
|
156
|
+
status can report not-ready while Docker is active. SDK preflight checks fixed
|
|
157
|
+
Container Manager files plus a listening Docker socket in the kernel inventory,
|
|
158
|
+
as the package UID without socket/API access or root. It fails before install
|
|
159
|
+
preparation/project startup with stable translated-message keys and an English
|
|
160
|
+
instruction to install/start Container Manager from Package Center. DSM's supplied
|
|
161
|
+
installer log, start failure and first-open CGI carry that diagnosis; the CGI
|
|
162
|
+
returns 503 even when the gateway is down. Availability is not identity attestation.
|
|
163
|
+
Declare only Container Manager-capable `arches`/INFO platforms; exclude unsupported
|
|
164
|
+
NAS architectures and review models in each family. Actual missing-package wizard
|
|
165
|
+
behavior and DSM's dependency not-ready behavior remain device acceptance items.
|
package/skills/reference/wrap.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
只使用 schema-2 `dsm-package.yaml`。先讀 [compose.md](compose.md) 的宣告、支援表及 DSM 阻擋條件,再讀 [identity-gateway.md](identity-gateway.md) 決定 none/header/token/OIDC。不架 SSO Server,也不需要假 backend 或 wrapper container。
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
|
-
synopkgland init -t compose --from ./compose.yaml my-app
|
|
6
|
+
synopkgland init -t compose --from ./compose.yaml --health-path /actual-readiness --health-status 200 my-app
|
|
7
7
|
synopkgland check --json
|
|
8
8
|
synopkgland build --arch x86_64
|
|
9
9
|
synopkgland inspect dist/MyApp-x86_64.spk --json
|
|
@@ -14,3 +14,5 @@ synopkgland inspect dist/MyApp-x86_64.spk --json
|
|
|
14
14
|
容器使用固定套件身分,不繼承登入者 File Station ACL。Shared Folder 需 composeMounts 明確 opt-in,安裝後由 admin 確認;官方 protected ACL/project control 未驗證前只啟動 SDK 設定頁,不 acquire 上游,不用 host root 繞過。每使用者檔案權限需 session/Broker Adapter,本次不做。
|
|
15
15
|
|
|
16
16
|
交付 inspect 的 upstream/effective hashes、image digests、run-as、mount、egress 與 acceptance blockers。Linux 通過不代表 DSM 已驗收。部署請讀 [deploy.md](deploy.md),保留資料並由專用 thread 驗 final SPK digest。
|
|
17
|
+
|
|
18
|
+
For an upstream fork, use [the upstream-fork recipe](upstream-fork.md) and `--upstream-fork` to keep all wrapper files in `synology/`. Container forks require `--upstream-image <official-repository>` and use official tag+digest images; native forks stage official release binaries pinned by SHA-256 or use a source build hook. Health flags require an actual App endpoint and expected 2xx response; readiness does not follow redirects.
|