@synopackageland/cli 0.15.4 → 0.17.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 +8 -1
- 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/add.js +3 -3
- package/dist/build-spk/uninstall-wizard.d.ts +1 -1
- package/dist/build-spk/uninstall-wizard.js +1 -1
- package/dist/build-spk/wizard-messages.js +9 -9
- package/dist/cli.js +16 -2
- package/dist/contract/validate.js +2 -2
- package/dist/e2e/dsm.d.ts +2 -2
- package/dist/e2e/dsm.js +3 -3
- package/dist/error-registry.js +8 -4
- 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/portal/alias.d.ts +2 -6
- package/dist/portal/alias.js +2 -6
- package/dist/portal/info.d.ts +4 -0
- package/dist/portal/info.js +32 -0
- package/dist/runtime/dsm-version.d.ts +1 -1
- package/dist/runtime/dsm-version.js +1 -1
- package/dist/runtime/executor-digests.json +4 -26
- package/dist/runtime/sdk-batch.d.ts +2 -0
- package/dist/runtime/sdk-batch.js +23 -0
- package/dist/runtime/uninstall-cleanup.js +1 -1
- package/dist/skill.js +1 -0
- package/dist/standalone/build.js +8 -1
- package/dist/standalone/compose-errors.js +15 -0
- package/dist/standalone/compose.d.ts +8 -0
- package/dist/standalone/compose.js +13 -3
- package/dist/standalone/dsm-window.js +25 -4
- package/dist/standalone/identity-gateway.js +1 -0
- package/dist/standalone/inspect-spk.js +17 -3
- package/dist/standalone/lifecycle.js +4 -1
- package/dist/standalone/manifest.js +7 -2
- package/dist/standalone/native-layout.js +6 -2
- package/dist/standalone/portal-window.js +1 -1
- package/dist/standalone/spk.d.ts +3 -0
- package/dist/standalone/spk.js +9 -2
- package/package.json +3 -3
- package/skills/SKILL.md +8 -14
- package/skills/reference/compose.md +25 -8
- package/skills/reference/create.md +4 -4
- package/skills/reference/portal.md +10 -6
- package/skills/reference/ui-vue.md +12 -0
- package/skills/reference/upstream-fork.md +165 -0
- package/skills/reference/wrap.md +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.17.0 (2026-10-19)
|
|
4
|
+
|
|
5
|
+
- Pin SDK/App 0.17.0 and templates 0.17.1, with UI 0.12.1 in Vue scaffolds; package the executor fixes for directory users and request batches.
|
|
6
|
+
- Fetch SynoToken early in standalone and Portal windows so the first API request can start sooner.
|
|
7
|
+
- Allow Portal without an executor from DSM 7.2-64570. Request DSM nginx reload through INFO during install, upgrade and uninstall; DSM 7.1 still needs an executor for registration.
|
|
8
|
+
|
|
9
|
+
- Expose ctx.batch in generated SDK windows, sharing the existing request guards and token recovery. Keep single-call retries unchanged; never replay a batch after partial execution.
|
|
10
|
+
|
|
11
|
+
## 0.16.0 (2026-10-19)
|
|
12
|
+
|
|
13
|
+
- Pin SDK/templates 0.16.0; generated Shared Folder flows use the shared SDK readback and settings components.
|
|
14
|
+
- Add the upstream-fork recipe/init/check workflow, Container Manager preflight and architecture-matched storage initializer packaging while preserving official images.
|
|
15
|
+
- Remove internal deployment details from published content and scan actual npm tarballs before release.
|
|
16
|
+
|
|
3
17
|
## 0.15.4 (2026-10-18)
|
|
4
18
|
|
|
5
19
|
- Fix Compose import and undeclared SDK defaults after CLI-only patch releases. Resolve the SDK from the exactly pinned templates' Go module instead of assuming the CLI and SDK share a patch version; daemon-control defaults use the same source.
|
|
@@ -174,8 +188,8 @@ only its protocol README's wording, and nothing in the CLI depends on it.
|
|
|
174
188
|
|
|
175
189
|
- `ctx.call` rejects `APP_STOPPED` at once: a stopped package waits for the
|
|
176
190
|
user to start it in Package Center, so the SDK's stopped copy shows within a
|
|
177
|
-
page's next read instead of 30 s later (Download Hub 29.3 s
|
|
178
|
-
showed its old content or loading
|
|
191
|
+
page's next read instead of 30 s later (Download Hub 29.3 s; Reclaim
|
|
192
|
+
showed its old content or loading). `APP_STARTING` and
|
|
179
193
|
`EXECUTOR_STARTING` repeat for 4 s of waiting (0.5, 1, 2, 0.5 s), so a short
|
|
180
194
|
start still shows the page nothing and the SDK's starting copy follows a
|
|
181
195
|
longer one within its own 4 s recheck. `executor.control`, which the window's
|
|
@@ -225,7 +239,7 @@ e2e 新增 `openUntilLoaded`(`signIn` 遇 `net::ERR_ABORTED` 最多重開約 3
|
|
|
225
239
|
the newest one it read (`lastStatus()`); a `status()` asked for while a read
|
|
226
240
|
runs shares it. The SDK's `PortalSettings` starts from it, so a settings
|
|
227
241
|
page no longer moves when its placeholder rows become the real section
|
|
228
|
-
(Reclaim: 100 px
|
|
242
|
+
(Reclaim: 100 px).
|
|
229
243
|
|
|
230
244
|
## 0.10.6 (2026-10-15)
|
|
231
245
|
|
|
@@ -244,7 +258,7 @@ digest 與 0.10.5 相同;SDK 下限維持 0.10.5。e2e 新增 `openPath`、`ha
|
|
|
244
258
|
|
|
245
259
|
- `signIn`, `openDesktop` and `openPortal` open their page once more when
|
|
246
260
|
the first navigation fails with `net::ERR_ABORTED`, which about one run in
|
|
247
|
-
ten met as DSM's desktop navigated on its own
|
|
261
|
+
ten met as DSM's desktop navigated on its own; any other
|
|
248
262
|
failure, or a second abort, still fails. The helper is exported as
|
|
249
263
|
`openPath(app, path)`.
|
|
250
264
|
- The DSM window's `status()` answers a pinned executor that reports
|
|
@@ -330,7 +344,7 @@ digest 與 0.10.4 相同;新增 `@synopackageland/cli/e2e`。
|
|
|
330
344
|
`authorization.status`, `executor.status`) while the package is stopped or
|
|
331
345
|
starting: they reject with the code at once, and SDK 0.10.5's panels show the
|
|
332
346
|
stopped or starting package and check again on their own. A page no longer
|
|
333
|
-
shows nothing for the first seconds after an upgrade (6.7 s in
|
|
347
|
+
shows nothing for the first seconds after an upgrade (6.7 s in the regression test). Calls
|
|
334
348
|
that change something, approvals and controls among them, still repeat for up
|
|
335
349
|
to 30 s. `ctx.executor.status()` for a user who is not an administrator says
|
|
336
350
|
the package is stopped or starting instead of `admin_required`.
|
|
@@ -362,12 +376,12 @@ before its timeout.
|
|
|
362
376
|
- The window approval of an upgrade (outdated tasks) makes 11 scheduler calls
|
|
363
377
|
instead of 12: the outdated boot task is deleted by the approval's own first
|
|
364
378
|
EventScheduler get, made moments before the password, instead of a second
|
|
365
|
-
get (2.9 s on the test NAS in
|
|
379
|
+
get (2.9 s on the test NAS in the regression test). The control task is still read again by id
|
|
366
380
|
before its delete (spec/scheduler/v1). A fresh approval stays at 8.
|
|
367
381
|
繁中:升級核准的排程呼叫由 12 降為 11(boot 任務不再於刪除前重讀)。
|
|
368
382
|
- A scope approval reads the executor's selection back every second at most
|
|
369
383
|
(it backed off to 2 s), so the window sees the landed scope within a second
|
|
370
|
-
of `scope.confirm` (the Dialog closed 4.8 s after it in
|
|
384
|
+
of `scope.confirm` (the Dialog closed 4.8 s after it in the regression test). Removing
|
|
371
385
|
the temporary selection task still lists Task Scheduler first, as
|
|
372
386
|
spec/scheduler/v1 requires of a delete.
|
|
373
387
|
繁中:範圍核准讀回間隔上限 2 s → 1 s,confirmed 後 1 s 內看到結果。
|
|
@@ -376,7 +390,7 @@ before its timeout.
|
|
|
376
390
|
before any request: the pinned, running executor reads `ready`; anything
|
|
377
391
|
else stays `blocked` with `admin_required`. Task Scheduler is still read only
|
|
378
392
|
for administrators. A user no longer sees an approved executor as waiting
|
|
379
|
-
for approval
|
|
393
|
+
for approval.
|
|
380
394
|
繁中:非管理員的 `ctx.executor.status()` 會向 App 的 `executor.status` 詢問,已核准且
|
|
381
395
|
執行中即為 `ready`,不再一律回 `admin_required`。
|
|
382
396
|
|
package/README.md
CHANGED
|
@@ -95,7 +95,7 @@ Add them with `init --with …` or later with `synopkgland add <capability…>`.
|
|
|
95
95
|
| `login` | Sign users in with their DSM account, without the DSM desktop | Nobody; requires HTTPS for password sign-in |
|
|
96
96
|
| `delegate` | Move files into a user's chosen folder while the user is offline | An administrator, once |
|
|
97
97
|
| `root-ops` | Run fixed `root/*.sh` scripts for administrators | An administrator, once |
|
|
98
|
-
| `portal` | Open the app from its own Application Portal alias, with a login page style (`login_style` changes the colour, wallpaper and title) | An administrator
|
|
98
|
+
| `portal` | Open the app from its own Application Portal alias, with a login page style (`login_style` changes the colour, wallpaper and title) | An administrator applies the alias; DSM 7.2-64570+ needs no executor (INFO asks DSM to reload nginx on uninstall). DSM 7.1-42661+ needs an executor to register the entry |
|
|
99
99
|
|
|
100
100
|
Delegate is implemented; its default operation is `files.moveFromStaging`.
|
|
101
101
|
Device lifecycle acceptance on real hardware is still pending for delegate and root operations.
|
|
@@ -311,3 +311,10 @@ Read `synopkgland skill show ctl` for the AI workflow, batch discovery approval,
|
|
|
311
311
|
cache refresh, host removal/revocation and explicit `ctl.commands` authoring.
|
|
312
312
|
API descriptions are private SPK files named `synopkgland-api.json`, served
|
|
313
313
|
only by the authenticated fixed `synopkgland.describe` CGI operation.
|
|
314
|
+
|
|
315
|
+
Generated SDK windows expose `ctx.batch([{method, params}, ...])` when the
|
|
316
|
+
selected SDK supplies its batch factory. One to eight independent JSON reads
|
|
317
|
+
share a request; results keep input order and retain per-method errors. A batch
|
|
318
|
+
uses the same token recovery, request deadline and cancellation as `ctx.call`,
|
|
319
|
+
and is never replayed automatically. Older SDKs and binary backends retain the
|
|
320
|
+
single-call client. Rebuild the window with the current CLI/SDK to adopt it.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Compose App development
|
|
2
|
+
|
|
3
|
+
Run `synopkgland skill list`, then `synopkgland skill show orchestrator`,
|
|
4
|
+
`synopkgland skill show compose` and `synopkgland skill show upstream-fork`.
|
|
5
|
+
Prefer `check --json`, `build --json`, `inspect <spk> --json` for reviewable results.
|
|
6
|
+
|
|
7
|
+
Never write `AppPortal.json` or `@appconf` directly, or relay QuickConnect.
|
|
8
|
+
Compose has no native Application Portal alias; use its supported web entry.
|
|
9
|
+
|
|
10
|
+
Use dsm-package.yaml schema 2. Keep upstream Compose bytes unchanged; declare DSM
|
|
11
|
+
policy in compose.files and compose.web. Never use file/targets/address_env or
|
|
12
|
+
identity_gateway.upstream: the SDK planner derives ingress.
|
|
13
|
+
|
|
14
|
+
## Compose health and launch
|
|
15
|
+
|
|
16
|
+
Select compose.web.health.path and its actual expected 2xx status before
|
|
17
|
+
check/build. Redirects are not followed; a 302 to login is unhealthy. check is
|
|
18
|
+
static, not an HTTP or arbitrary-UID/storage probe. Test the selected official image as
|
|
19
|
+
the package UID, including writable volumes, first setup and upgrade.
|
|
20
|
+
Omitted ui.launch means new-page via the SDK standalone launcher, without
|
|
21
|
+
Application Portal. Headless Apps omit compose.web and have no launcher.
|
|
22
|
+
Compose ctl is unsupported.
|
|
23
|
+
|
|
24
|
+
Use supported upstream config files, environment, argv or reverse proxy for
|
|
25
|
+
login/settings. For integrated upstream Apps, select supported trusted-header authentication or disable upstream login behind the DSM admin gate; or use the existing SDK 0.17.0 token adapter: OpenList uses mode: token, access: dsm-admins and entry: admin-only. Its unmodified admin token CLI supplies a package-private 0600 credential; the gateway injects Authorization server-side after DSM admission, without delivering it to the browser. All DSM admins share one upstream admin; token reset requires package restart; WebDAV keeps its own credentials. Verify each App adapter and DSM admission separately.
|
|
26
|
+
Containers use the package account and a dedicated bridge; they do not inherit
|
|
27
|
+
an interactive user's NAS ACL. Do not use root/chown during any App lifecycle
|
|
28
|
+
phase. Preserve licenses and dependency/source provenance. Read the CLI skill
|
|
29
|
+
reference/compose.md and reference/upstream-fork.md for the full recipe.
|
|
30
|
+
|
|
31
|
+
Public npm access does not grant anonymous Go SDK access. Obtain authorised
|
|
32
|
+
source/cache access or a matching SYNOPKGLAND_SDK_DIR checkout first. Never copy
|
|
33
|
+
credentials into the App. Do not install, publish or modify NAS data without task authorization.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
# Native App payload hook. SPL_UPSTREAM_SOURCE is the merged fork root.
|
|
4
|
+
# Stage official release binaries only after verifying pinned SHA-256 values
|
|
5
|
+
# (the OpenList route), or compile the merged source with a source build hook.
|
|
6
|
+
# Stage daemon.targets under synology/; pin toolchains/dependencies for builds.
|
|
7
|
+
# Keep upstream source unchanged.
|
|
8
|
+
# Every App lifecycle phase uses the package UID. Do not add root/chown hooks.
|
|
9
|
+
echo 'SOURCE_BUILD_REQUIRED: implement the native synology/build-source.sh.' >&2
|
|
10
|
+
exit 2
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Use the CLI's existing OCI registry resolver; never build or publish an image.
|
|
3
|
+
import {mkdtempSync, readFileSync, writeFileSync, rmSync} from 'node:fs';
|
|
4
|
+
import {tmpdir} from 'node:os';
|
|
5
|
+
import {join} from 'node:path';
|
|
6
|
+
import {spawnSync} from 'node:child_process';
|
|
7
|
+
const [tag] = process.argv.slice(2);
|
|
8
|
+
const match = /^v?((?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*))$/.exec(tag ?? '');
|
|
9
|
+
if (!match) throw new Error('Use the selected stable upstream tag.');
|
|
10
|
+
const config = JSON.parse(readFileSync(new URL('./upstream-recipe.json', import.meta.url), 'utf8'));
|
|
11
|
+
if (config.kind !== 'official-image' || !['tag', 'version'].includes(config.imageTag)) throw new Error('Invalid official image recipe.');
|
|
12
|
+
const imageTag = config.imageTag === 'version' ? match[1] : tag;
|
|
13
|
+
const reference = `${config.repository}:${imageTag}`;
|
|
14
|
+
const temp = mkdtempSync(join(tmpdir(), 'synopkgland-image-pin-'));
|
|
15
|
+
try {
|
|
16
|
+
// images pin edits only compose.yaml; use a bounded temporary project so the
|
|
17
|
+
// App's base Compose and upstream source remain unchanged on lookup failure.
|
|
18
|
+
writeFileSync(join(temp, 'dsm-package.yaml'), 'schema: 2\n');
|
|
19
|
+
writeFileSync(join(temp, 'compose.yaml'), JSON.stringify({services: {[config.service]: {image: reference}}}, null, 2) + '\n');
|
|
20
|
+
const result = spawnSync('synopkgland', ['--project', temp, 'images', 'pin'], {stdio: 'inherit'});
|
|
21
|
+
if (result.error || result.status !== 0) throw new Error('Official image digest lookup failed; no Compose override was changed.');
|
|
22
|
+
const pinned = JSON.parse(readFileSync(join(temp, 'compose.yaml'), 'utf8'));
|
|
23
|
+
const image = pinned.services[config.service].image;
|
|
24
|
+
if (!image.startsWith(`${reference}@sha256:`) || !/@sha256:[0-9a-f]{64}$/.test(image)) throw new Error('Registry lookup did not produce the selected tag plus digest.');
|
|
25
|
+
writeFileSync(new URL('./compose.image.yaml', import.meta.url), JSON.stringify(pinned, null, 2) + '\n');
|
|
26
|
+
} finally {rmSync(temp, {recursive: true, force: true});}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# One explicit stable tag; no latest guessing, push, image publication or NAS access.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
packaging=$(cd -- "$(dirname -- "$0")" && pwd)
|
|
5
|
+
repo=$(cd -- "$packaging/.." && pwd)
|
|
6
|
+
cd "$repo"
|
|
7
|
+
[[ $# == 2 && $1 =~ ^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] || {
|
|
8
|
+
echo 'Usage: bash synology/upgrade.sh <[v]X.Y.Z> <increasing-build-number>' >&2; exit 2;
|
|
9
|
+
}
|
|
10
|
+
[[ $(git rev-parse --show-toplevel) == "$repo" ]] || { echo 'Run inside a source fork.' >&2; exit 2; }
|
|
11
|
+
[[ -z $(git status --porcelain --untracked-files=normal) ]] || { echo 'Commit changes before upgrading.' >&2; exit 2; }
|
|
12
|
+
git remote get-url upstream >/dev/null
|
|
13
|
+
node "$packaging/version.mjs" "$1" "$2" --validate
|
|
14
|
+
# Fetch the named remote tag into a private ref: a local tag can never substitute for it.
|
|
15
|
+
git fetch --no-tags upstream "+refs/tags/$1:refs/synopkgland/selected-upstream"
|
|
16
|
+
commit=$(git rev-parse 'refs/synopkgland/selected-upstream^{commit}')
|
|
17
|
+
[[ -z $(git ls-tree "$commit" -- synology) ]] || { echo 'Upstream owns synology/; choose another integration layout.' >&2; exit 2; }
|
|
18
|
+
previous=$(git describe --tags --abbrev=0 --match '[0-9]*' --match 'v[0-9]*' HEAD 2>/dev/null || true)
|
|
19
|
+
if [[ -f "$packaging/upstream-source.json" ]]; then
|
|
20
|
+
previous=$(node -e 'process.stdout.write(JSON.parse(require("fs").readFileSync(process.argv[1])).commit)' "$packaging/upstream-source.json")
|
|
21
|
+
fi
|
|
22
|
+
if [[ -n $previous ]]; then
|
|
23
|
+
git merge-base --is-ancestor "$previous" "$commit" || { echo 'Refusing upstream downgrade/divergent history.' >&2; exit 2; }
|
|
24
|
+
[[ -z $(git diff "$previous" HEAD -- . ':(exclude)synology') ]] || { echo 'Wrapper changed upstream files outside synology/.' >&2; exit 2; }
|
|
25
|
+
else
|
|
26
|
+
# First adoption must already be based on the selected source tag.
|
|
27
|
+
git merge-base --is-ancestor "$commit" HEAD
|
|
28
|
+
[[ -z $(git diff "$commit" HEAD -- . ':(exclude)synology') ]] || { echo 'Initial fork must preserve upstream source.' >&2; exit 2; }
|
|
29
|
+
fi
|
|
30
|
+
git merge --no-ff --no-edit "$commit"
|
|
31
|
+
[[ -z $(git diff "$commit" HEAD -- . ':(exclude)synology') ]] || { echo 'Upstream files differ after merge; resolve before building.' >&2; exit 2; }
|
|
32
|
+
kind=$(node -e 'process.stdout.write(JSON.parse(require("fs").readFileSync(process.argv[1])).kind)' "$packaging/upstream-recipe.json")
|
|
33
|
+
if [[ $kind == official-image ]]; then
|
|
34
|
+
node "$packaging/update-image.mjs" "$1"
|
|
35
|
+
elif [[ $kind != native-source ]]; then
|
|
36
|
+
echo 'Unknown upstream recipe kind.' >&2; exit 2
|
|
37
|
+
fi
|
|
38
|
+
node "$packaging/version.mjs" "$1" "$2"
|
|
39
|
+
node -e 'const fs=require("fs");fs.writeFileSync(process.argv[1],JSON.stringify({tag:process.argv[2],commit:process.argv[3],build:Number(process.argv[4])},null,2)+"\n")' "$packaging/upstream-source.json" "$1" "$commit" "$2"
|
|
40
|
+
export SPL_UPSTREAM_SOURCE="$repo"
|
|
41
|
+
export SPL_UPSTREAM_TAG="$1"
|
|
42
|
+
export SPL_UPSTREAM_COMMIT="$commit"
|
|
43
|
+
cd "$packaging"
|
|
44
|
+
if [[ $kind == native-source ]]; then bash ./build-source.sh; fi
|
|
45
|
+
synopkgland check --json
|
|
46
|
+
synopkgland build --json
|
|
47
|
+
# Review version/provenance and commit synology/ before the next upgrade.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Stable tag -> DSM version. Keep all wrapper-owned changes under synology/.
|
|
3
|
+
import {readFileSync, writeFileSync} from 'node:fs';
|
|
4
|
+
const [tag, build, mode] = process.argv.slice(2);
|
|
5
|
+
const match = /^v?((?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*))$/.exec(tag ?? '');
|
|
6
|
+
const file = new URL('./dsm-package.yaml', import.meta.url);
|
|
7
|
+
const source = readFileSync(file, 'utf8');
|
|
8
|
+
const current = /^build: (\d+)\s*$/m.exec(source);
|
|
9
|
+
if (!match || !/^[1-9][0-9]{0,3}$/.test(build ?? '') || !current || Number(build) <= Number(current[1]))
|
|
10
|
+
throw new Error('Use a stable [v]X.Y.Z tag and an increasing packaging build (1–9999).');
|
|
11
|
+
if (!/^version: .+$/m.test(source)) throw new Error('Missing scaffold version.');
|
|
12
|
+
if (mode !== '--validate') writeFileSync(file, source.replace(/^version: .+$/m, `version: ${match[1]}`).replace(/^build: \d+\s*$/m, `build: ${Number(build)}`));
|
package/dist/add.js
CHANGED
|
@@ -8,7 +8,7 @@ import { initFromTemplate, nativeCapabilitySettings } from './init.js';
|
|
|
8
8
|
import { templatesRoot } from './template-paths.js';
|
|
9
9
|
import { borrowedFiles, borrowedTarget, capabilityOverlays } from './capability-overlays.js';
|
|
10
10
|
import { API_PERMISSIONS_FILE } from './standalone/api-permissions.js';
|
|
11
|
-
import { DEFAULT_MIN_DSM, isDsmAtLeast, PORTAL_EXECUTOR_MIN_DSM } from './build-spk/dsm-version.js';
|
|
11
|
+
import { DEFAULT_MIN_DSM, isDsmAtLeast, portalByExecutor, PORTAL_EXECUTOR_MIN_DSM } from './build-spk/dsm-version.js';
|
|
12
12
|
import { loadStandaloneManifest } from './standalone/manifest.js';
|
|
13
13
|
import { PORTAL_REQUIRES_EXECUTOR } from './portal/alias.js';
|
|
14
14
|
function files(root, dir = root) {
|
|
@@ -66,13 +66,13 @@ function addPortal(root, result) {
|
|
|
66
66
|
writeFileSync(manifestPath, setTopLevel(readFileSync(manifestPath, 'utf8'), { ...values, portal: {} }));
|
|
67
67
|
return withNotes({ ...result, added: [...result.added, 'portal'] }, notes);
|
|
68
68
|
}
|
|
69
|
-
/**
|
|
69
|
+
/** Before DSM 7.2 a portal needs an executor, already declared or added in the same call. */
|
|
70
70
|
function refuseNoPrivilegePortal(root, rest) {
|
|
71
71
|
const loaded = loadStandaloneManifest(root);
|
|
72
72
|
if (!loaded.ok)
|
|
73
73
|
throw new Error(loaded.errors.map(e => `[${e.code}] ${e.message}`).join('\n'));
|
|
74
74
|
const { rootOps, delegate, webapi } = loaded.manifest.capabilities;
|
|
75
|
-
if (!rootOps && !delegate && !webapi && !rest.some(name => name === 'root-ops' || name === 'delegate')) {
|
|
75
|
+
if (portalByExecutor(loaded.manifest.os_min_ver) && !rootOps && !delegate && !webapi && !rest.some(name => name === 'root-ops' || name === 'delegate')) {
|
|
76
76
|
throw new Error(`[STANDALONE_PORTAL_REQUIRES_EXECUTOR] ${PORTAL_REQUIRES_EXECUTOR}`);
|
|
77
77
|
}
|
|
78
78
|
}
|
|
@@ -7,7 +7,7 @@ export type UninstallWizardKind = "native" | "container";
|
|
|
7
7
|
interface UninstallWizardOptions {
|
|
8
8
|
/** The App runs the root executor (rootOps or delegate), which purges DSM's leftovers on uninstall. */
|
|
9
9
|
executor?: boolean;
|
|
10
|
-
/** The App declares a Login Portal alias (web-config app-portal);
|
|
10
|
+
/** The App declares a Login Portal alias (web-config app-portal); DSM reloads nginx through INFO. */
|
|
11
11
|
portal?: boolean;
|
|
12
12
|
/** Below DSM 7.2 the executor registered the App's Application Portal entry. */
|
|
13
13
|
portalEntry?: boolean;
|
|
@@ -26,7 +26,7 @@ function renderUninstallWizard(t, locale, packageName, kind, { executor = false,
|
|
|
26
26
|
...(kind === "native" ? [{ desc: t("uninstall.retained.native", vars) }] : []),
|
|
27
27
|
...(executor ? [{ desc: t("uninstall.executor", vars) }] : []),
|
|
28
28
|
{ desc: t(executor ? "uninstall.appPrivilege.auto" : "uninstall.appPrivilege", vars) },
|
|
29
|
-
...(
|
|
29
|
+
...(portal ? [{ desc: t("uninstall.portal.auto", vars) }] : []),
|
|
30
30
|
...(executor && portalEntry ? [
|
|
31
31
|
{ desc: t("uninstall.portalEntry", vars) },
|
|
32
32
|
{ desc: portalEntryCleanupCommands(packageName).map((line) => `<code>${html(line)}</code>`).join("<br>") },
|
|
@@ -407,15 +407,15 @@ export const WIZARD_MESSAGES = {
|
|
|
407
407
|
it: "La disinstallazione rimuove automaticamente le regole di {package} in Pannello di controllo > Privilegi applicazione, così una successiva installazione con lo stesso nome non le applica di nuovo. Solo se questa pulizia non riesce, come segnala la voce synopkgland-executor del registro di sistema, un amministratore deve rimuovere le regole da lì.",
|
|
408
408
|
},
|
|
409
409
|
"uninstall.portal.auto": {
|
|
410
|
-
en: "
|
|
411
|
-
"zh-TW": "DSM
|
|
412
|
-
"zh-CN": "DSM
|
|
413
|
-
ja: "DSM
|
|
414
|
-
ko: "DSM
|
|
415
|
-
de: "
|
|
416
|
-
fr: "
|
|
417
|
-
es: "
|
|
418
|
-
it: "
|
|
410
|
+
en: "DSM removes the Login Portal alias of {package} and reloads the web server automatically. The reload can finish after uninstall. If the alias still opens a DSM page, an administrator can save an application alias in Control Panel > Login Portal > Applications to reload the web server.",
|
|
411
|
+
"zh-TW": "DSM 會移除 {package} 的登入入口別名並自動重新載入網頁伺服器;重新載入可能在解除安裝結束後才完成。如果別名仍開啟 DSM 頁面,管理員可在「控制台」>「登入入口」>「應用程式」儲存任一應用程式別名,以重新載入網頁伺服器。",
|
|
412
|
+
"zh-CN": "DSM 会删除 {package} 的登录门户别名并自动重新加载网页服务器;重新加载可能在卸载结束后才完成。如果别名仍打开 DSM 页面,管理员可在“控制面板”>“登录门户”>“应用程序”保存任一应用程序别名,以重新加载网页服务器。",
|
|
413
|
+
ja: "DSM は {package} のログイン ポータルのエイリアスを削除し、Web サーバーを自動的に再読み込みします。再読み込みはアンインストール完了後に終了する場合があります。エイリアスで DSM ページが開く場合、管理者は [コントロール パネル] > [ログイン ポータル] > [アプリケーション] でアプリケーションのエイリアスを保存して再読み込みできます。",
|
|
414
|
+
ko: "DSM은 {package}의 로그인 포털 별칭을 삭제하고 웹 서버를 자동으로 다시 로드합니다. 다시 로드는 제거 완료 후에 끝날 수 있습니다. 별칭이 여전히 DSM 페이지를 열면 관리자는 제어판 > 로그인 포털 > 응용 프로그램에서 응용 프로그램 별칭을 저장하여 웹 서버를 다시 로드할 수 있습니다.",
|
|
415
|
+
de: "DSM entfernt den Anmeldeportal-Alias von {package} und lädt den Webserver automatisch neu. Das Neuladen kann nach der Deinstallation enden. Öffnet der Alias weiterhin eine DSM-Seite, kann ein Administrator unter Systemsteuerung > Anmeldeportal > Anwendungen einen Anwendungsalias speichern, um den Webserver neu zu laden.",
|
|
416
|
+
fr: "DSM supprime l’alias du Portail de connexion de {package} et recharge automatiquement le serveur web. Le rechargement peut se terminer après la désinstallation. Si l’alias ouvre encore une page DSM, un administrateur peut enregistrer un alias d’application dans Panneau de configuration > Portail de connexion > Applications pour recharger le serveur web.",
|
|
417
|
+
es: "DSM elimina el alias del Portal de inicio de sesión de {package} y recarga automáticamente el servidor web. La recarga puede terminar después de la desinstalación. Si el alias aún abre una página DSM, un administrador puede guardar un alias de aplicación en Panel de control > Portal de inicio de sesión > Aplicaciones para recargar el servidor web.",
|
|
418
|
+
it: "DSM rimuove l’alias del Portale di accesso di {package} e ricarica automaticamente il server web. Il ricaricamento può terminare dopo la disinstallazione. Se l’alias apre ancora una pagina DSM, un amministratore può salvare un alias di applicazione in Pannello di controllo > Portale di accesso > Applicazioni per ricaricare il server web.",
|
|
419
419
|
},
|
|
420
420
|
"uninstall.portalEntry": {
|
|
421
421
|
en: "Uninstall asks the root executor to remove the Application Portal entry of {package}. If the executor is not running or was never approved, uninstall does not remove the entry; an administrator then runs these commands as root:",
|
package/dist/cli.js
CHANGED
|
@@ -421,6 +421,10 @@ export async function runCli(argv) {
|
|
|
421
421
|
.option("-t, --template <name>", `Template: ${listTemplates().join(" | ")}`)
|
|
422
422
|
.option("--package <id>", "INFO.package (defaults to the directory name)")
|
|
423
423
|
.option("--name <display>", "Display name")
|
|
424
|
+
.option("--upstream-image <repository>", "compose fork: official image repository; upgrade pins its matching tag and index digest")
|
|
425
|
+
.option("--upstream-fork", "Create only synology/ inside an upstream source fork (compose or native-go)")
|
|
426
|
+
.option("--health-path <path>", "compose: actual readiness endpoint; redirects are not followed")
|
|
427
|
+
.option("--health-status <status>", "compose: expected 2xx readiness status (no default)", Number)
|
|
424
428
|
.option("--ui <vue|plain>", `Native UI ${JSON.parse(readFileSync(join(templatesRoot(), "native-go-vue/frontend/package.json"), "utf8")).dependencies["@synopackageland/ui"]} by default (vue); plain uses ui/app.js`)
|
|
425
429
|
.option("--with <caps>", `Capabilities (comma-separated): ${CAPABILITY_NAMES.join(",")}`)
|
|
426
430
|
.option("--from <compose.yaml>", "compose: import an existing Compose; omit to create an example")
|
|
@@ -430,7 +434,7 @@ export async function runCli(argv) {
|
|
|
430
434
|
.option("--shared-folder-permission <ro|rw>", "Package account permission, defaults to rw")
|
|
431
435
|
.option("--min-dsm <version>", `Minimum DSM version (native templates and binary default to ${DEFAULT_MIN_DSM}, compose to ${CONTAINER_MIN_DSM})`)
|
|
432
436
|
.argument("[dir]", "Target directory", ".")
|
|
433
|
-
.addHelpText("after", "\nNative projects: native-go, native-python, native-node, native-rust and binary use dsm-package.yaml schema 2.\nCompose projects: compose uses
|
|
437
|
+
.addHelpText("after", "\nNative projects: native-go, native-python, native-node, native-rust and binary use dsm-package.yaml schema 2.\nCompose projects: compose uses dsm-package.yaml schema 2; --from imports compose.yaml.\nQuick start: synopkgland init -t native-go --with files,jobs,login my-app\n cd my-app && synopkgland build\n")
|
|
434
438
|
.action((dir, options, command) => {
|
|
435
439
|
if (command.parent?.opts().project)
|
|
436
440
|
throw failure("CLI_OPTION_INVALID", "--project does not apply to init.");
|
|
@@ -440,6 +444,10 @@ export async function runCli(argv) {
|
|
|
440
444
|
throw failure("INIT_TEMPLATE_UNKNOWN", `Unknown template: ${options.template}`);
|
|
441
445
|
const targetDir = resolve(dir);
|
|
442
446
|
initFromTemplate(options.template, targetDir, {
|
|
447
|
+
upstreamFork: options.upstreamFork,
|
|
448
|
+
upstreamImage: options.upstreamImage,
|
|
449
|
+
healthPath: options.healthPath,
|
|
450
|
+
healthStatus: options.healthStatus,
|
|
443
451
|
packageId: options.package,
|
|
444
452
|
displayName: options.name,
|
|
445
453
|
minDsm: options.minDsm,
|
|
@@ -457,7 +465,13 @@ export async function runCli(argv) {
|
|
|
457
465
|
permission: options.sharedFolderPermission,
|
|
458
466
|
},
|
|
459
467
|
});
|
|
460
|
-
stdout(`Created the ${options.template} project at ${targetDir}`);
|
|
468
|
+
stdout(`Created the ${options.template} project at ${options.upstreamFork ? join(targetDir, "synology") : targetDir}`);
|
|
469
|
+
if (options.template === "compose")
|
|
470
|
+
stdout("Select actual compose.web.health path/status before check/build. Redirects are not followed; check does not probe runtime health.");
|
|
471
|
+
if (options.upstreamFork) {
|
|
472
|
+
stdout("Read synology/AGENTS.md; configure the official image recipe or the native source hook, then run bash synology/upgrade.sh <upstream-tag> <build-number>.");
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
461
475
|
const next = initNextSteps(options.template, dir, existsSync(join(targetDir, "frontend", "package.json")), Boolean(process.env.SYNOPKGLAND_SDK_DIR));
|
|
462
476
|
if (next)
|
|
463
477
|
stdout(next);
|
|
@@ -131,9 +131,9 @@ function validateWebPortal(parsed, contractFile) {
|
|
|
131
131
|
return [];
|
|
132
132
|
}
|
|
133
133
|
const pos = getValuePosition(doc, ["integration", "web", "portal"]) ?? { line: 1, column: 1 };
|
|
134
|
-
//
|
|
134
|
+
// The legacy Compose contract does not generate Application Portal resources.
|
|
135
135
|
return [
|
|
136
|
-
makeError("CONTRACT_PORTAL_REQUIRES_EXECUTOR", contractSpan(contractFile, pos.line, pos.column, "integration.web.portal"), "
|
|
136
|
+
makeError("CONTRACT_PORTAL_REQUIRES_EXECUTOR", contractSpan(contractFile, pos.line, pos.column, "integration.web.portal"), "The legacy Compose contract does not support integration.web.portal. Use a schema-2 App with a top-level portal declaration and DSM 7.2-64570 or later.", "Migrate to schema 2 and top-level portal, or remove integration.web.portal."),
|
|
137
137
|
];
|
|
138
138
|
}
|
|
139
139
|
function validateDeveloperString(value, contractFile, path, pos) {
|
package/dist/e2e/dsm.d.ts
CHANGED
|
@@ -105,7 +105,7 @@ type SessionPage = {
|
|
|
105
105
|
/**
|
|
106
106
|
* Registers the signed-in session's cookies (id, did, _SSID) and SynoToken as
|
|
107
107
|
* e2e secrets, so e2e masks them in its report and output, as a request's
|
|
108
|
-
* Playwright call log carries them
|
|
108
|
+
* Playwright call log carries them. Returns how many were registered.
|
|
109
109
|
*/
|
|
110
110
|
export declare function registerDsmSecrets(page?: SessionPage, ledger?: SecretLedger): Promise<number>;
|
|
111
111
|
/**
|
|
@@ -136,7 +136,7 @@ export declare function dsmSessionLive(page: Pick<SessionPage, "evaluate">): Pro
|
|
|
136
136
|
/**
|
|
137
137
|
* `app.open(path)`, once more after net::ERR_ABORTED: about one first
|
|
138
138
|
* navigation in ten was aborted as DSM's desktop navigated on its own, and the
|
|
139
|
-
* next one loaded
|
|
139
|
+
* next one loaded. Any other failure, or a second abort, is
|
|
140
140
|
* thrown.
|
|
141
141
|
*/
|
|
142
142
|
export declare function openPath(app: Pick<App, "open">, path: string): Promise<void>;
|
package/dist/e2e/dsm.js
CHANGED
|
@@ -135,7 +135,7 @@ export async function runWithTunnel(command, outer = process.env) {
|
|
|
135
135
|
// ---- Secrets in output ----------------------------------------------------
|
|
136
136
|
// e2e redacts only the secrets its config declares. A Playwright call log in a
|
|
137
137
|
// run error lists a request's headers, DSM's session cookie among them, and e2e
|
|
138
|
-
// writes it to the report and stdout
|
|
138
|
+
// writes it to the report and stdout. A value runs to a quote, a
|
|
139
139
|
// backslash or a line end, so a JSON string's escapes end it too.
|
|
140
140
|
const mask = "***";
|
|
141
141
|
// A Cookie or Set-Cookie header, and in it DSM's session (id, _SSID) and device (did) cookies.
|
|
@@ -324,7 +324,7 @@ async function e2eSecretLedger() {
|
|
|
324
324
|
/**
|
|
325
325
|
* Registers the signed-in session's cookies (id, did, _SSID) and SynoToken as
|
|
326
326
|
* e2e secrets, so e2e masks them in its report and output, as a request's
|
|
327
|
-
* Playwright call log carries them
|
|
327
|
+
* Playwright call log carries them. Returns how many were registered.
|
|
328
328
|
*/
|
|
329
329
|
export async function registerDsmSecrets(page = dsmPage(), ledger) {
|
|
330
330
|
const target = ledger ?? await e2eSecretLedger();
|
|
@@ -410,7 +410,7 @@ function accountFile(user, env) {
|
|
|
410
410
|
/**
|
|
411
411
|
* `app.open(path)`, once more after net::ERR_ABORTED: about one first
|
|
412
412
|
* navigation in ten was aborted as DSM's desktop navigated on its own, and the
|
|
413
|
-
* next one loaded
|
|
413
|
+
* next one loaded. Any other failure, or a second abort, is
|
|
414
414
|
* thrown.
|
|
415
415
|
*/
|
|
416
416
|
export async function openPath(app, path) {
|
package/dist/error-registry.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { COMPOSE_ERRORS } from "./standalone/compose-errors.js";
|
|
2
2
|
export const ERROR_REGISTRY = {
|
|
3
3
|
...COMPOSE_ERRORS,
|
|
4
|
+
INIT_HEALTH_INVALID: { code: "INIT_HEALTH_INVALID", description: "The Compose readiness selection is invalid.", suggestion: "Supply both --health-path and --health-status with the actual endpoint and expected 2xx response; redirects are not followed." },
|
|
5
|
+
INIT_UPSTREAM_IMAGE_REQUIRED: { code: "INIT_UPSTREAM_IMAGE_REQUIRED", description: "The official upstream image repository was not selected.", suggestion: "Use init --upstream-fork --upstream-image <official-repository> for Compose." },
|
|
6
|
+
INIT_UPSTREAM_IMAGE_INVALID: { code: "INIT_UPSTREAM_IMAGE_INVALID", description: "The upstream image repository is invalid.", suggestion: "Supply the official repository without a tag or digest; upgrade.sh selects the matching release and resolves its digest." },
|
|
4
7
|
CTL_TRUST_REQUIRED: { "code": "CTL_TRUST_REQUIRED", "description": "App description access requires approval.", "suggestion": "Run synopkgland ctl list with explicit host and approve the candidate batch." },
|
|
8
|
+
COMPOSE_HEALTH_REQUIRED: { code: "COMPOSE_HEALTH_REQUIRED", description: "Compose web readiness was not selected.", suggestion: "Set compose.web.health.path and expected 2xx status for the actual App; redirects are not followed." },
|
|
5
9
|
CTL_UNSUPPORTED_BACKEND: { "code": "CTL_UNSUPPORTED_BACKEND", "description": "Automatic descriptions require a native SDK backend.", "suggestion": "Disable ctl for binary/Compose wrappers or implement the public contract separately." },
|
|
6
10
|
CTL_DESCRIBE_REQUIRED: { "code": "CTL_DESCRIBE_REQUIRED", "description": "The Go backend has no build-only registry exporter.", "suggestion": "Update the native template and implement --describe using api.DescribeRoutes." },
|
|
7
11
|
CTL_DESCRIBE_FAILED: { "code": "CTL_DESCRIBE_FAILED", "description": "The backend registry export failed.", "suggestion": "Check the backend --describe output and build diagnostics." },
|
|
@@ -71,8 +75,8 @@ export const ERROR_REGISTRY = {
|
|
|
71
75
|
},
|
|
72
76
|
CONTRACT_PORTAL_REQUIRES_EXECUTOR: {
|
|
73
77
|
code: "CONTRACT_PORTAL_REQUIRES_EXECUTOR",
|
|
74
|
-
description: "The Compose
|
|
75
|
-
suggestion: "
|
|
78
|
+
description: "The legacy Compose contract declares unsupported integration.web.portal.",
|
|
79
|
+
suggestion: "Use schema 2 with a top-level portal declaration and DSM 7.2-64570+, or remove integration.web.portal.",
|
|
76
80
|
},
|
|
77
81
|
CONTRACT_ACCESS_INVALID: {
|
|
78
82
|
code: "CONTRACT_ACCESS_INVALID",
|
|
@@ -526,8 +530,8 @@ export const ERROR_REGISTRY = {
|
|
|
526
530
|
},
|
|
527
531
|
STANDALONE_PORTAL_REQUIRES_EXECUTOR: {
|
|
528
532
|
code: "STANDALONE_PORTAL_REQUIRES_EXECUTOR",
|
|
529
|
-
description: "An App
|
|
530
|
-
suggestion: "
|
|
533
|
+
description: "An App targeting DSM below 7.2-64570 declares portal without an executor.",
|
|
534
|
+
suggestion: "Raise os_min_ver to 7.2-64570 or later to use DSM web-config and INFO nginx reload without root. On DSM 7.1, add an executor capability to register the entry, or remove portal.",
|
|
531
535
|
},
|
|
532
536
|
STANDALONE_LOGIN_STYLE_INVALID: {
|
|
533
537
|
code: "STANDALONE_LOGIN_STYLE_INVALID",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* extra_values directly, and share permission can change after install.
|
|
11
11
|
*
|
|
12
12
|
* The rows are SYNO.Core.Share.Permission v1 output verbatim. Read the tests
|
|
13
|
-
* before changing the predicate: on the test NAS, `
|
|
13
|
+
* before changing the predicate: on the test NAS, `fixtureadmin` has is_writable=false on
|
|
14
14
|
* its own row for `pkgland-p0b` and still writes there, because the
|
|
15
15
|
* `administrators` group row carries the grant. A user-row-only check rejects
|
|
16
16
|
* the only configuration with live proof behind it.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* extra_values directly, and share permission can change after install.
|
|
11
11
|
*
|
|
12
12
|
* The rows are SYNO.Core.Share.Permission v1 output verbatim. Read the tests
|
|
13
|
-
* before changing the predicate: on the test NAS, `
|
|
13
|
+
* before changing the predicate: on the test NAS, `fixtureadmin` has is_writable=false on
|
|
14
14
|
* its own row for `pkgland-p0b` and still writes there, because the
|
|
15
15
|
* `administrators` group row carries the grant. A user-row-only check rejects
|
|
16
16
|
* the only configuration with live proof behind it.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface ForkRecipeOptions {
|
|
2
|
+
upstreamImage?: string;
|
|
3
|
+
from?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function validateForkRecipe(template: 'compose' | 'native-go', options: ForkRecipeOptions): void;
|
|
6
|
+
/** CLI-owned packaging recipe; upstream files and history stay untouched. */
|
|
7
|
+
export declare function writeForkScaffold(packaging: string, template: 'compose' | 'native-go', options: ForkRecipeOptions): void;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { cpSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { parseDocument } from 'yaml';
|
|
5
|
+
import { API_PERMISSIONS_FILE } from './standalone/api-permissions.js';
|
|
6
|
+
import { parseImageReference } from './images/pin.js';
|
|
7
|
+
export function validateForkRecipe(template, options) {
|
|
8
|
+
if (template === 'native-go' && options.upstreamImage)
|
|
9
|
+
throw new Error('[INIT_OPTIONS_UNSUPPORTED] Native forks stage official release binaries or use a source build hook; image options apply only to compose.');
|
|
10
|
+
if (template !== 'compose')
|
|
11
|
+
return;
|
|
12
|
+
if (!options.upstreamImage)
|
|
13
|
+
throw new Error('[INIT_UPSTREAM_IMAGE_REQUIRED] Compose forks use official images: supply --upstream-image <official-repository>.');
|
|
14
|
+
const parsed = parseImageReference(options.upstreamImage);
|
|
15
|
+
if (!parsed || parsed.explicitTag || parsed.digest || !/^[a-z0-9][a-z0-9._:/-]*$/.test(options.upstreamImage) || options.upstreamImage.includes('//'))
|
|
16
|
+
throw new Error('[INIT_UPSTREAM_IMAGE_INVALID] Supply the official image repository without a tag or digest; upgrade.sh selects the matching upstream release tag.');
|
|
17
|
+
}
|
|
18
|
+
/** CLI-owned packaging recipe; upstream files and history stay untouched. */
|
|
19
|
+
export function writeForkScaffold(packaging, template, options) {
|
|
20
|
+
const official = template === 'compose';
|
|
21
|
+
const assets = fileURLToPath(new URL('../assets/init-upstream/', import.meta.url));
|
|
22
|
+
for (const file of ['upgrade.sh', 'version.mjs', official ? 'update-image.mjs' : 'build-source.sh'])
|
|
23
|
+
cpSync(join(assets, file), join(packaging, file));
|
|
24
|
+
const manifest = join(packaging, 'dsm-package.yaml');
|
|
25
|
+
const doc = parseDocument(readFileSync(manifest, 'utf8'));
|
|
26
|
+
const service = official ? (doc.toJS().compose.web?.service ?? 'web') : undefined;
|
|
27
|
+
const recipe = official
|
|
28
|
+
? { kind: 'official-image', repository: options.upstreamImage, service, imageTag: 'tag' }
|
|
29
|
+
: { kind: 'native-source' };
|
|
30
|
+
writeFileSync(join(packaging, 'upstream-recipe.json'), JSON.stringify(recipe, null, 2) + '\n');
|
|
31
|
+
doc.set('entry', 'admin-only');
|
|
32
|
+
if (official) {
|
|
33
|
+
doc.setIn(['identity_gateway', 'access'], 'dsm-admins');
|
|
34
|
+
if (!options.from) {
|
|
35
|
+
const path = join(packaging, 'compose.yaml');
|
|
36
|
+
const base = parseDocument(readFileSync(path, 'utf8'));
|
|
37
|
+
base.deleteIn(['services', service, 'command']);
|
|
38
|
+
base.setIn(['services', service, 'image'], `${options.upstreamImage}:SELECT_UPSTREAM_TAG`);
|
|
39
|
+
writeFileSync(path, String(base));
|
|
40
|
+
}
|
|
41
|
+
doc.setIn(['compose', 'files'], [...doc.toJS().compose.files, 'compose.image.yaml']);
|
|
42
|
+
writeFileSync(join(packaging, 'compose.image.yaml'), JSON.stringify({ services: { [service]: { image: `${options.upstreamImage}:SELECT_UPSTREAM_TAG` } } }, null, 2) + '\n');
|
|
43
|
+
}
|
|
44
|
+
if (!official) {
|
|
45
|
+
const path = join(packaging, 'backend', API_PERMISSIONS_FILE.go);
|
|
46
|
+
const policy = JSON.parse(readFileSync(path, 'utf8'));
|
|
47
|
+
for (const method of Object.keys(policy.methods))
|
|
48
|
+
policy.methods[method] = 'administrators';
|
|
49
|
+
writeFileSync(path, JSON.stringify(policy, null, 2) + '\n');
|
|
50
|
+
}
|
|
51
|
+
writeFileSync(manifest, String(doc));
|
|
52
|
+
const guidance = `
|
|
53
|
+
## Integrating an existing open-source App
|
|
54
|
+
|
|
55
|
+
Keep the upstream fork/history and remote upstream. All wrapper-owned files belong in synology/; never edit upstream files or treat the App as an SDK component. Read synopkgland skill show upstream-fork.
|
|
56
|
+
|
|
57
|
+
Commit the scaffold, then run bash synology/upgrade.sh <upstream-tag> <build-number>. It fetches the exact tag, verifies ancestry/untouched upstream files, merges, derives version/provenance, checks and builds. Commit synology/ before upgrading again. It does not publish or touch a NAS.
|
|
58
|
+
|
|
59
|
+
${official ? 'Default: use the upstream official image. upgrade.sh updates its release tag and resolves an OCI index/manifest digest through synopkgland images pin, writing only synology/compose.image.yaml. Container Manager downloads that pinned official image at install. Configure upstream-recipe.json repository/service and imageTag: tag (exact upstream tag) or version (strip v only when upstream image naming requires it). Review multi-architecture support and upstream source/license correspondence. The SELECT_UPSTREAM_TAG placeholder must be replaced by a successful upgrade before check/build.' : 'Native App: implement build-source.sh as the native payload hook; it deliberately fails until implemented. Stage official release binaries pinned by SHA-256 (the OpenList route), or compile the merged source with a source build hook. Verify the release digest before staging architecture-specific daemon.targets; keep the SDK management shell separate.'}
|
|
60
|
+
|
|
61
|
+
Use DSM administrator identity without an additional App password, an OIDC/SSO server or upstream source changes. Prefer supported trusted-header/proxy auth (FreshRSS), otherwise disable upstream login/enable guest behind the administrator-only gateway after a bounded first bootstrap (Uptime Kuma disableAuth). The third shipped pattern is the existing SDK 0.17.0 token adapter: mode: token, access: dsm-admins, entry: admin-only. OpenList obtains its native API token with the unmodified admin token CLI while stopped, stores it package-private 0600 under a 0700 directory, and the gateway injects Authorization server-side after DSM admission; packaging never delivers it to the browser. All DSM admins share one upstream admin; token reset requires package restart; WebDAV keeps its own native credentials. The upstream admin token/settings APIs remain unchanged. Verify actual DSM admission/revocation separately. Use supported config, environment, argv or proxy adapters. Every App lifecycle phase runs as the package UID. For upstream fixed-UID data directories, currently provide a synology/ startup adapter or writable path without root/chown; replace it with the framework ownership mechanism when available. Preserve licenses and the full source/component inventory.
|
|
62
|
+
`;
|
|
63
|
+
writeFileSync(join(packaging, 'AGENTS.md'), readFileSync(join(packaging, 'AGENTS.md'), 'utf8') + guidance);
|
|
64
|
+
}
|
package/dist/init.d.ts
CHANGED
|
@@ -16,6 +16,10 @@ export interface InitOptions {
|
|
|
16
16
|
ui?: "plain" | "vue";
|
|
17
17
|
with?: string[];
|
|
18
18
|
from?: string;
|
|
19
|
+
upstreamFork?: boolean;
|
|
20
|
+
upstreamImage?: string;
|
|
21
|
+
healthPath?: string;
|
|
22
|
+
healthStatus?: number;
|
|
19
23
|
/** native-go: `install.sharedFolder` (select from the NAS, or create one). key defaults to "shared", permission to "rw". */
|
|
20
24
|
sharedFolder?: {
|
|
21
25
|
mode: "select" | "create";
|