@synopackageland/cli 0.26.0 → 0.27.1

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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.27.1 (2026-10-23)
4
+
5
+ - Read executor release mappings, toolchain and six payload pins from SDK executor manifest format 1. Accept compatible SDK minors independently of CLI versions; keep the frozen 0.26.0/0.27.0 fallback. Remove the CLI digest-recording script.
6
+
7
+ ## 0.27.0 (2026-10-23)
8
+
9
+ - Depend on templates `~0.27.0` with SDK/App 0.27.0 and UI 0.14.9. Templates patches can ship independently and resolve during npm installation/update; runtime uses the installed templates without network fetching. SDK minor upgrades still require a CLI release for executor mappings/digests and `SDK_UNSUPPORTED` checks. New scaffolds use browser same-origin DSM file operations and the idle-storage fixes.
10
+ - Resolve SDK 0.27.0 to the new executor/v0.27.0 and ship all six verified executor/lifecycle digests. Preserve the previous 0.26.0 inventory and exact-version refusal for unrecorded patches. Protected Apps must rebuild and follow normal administrator approval.
11
+ - Keep Go 1.26.10 and the existing runtime/toolchain requirements. Rust crate publication and NAS/App upgrade certification are outside this release.
12
+
3
13
  ## 0.26.0 (2026-10-22)
4
14
 
5
15
  - Support SDK/App 0.26.x and pin templates 0.26.0, retaining UI 0.14.3 and executor/v0.24.0 with unchanged protected payload digests.
package/README.md CHANGED
@@ -52,20 +52,35 @@ The CLI packages, deploys and checks. The app-side code is the SDK
52
52
  `github.com/synopackageland/syno-pkg-land-sdk` and the npm package `@synopackageland/sdk` (Vue components at
53
53
  `@synopackageland/sdk/vue`). The `require github.com/synopackageland/syno-pkg-land-sdk` line in
54
54
  `backend/go.mod` is the app's only SDK version; `build` compiles the executor of that version
55
- and checks it against the digests shipped in this CLI. A CLI builds the SDK releases of its own minor version
56
- (CLI X.Y.z builds SDK X.Y.*) that its digest table records; `EXECUTOR_DIGEST_MISMATCH` names the ones it knows.
55
+ and checks both payloads against the resolved SDK's `executor-manifest.json`.
56
+ The manifest declares format 1, the exact executor release, its Go toolchain and
57
+ six SHA-256 digests. SDK minor versions are independent of CLI minor versions.
57
58
 
58
- The SDK repositories are private. The CLI adds `github.com/synopackageland/*` to its Go
59
- children's `GOPRIVATE`, `GONOPROXY` and `GONOSUMDB`, preserving existing environment and
60
- `go env -w` settings. Give git a credential that can read
61
- `https://github.com/synopackageland/syno-pkg-land-sdk` (check with `git ls-remote`);
62
- a failed fetch reports `SDK_DOWNLOAD_FAILED`.
59
+ SDK module downloads preserve normal Go checksum verification and the user's Go
60
+ settings; Go Apps also check their existing `backend/go.sum`. Commit that file
61
+ alongside npm lockfiles. Do not disable checksum verification for public SDK
62
+ packages. Private mirrors can use explicitly configured Go authentication and
63
+ checksum settings; a failed fetch reports `SDK_DOWNLOAD_FAILED`.
63
64
 
64
65
  CLI-only patch releases can differ from the SDK patch. Compose import and SDK defaults
65
66
  read the SDK pin from the installed templates; explicit App SDK declarations take precedence.
66
67
 
67
- `init` and `add` read their templates from `@synopackageland/templates`, an exact-pinned
68
- dependency. For development against local checkouts, set `SYNOPKGLAND_TEMPLATES_DIR` to a
68
+ `init` and `add` read their templates from the locally installed
69
+ `@synopackageland/templates`. CLI 0.27.1 depends on `~0.27.0`: npm installation or
70
+ update can select the latest templates patch in the 0.27 minor line. Templates
71
+ patches need only a templates release; update the npm installation to receive them.
72
+ The CLI never fetches templates at runtime, so offline use works after installation;
73
+ commit a lockfile and use `npm ci` to reproduce a chosen dependency tree.
74
+ The CLI accepts SDK executor manifest format 1 across SDK minor versions. The
75
+ exact SDK Go module sum anchors the build manifest; npm integrity anchors the
76
+ identical manifest in `@synopackageland/sdk/executor-manifest.json`. No manifest
77
+ is fetched at NAS runtime. Missing, malformed or unsupported new manifests fail
78
+ closed. SDK 0.26.0 and 0.27.0 retain the CLI's frozen legacy inventory.
79
+ SDK defaults continue to come from the actual installed templates. An SDK minor
80
+ release no longer requires a CLI release; new CLI capabilities or a new manifest
81
+ format can still require one.
82
+
83
+ For development against local checkouts, set `SYNOPKGLAND_TEMPLATES_DIR` to a
69
84
  templates checkout and `SYNOPKGLAND_SDK_DIR` to an SDK checkout; SPKs built with
70
85
  `SYNOPKGLAND_SDK_DIR` warn `EXECUTOR_DIGEST_UNVERIFIED` and must not be published. A Vue project
71
86
  packs the local SDK into its frontend (`npm install --install-links`) without building it: run
@@ -81,7 +96,7 @@ This compares `OFF_GLOSSARY` and the Italian sentence rule, including regex sour
81
96
  flags. It supports declaration-only rules with read-only use in `glossaryProblems`;
82
97
  post-declaration mutations, aliases and other unsupported rule uses fail the check instead of
83
98
  comparing a partial snapshot. The command also fails on drift or missing source. It needs a checkout because templates
84
- 0.15.1 does not publish `scripts/test/i18n.test.mjs`. The npm gate runs this check when
99
+ does not publish `scripts/test/i18n.test.mjs`. The npm gate runs this check when
85
100
  `SYNOPKGLAND_TEMPLATES_DIR` is set; otherwise it explicitly reports that the drift check was skipped.
86
101
 
87
102
  ## Capabilities
@@ -909,8 +909,13 @@ export const ERROR_REGISTRY = {
909
909
  },
910
910
  SDK_UNSUPPORTED: {
911
911
  code: "SDK_UNSUPPORTED",
912
- description: "The SDK the backend requires (backend/go.mod or backend/Cargo.lock) is not on this CLI's minor line.",
913
- suggestion: "Use a CLI on the same minor as the SDK, or raise the SDK to a version line the CLI supports (listed in the message).",
912
+ description: "The SDK has an unsupported executor manifest format or lacks a known legacy inventory.",
913
+ suggestion: "Use an SDK with executor manifest format 1, or update the CLI when a new manifest format is needed.",
914
+ },
915
+ SDK_MANIFEST_INVALID: {
916
+ code: "SDK_MANIFEST_INVALID",
917
+ description: "The resolved SDK executor manifest has invalid versions, toolchain or payload digests.",
918
+ suggestion: "Reinstall the exact locked SDK package and verify its integrity; do not replace its manifest with runtime data.",
914
919
  },
915
920
  SDK_DIR_NOT_READY: {
916
921
  code: "SDK_DIR_NOT_READY",
@@ -1,7 +1,7 @@
1
1
  import { goChildEnv } from "./go-env.js";
2
2
  import { mkdirSync, readFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
- import { PINNED_GO_HINT, pinnedGoBuildEnv, pinnedGoProblem } from '../build-spk/go-toolchain.js';
4
+ import { PINNED_GO_TOOLCHAIN, pinnedGoBuildEnv, goBinaryVersion } from '../build-spk/go-toolchain.js';
5
5
  import { sdkBackend } from '../standalone/manifest.js';
6
6
  import { StandaloneBuildError } from '../standalone/spk.js';
7
7
  import { checkCgiBinary } from '../standalone/binary.js';
@@ -11,7 +11,7 @@ import { copyModule, verifyExecutorDigests } from './executor-source.js';
11
11
  import { requiredSdk } from './sdk-source.js';
12
12
  import { rootProgramsUnsupported } from './root-programs.js';
13
13
  import { executorPolicyProblem } from './executor-policy-check.js';
14
- /** CLI-owned build flags; the release digest table is computed the same way, so they must not change within an SDK minor. */
14
+ /** Manifest format 1's reproducible build flags; changing them requires a new format. */
15
15
  const GO_BUILD = ['build', '-trimpath', '-buildvcs=false', '-ldflags', '-s -w -buildid=', '-o'];
16
16
  function failure(error) {
17
17
  const message = error.message;
@@ -32,6 +32,8 @@ export async function buildExecutor(m, arch, work, run, options = {}) {
32
32
  catch (error) {
33
33
  throw failure(error);
34
34
  }
35
+ const toolchain = sdk.manifest?.executorDigests.goToolchain ?? PINNED_GO_TOOLCHAIN;
36
+ const toolchainHint = `These executor binaries need ${toolchain}; install it on PATH or allow Go to download that exact toolchain.`;
35
37
  const dir = join(work, `executor-${arch}`);
36
38
  mkdirSync(dir);
37
39
  const source = join(dir, 'source');
@@ -42,22 +44,24 @@ export async function buildExecutor(m, arch, work, run, options = {}) {
42
44
  throw new StandaloneBuildError('EXECUTOR_BUILD_FAILED', `Executor sources of SDK ${sdk.version} are missing: ${error.message}`);
43
45
  }
44
46
  const binary = join(dir, 'executor');
45
- const result = await run('go', [...GO_BUILD, binary, '.'], { cwd: source, env: goChildEnv({ ...process.env, ...pinnedGoBuildEnv(arch) }) });
47
+ const result = await run('go', [...GO_BUILD, binary, '.'], { cwd: source, env: goChildEnv({ ...process.env, ...pinnedGoBuildEnv(arch), GOTOOLCHAIN: toolchain }) });
46
48
  if (result.missing || result.status !== 0)
47
- throw new StandaloneBuildError('EXECUTOR_BUILD_FAILED', `Executor build failed (${arch}). ${PINNED_GO_HINT}\n${result.stderr}`);
49
+ throw new StandaloneBuildError('EXECUTOR_BUILD_FAILED', `Executor build failed (${arch}). ${toolchainHint}\n${result.stderr}`);
48
50
  const problem = checkCgiBinary(work, binary, arch);
49
51
  if (problem)
50
52
  throw new StandaloneBuildError(problem.code, problem.message);
51
53
  const lifecycleBinary = join(dir, 'lifecycle');
52
- const lifecycleResult = await run('go', [...GO_BUILD, lifecycleBinary, './cmd/lifecycle'], { cwd: source, env: goChildEnv({ ...process.env, ...pinnedGoBuildEnv(arch) }) });
54
+ const lifecycleResult = await run('go', [...GO_BUILD, lifecycleBinary, './cmd/lifecycle'], { cwd: source, env: goChildEnv({ ...process.env, ...pinnedGoBuildEnv(arch), GOTOOLCHAIN: toolchain }) });
53
55
  if (lifecycleResult.missing || lifecycleResult.status !== 0)
54
- throw new StandaloneBuildError('EXECUTOR_BUILD_FAILED', `Lifecycle helper build failed (${arch}). ${PINNED_GO_HINT}\n${lifecycleResult.stderr}`);
56
+ throw new StandaloneBuildError('EXECUTOR_BUILD_FAILED', `Lifecycle helper build failed (${arch}). ${toolchainHint}\n${lifecycleResult.stderr}`);
55
57
  const lifecycleProblem = checkCgiBinary(work, lifecycleBinary, arch);
56
58
  if (lifecycleProblem)
57
59
  throw new StandaloneBuildError(lifecycleProblem.code, lifecycleProblem.message);
58
- const toolchainProblem = pinnedGoProblem(binary) ?? pinnedGoProblem(lifecycleBinary);
59
- if (toolchainProblem)
60
- throw new StandaloneBuildError('EXECUTOR_BUILD_FAILED', toolchainProblem);
60
+ for (const file of [binary, lifecycleBinary]) {
61
+ const actual = goBinaryVersion(file);
62
+ if (actual !== toolchain)
63
+ throw new StandaloneBuildError('EXECUTOR_BUILD_FAILED', `${file} was built by ${actual ?? 'an unknown Go release'}, not ${toolchain}.`);
64
+ }
61
65
  const executorDigest = sha256(readFileSync(binary));
62
66
  let warnings;
63
67
  try {
@@ -79,7 +83,7 @@ export async function buildExecutor(m, arch, work, run, options = {}) {
79
83
  const policyText = executorPolicyText(policy);
80
84
  let refused;
81
85
  try {
82
- refused = executorPolicyProblem(source, sdk.version, m.package, policyText, options.env);
86
+ refused = executorPolicyProblem(source, sdk.version, m.package, policyText, options.env, toolchain);
83
87
  }
84
88
  catch (error) {
85
89
  throw failure(error);
@@ -9,5 +9,16 @@
9
9
  "armv7": "6d25fc972eba12e3ac68f8b6a95a45e9e5706ebe19ffc1ff774fc318acbeb90f"
10
10
  },
11
11
  "goToolchain": "go1.26.10"
12
+ },
13
+ "0.27.0": {
14
+ "x86_64": "35f760533c133f7ed99d62c11dc4696d06a56fd2d125d53291aa21743d8327cc",
15
+ "aarch64": "04dff10b52ca4c54e3e6a825c27bac876f296031d8f572f667e6902e18464e31",
16
+ "armv7": "8789b2663b683ae0a5a457b41ccfcff4d54be4c98dcd3af338622051ad24f943",
17
+ "lifecycle": {
18
+ "x86_64": "c6fbb377ecd5ec7ee5b96439f22091d9fa5965dfd616d510ca9445722f436721",
19
+ "aarch64": "a9ca1750d4d5cff4727dc29789e6eaf0a365cb0e674f1bb796971d82f2d4fa27",
20
+ "armv7": "6d25fc972eba12e3ac68f8b6a95a45e9e5706ebe19ffc1ff774fc318acbeb90f"
21
+ },
22
+ "goToolchain": "go1.26.10"
12
23
  }
13
24
  }
@@ -4,9 +4,9 @@ import { type StandaloneManifest } from '../standalone/manifest.js';
4
4
  * undefined when it accepts it. The executor is
5
5
  * built for this host from executorDir, so its own rules decide, not a copy in the CLI.
6
6
  */
7
- export declare function executorPolicyRefusal(executorDir: string, pkg: string, policyText: string, env?: NodeJS.ProcessEnv): string | undefined;
7
+ export declare function executorPolicyRefusal(executorDir: string, pkg: string, policyText: string, env?: NodeJS.ProcessEnv, toolchain?: string): string | undefined;
8
8
  /** The EXECUTOR_POLICY_REJECTED message for a policy the SDK executor refuses; undefined when it accepts it. */
9
- export declare function executorPolicyProblem(executorDir: string, sdkVersion: string, pkg: string, policyText: string, env?: NodeJS.ProcessEnv): string | undefined;
9
+ export declare function executorPolicyProblem(executorDir: string, sdkVersion: string, pkg: string, policyText: string, env?: NodeJS.ProcessEnv, toolchain?: string): string | undefined;
10
10
  /** The policy of each architecture `check` would build, checked by the App's SDK executor; Apps without executor need none. */
11
11
  export declare function manifestPolicyProblem(m: StandaloneManifest, env?: NodeJS.ProcessEnv): {
12
12
  code: string;
@@ -3,7 +3,7 @@ import { spawnSync } from 'node:child_process';
3
3
  import { mkdtempSync, rmSync } from 'node:fs';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { join } from 'node:path';
6
- import { PINNED_GO_HINT, PINNED_GO_TOOLCHAIN } from '../build-spk/go-toolchain.js';
6
+ import { PINNED_GO_TOOLCHAIN } from '../build-spk/go-toolchain.js';
7
7
  import { sdkBackend } from '../standalone/manifest.js';
8
8
  import { executorPolicy, executorPolicyText } from './executor-policy.js';
9
9
  import { requiredSdk } from './sdk-source.js';
@@ -14,14 +14,14 @@ const CHECK_POLICY = 'check-policy';
14
14
  * undefined when it accepts it. The executor is
15
15
  * built for this host from executorDir, so its own rules decide, not a copy in the CLI.
16
16
  */
17
- export function executorPolicyRefusal(executorDir, pkg, policyText, env = process.env) {
17
+ export function executorPolicyRefusal(executorDir, pkg, policyText, env = process.env, toolchain = PINNED_GO_TOOLCHAIN) {
18
18
  const dir = mkdtempSync(join(tmpdir(), 'synopkgland-check-policy-'));
19
19
  try {
20
20
  const binary = join(dir, 'executor');
21
21
  const { GOOS: _os, GOARCH: _arch, ...host } = env;
22
- const build = spawnSync('go', ['build', '-o', binary, '.'], { cwd: executorDir, env: goChildEnv({ ...host, GOTOOLCHAIN: PINNED_GO_TOOLCHAIN, GOFLAGS: '', GOWORK: 'off', CGO_ENABLED: '0' }), encoding: 'utf8' });
22
+ const build = spawnSync('go', ['build', '-o', binary, '.'], { cwd: executorDir, env: goChildEnv({ ...host, GOTOOLCHAIN: toolchain, GOFLAGS: '', GOWORK: 'off', CGO_ENABLED: '0' }), encoding: 'utf8' });
23
23
  if (build.error || build.status !== 0)
24
- throw new Error(`[EXECUTOR_BUILD_FAILED] Building the SDK executor for this host to check the policy failed. ${PINNED_GO_HINT}\n${build.stderr || build.error?.message}`);
24
+ throw new Error(`[EXECUTOR_BUILD_FAILED] Building the SDK executor for this host to check the policy failed. This SDK's executor needs ${toolchain}.\n${build.stderr || build.error?.message}`);
25
25
  const result = spawnSync(binary, [CHECK_POLICY, '--package', pkg], { input: policyText, encoding: 'utf8' });
26
26
  if (result.status === 0)
27
27
  return undefined;
@@ -43,13 +43,13 @@ const POLICY_FIX = [
43
43
  [/policy entry/, 'fix entry in dsm-package.yaml'],
44
44
  ];
45
45
  /** The EXECUTOR_POLICY_REJECTED message for a policy the SDK executor refuses; undefined when it accepts it. */
46
- export function executorPolicyProblem(executorDir, sdkVersion, pkg, policyText, env = process.env) {
47
- const reason = executorPolicyRefusal(executorDir, pkg, policyText, env);
46
+ export function executorPolicyProblem(executorDir, sdkVersion, pkg, policyText, env = process.env, toolchain = PINNED_GO_TOOLCHAIN) {
47
+ const reason = executorPolicyRefusal(executorDir, pkg, policyText, env, toolchain);
48
48
  if (!reason)
49
49
  return undefined;
50
50
  const fix = POLICY_FIX.find(([pattern]) => pattern.test(reason))?.[1] ?? 'fix the declaration in dsm-package.yaml';
51
51
  return `The executor of SDK ${sdkVersion} refuses this App's policy (executor: ${reason}), so the NAS would refuse it at approval: ${fix}.`
52
- + ' If dsm-package.yaml already matches the manifest reference, this CLI and the SDK disagree: use the SDK version this CLI release names.';
52
+ + ' If dsm-package.yaml already matches the manifest reference, check the SDK manifest contract and the CLI capability support.';
53
53
  }
54
54
  /** The policy of each architecture `check` would build, checked by the App's SDK executor; Apps without executor need none. */
55
55
  export function manifestPolicyProblem(m, env = process.env) {
@@ -58,7 +58,7 @@ export function manifestPolicyProblem(m, env = process.env) {
58
58
  try {
59
59
  const sdk = requiredSdk(sdkBackend(m), env);
60
60
  for (const text of new Set(m.arches.map(arch => executorPolicyText(executorPolicy(m, arch))))) {
61
- const message = executorPolicyProblem(sdk.executorDir, sdk.version, m.package, text, env);
61
+ const message = executorPolicyProblem(sdk.executorDir, sdk.version, m.package, text, env, sdk.manifest?.executorDigests.goToolchain);
62
62
  if (message)
63
63
  return { code: 'EXECUTOR_POLICY_REJECTED', message };
64
64
  }
@@ -1,21 +1,17 @@
1
1
  import type { NativeArchFamily } from '../build-spk/dsm-arch.js';
2
2
  import type { SdkSource } from './sdk-source.js';
3
- type ArchDigests = Record<NativeArchFamily, string>;
3
+ import { type ExecutorDigests } from './sdk-executor-manifest.js';
4
4
  /** Released SDK versions: the sha256 of the executor and lifecycle helper built from them, per architecture. */
5
- export type ExecutorDigestTable = Record<string, ArchDigests & {
6
- lifecycle: ArchDigests;
7
- goToolchain: string;
8
- }>;
5
+ export type ExecutorDigestTable = Record<string, ExecutorDigests>;
9
6
  export declare const SHIPPED_EXECUTOR_DIGESTS: ExecutorDigestTable;
10
7
  /** Copy a module directory (the module cache is read-only) so the build owns a writable tree. */
11
8
  export declare function copyModule(source: string, target: string): void;
12
9
  /**
13
10
  * A released SDK's executor is only shipped when its bytes equal the digest
14
- * table; a local SDK checkout (SYNOPKGLAND_SDK_DIR) is built unverified.
11
+ * manifest (or frozen legacy table); a local SDK checkout is built unverified.
15
12
  * Returns the warnings to show.
16
13
  */
17
14
  export declare function verifyExecutorDigests(sdk: SdkSource, arch: NativeArchFamily, built: {
18
15
  executor: string;
19
16
  lifecycle: string;
20
17
  }, table?: ExecutorDigestTable): string[];
21
- export {};
@@ -1,6 +1,7 @@
1
1
  import { chmodSync, cpSync, lstatSync, readdirSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import EXECUTOR_DIGESTS from './executor-digests.json' with { type: 'json' };
4
+ import { readExecutorManifest, predatesExecutorManifest } from './sdk-executor-manifest.js';
4
5
  export const SHIPPED_EXECUTOR_DIGESTS = EXECUTOR_DIGESTS;
5
6
  /** Copy a module directory (the module cache is read-only) so the build owns a writable tree. */
6
7
  export function copyModule(source, target) {
@@ -18,13 +19,14 @@ export function copyModule(source, target) {
18
19
  }
19
20
  /**
20
21
  * A released SDK's executor is only shipped when its bytes equal the digest
21
- * table; a local SDK checkout (SYNOPKGLAND_SDK_DIR) is built unverified.
22
+ * manifest (or frozen legacy table); a local SDK checkout is built unverified.
22
23
  * Returns the warnings to show.
23
24
  */
24
25
  export function verifyExecutorDigests(sdk, arch, built, table = SHIPPED_EXECUTOR_DIGESTS) {
25
26
  if (sdk.local)
26
27
  return [`[EXECUTOR_DIGEST_UNVERIFIED] executor built from the local SDK checkout SYNOPKGLAND_SDK_DIR (${arch}, sha256 ${built.executor}); the digest table was not checked. Do not publish this SPK.`];
27
- const expected = table[sdk.version];
28
+ const manifest = sdk.manifest ?? readExecutorManifest(sdk.goDir, sdk.version);
29
+ const expected = manifest?.executorDigests ?? (predatesExecutorManifest(sdk.version) ? table[sdk.version] : undefined);
28
30
  if (!expected)
29
31
  throw new Error(`[EXECUTOR_DIGEST_MISMATCH] This CLI has no recorded executor digest for SDK ${sdk.version}; it knows ${Object.keys(table).join(', ') || 'no released SDK'}.`);
30
32
  if (expected[arch] !== built.executor)
@@ -0,0 +1,16 @@
1
+ import type { NativeArchFamily } from '../build-spk/dsm-arch.js';
2
+ export type ArchDigests = Record<NativeArchFamily, string>;
3
+ export type ExecutorDigests = ArchDigests & {
4
+ lifecycle: ArchDigests;
5
+ goToolchain: string;
6
+ };
7
+ export interface ExecutorManifest {
8
+ formatVersion: 1;
9
+ sdkVersion: string;
10
+ executorVersion: string;
11
+ executorDigests: ExecutorDigests;
12
+ }
13
+ /** Read only the resolved, integrity-protected SDK module, never a remote manifest. */
14
+ export declare function readExecutorManifest(goDir: string, version: string): ExecutorManifest | undefined;
15
+ /** Only SDKs released before the manifest contract can use the frozen inventory. */
16
+ export declare function predatesExecutorManifest(version: string): boolean;
@@ -0,0 +1,45 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ const versionPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
4
+ /** Read only the resolved, integrity-protected SDK module, never a remote manifest. */
5
+ export function readExecutorManifest(goDir, version) {
6
+ let text;
7
+ try {
8
+ text = readFileSync(join(goDir, 'executor-manifest.json'), 'utf8');
9
+ }
10
+ catch (error) {
11
+ if (error.code === 'ENOENT')
12
+ return undefined;
13
+ throw error;
14
+ }
15
+ const invalid = () => { throw new Error(`[SDK_MANIFEST_INVALID] SDK ${version} has an invalid executor-manifest.json; reinstall the exact locked SDK package.`); };
16
+ let m;
17
+ try {
18
+ m = JSON.parse(text);
19
+ }
20
+ catch {
21
+ return invalid();
22
+ }
23
+ if (!m || typeof m !== 'object' || !Number.isInteger(m.formatVersion))
24
+ return invalid();
25
+ if (m.formatVersion !== 1)
26
+ throw new Error(`[SDK_UNSUPPORTED] SDK ${version} declares executor manifest format ${m.formatVersion}; this CLI understands format 1. Update the CLI to support that format.`);
27
+ if (m.sdkVersion !== version || !versionPattern.test(m.sdkVersion) || typeof m.executorVersion !== 'string' || !versionPattern.test(m.executorVersion))
28
+ return invalid();
29
+ const d = m.executorDigests;
30
+ if (!d || typeof d.goToolchain !== 'string' || !/^go1\.\d+\.\d+$/.test(d.goToolchain))
31
+ return invalid();
32
+ for (const hashes of [d, d.lifecycle])
33
+ for (const arch of ['x86_64', 'aarch64', 'armv7']) {
34
+ if (!hashes || typeof hashes[arch] !== 'string' || !/^[a-f0-9]{64}$/.test(hashes[arch]))
35
+ return invalid();
36
+ }
37
+ return m;
38
+ }
39
+ /** Only SDKs released before the manifest contract can use the frozen inventory. */
40
+ export function predatesExecutorManifest(version) {
41
+ if (!versionPattern.test(version))
42
+ return false;
43
+ const [major, minor, patch] = version.split('.').map(Number);
44
+ return major === 0 && (minor < 27 || (minor === 27 && patch === 0));
45
+ }
@@ -1,4 +1,5 @@
1
1
  export { goPrivateCovers } from './go-env.js';
2
+ import { type ExecutorManifest } from './sdk-executor-manifest.js';
2
3
  export declare const SDK_PACKAGE = "@synopackageland/sdk";
3
4
  export declare const SDK_MODULE = "github.com/synopackageland/syno-pkg-land-sdk";
4
5
  export declare const EXECUTOR_MODULE = "github.com/synopackageland/syno-pkg-land-sdk/executor";
@@ -19,6 +20,7 @@ export interface SdkSource {
19
20
  goDir: string;
20
21
  executorDir: string;
21
22
  local: boolean;
23
+ manifest?: ExecutorManifest;
22
24
  }
23
25
  /** How each backend declares the SDK. */
24
26
  export declare const SDK_DEPENDENCY: Record<SdkBackend['kind'], string>;
@@ -41,7 +43,7 @@ export declare function goModulePath(backendDir: string): string | undefined;
41
43
  */
42
44
  export declare function sdkSource(version: string, env?: NodeJS.ProcessEnv, download?: (spec: string) => string): SdkSource;
43
45
  /** The module cache directory of module@version; the cache is read-only, so builds copy it first. */
44
- export declare function goModDownload(spec: string, env?: NodeJS.ProcessEnv): string;
46
+ export declare function goModDownload(spec: string, env?: NodeJS.ProcessEnv, moduleDir?: string): string;
45
47
  /** The SDK an App builds its executor with; delegate and rootOps cannot build without one. */
46
48
  export declare function requiredSdk(backend: SdkBackend, env?: NodeJS.ProcessEnv, download?: (spec: string) => string): SdkSource;
47
49
  /** Go children fetch the private SDK directly; failed fetches need git credentials. */
@@ -1,9 +1,9 @@
1
1
  import { spawnSync } from 'node:child_process';
2
- import { goChildEnv } from './go-env.js';
3
2
  export { goPrivateCovers } from './go-env.js';
4
3
  import { readFileSync } from 'node:fs';
5
4
  import { tmpdir } from 'node:os';
6
5
  import { join } from 'node:path';
6
+ import { readExecutorManifest, predatesExecutorManifest } from './sdk-executor-manifest.js';
7
7
  export const SDK_PACKAGE = '@synopackageland/sdk';
8
8
  export const SDK_MODULE = 'github.com/synopackageland/syno-pkg-land-sdk';
9
9
  export const EXECUTOR_MODULE = `${SDK_MODULE}/executor`;
@@ -75,8 +75,8 @@ function goMod(backendDir) {
75
75
  return '';
76
76
  }
77
77
  }
78
- /** SDK releases that reuse an immutable executor release with identical payload bytes. */
79
- const EXECUTOR_RELEASES = { '0.21.0': '0.20.0', '0.21.1': '0.20.0', '0.22.0': '0.20.0', '0.23.0': '0.20.0', '0.23.1': '0.20.0', '0.23.2': '0.20.0', '0.24.1': '0.24.0', '0.25.0': '0.24.0', '0.26.0': '0.24.0' };
78
+ /** Explicit executor release pins; older SDK versions can reuse an immutable release with identical payload bytes. */
79
+ const EXECUTOR_RELEASES = { '0.21.0': '0.20.0', '0.21.1': '0.20.0', '0.22.0': '0.20.0', '0.23.0': '0.20.0', '0.23.1': '0.20.0', '0.23.2': '0.20.0', '0.24.1': '0.24.0', '0.25.0': '0.24.0', '0.26.0': '0.24.0', '0.27.0': '0.27.0' };
80
80
  /**
81
81
  * The module directories of one SDK version. SYNOPKGLAND_SDK_DIR names a
82
82
  * local sdk checkout (development and gates only); otherwise modules come from
@@ -86,16 +86,23 @@ export function sdkSource(version, env = process.env, download = (spec) => goMod
86
86
  const dir = env.SYNOPKGLAND_SDK_DIR;
87
87
  if (dir)
88
88
  return { version, goDir: dir, executorDir: join(dir, 'executor'), local: true };
89
- return { version, goDir: download(`${SDK_MODULE}@v${version}`), executorDir: download(`${EXECUTOR_MODULE}@v${EXECUTOR_RELEASES[version] ?? version}`), local: false };
89
+ const goDir = download(`${SDK_MODULE}@v${version}`);
90
+ const manifest = readExecutorManifest(goDir, version);
91
+ if (!manifest && !predatesExecutorManifest(version))
92
+ throw new Error(`[SDK_UNSUPPORTED] SDK ${version} is missing executor-manifest.json; install a released SDK with manifest format 1.`);
93
+ return { version, goDir, executorDir: download(`${EXECUTOR_MODULE}@v${manifest?.executorVersion ?? EXECUTOR_RELEASES[version] ?? version}`), local: false, ...(manifest ? { manifest } : {}) };
90
94
  }
91
95
  const downloaded = new Map();
92
96
  /** The module cache directory of module@version; the cache is read-only, so builds copy it first. */
93
- export function goModDownload(spec, env = process.env) {
94
- const key = [spec, env.GOPROXY, env.GOMODCACHE, env.GOPATH].join('\0');
97
+ export function goModDownload(spec, env = process.env, moduleDir = tmpdir()) {
98
+ const key = [spec, moduleDir, env.GOPROXY, env.GOMODCACHE, env.GOPATH, env.GOSUMDB, env.GONOSUMDB, env.GOPRIVATE].join('\0');
95
99
  const cached = downloaded.get(key);
96
100
  if (cached)
97
101
  return cached;
98
- const result = spawnSync('go', ['mod', 'download', '-json', spec], { cwd: tmpdir(), env: goChildEnv({ ...env, GOWORK: 'off', GOFLAGS: '' }), encoding: 'utf8' });
102
+ // Public SDK manifests rely on Go module sums. Do not add the namespace to
103
+ // GOPRIVATE/GONOSUMDB as other legacy build children do. For Go Apps the
104
+ // command also checks the App's existing go.sum in its own module directory.
105
+ const result = spawnSync('go', ['mod', 'download', '-json', spec], { cwd: moduleDir, env: { ...env, GOWORK: 'off', GOFLAGS: '' }, encoding: 'utf8' });
99
106
  let info = {};
100
107
  try {
101
108
  info = JSON.parse(result.stdout);
@@ -104,6 +111,8 @@ export function goModDownload(spec, env = process.env) {
104
111
  if (result.error || result.status !== 0 || !info.Dir) {
105
112
  throw new Error(`[SDK_DOWNLOAD_FAILED] go mod download ${spec} failed: ${info.Error ?? result.stderr ?? result.error?.message}${privateSdkHint()}`);
106
113
  }
114
+ if (readExecutorManifest(info.Dir, spec.split('@v')[1] ?? '') && (typeof info.Sum !== 'string' || !/^h1:[A-Za-z0-9+/]{43}=$/.test(info.Sum)))
115
+ throw new Error(`[SDK_MANIFEST_INVALID] SDK module ${spec} has no verified Go module sum.`);
107
116
  downloaded.set(key, info.Dir);
108
117
  return info.Dir;
109
118
  }
@@ -112,7 +121,7 @@ export function requiredSdk(backend, env = process.env, download) {
112
121
  const version = appSdkVersion(backend);
113
122
  if (!version)
114
123
  throw new Error(`[SDK_REQUIRED] capabilities.delegate and rootOps need the SDK: ${SDK_DEPENDENCY[backend.kind]}`);
115
- return download ? sdkSource(version, env, download) : sdkSource(version, env);
124
+ return sdkSource(version, env, download ?? (spec => goModDownload(spec, env, backend.kind === 'go' ? backend.dir : undefined)));
116
125
  }
117
126
  /** Go children fetch the private SDK directly; failed fetches need git credentials. */
118
127
  export function privateSdkHint() {
@@ -5,7 +5,7 @@ export interface SdkProblem {
5
5
  }
6
6
  /**
7
7
  * The backend's dependency files name the App's SDK version; the frontend
8
- * package and this CLI must agree with it. `needsSdk`: the App declares
8
+ * must agree, and the CLI must understand its manifest. `needsSdk`: the App declares
9
9
  * delegate or rootOps.
10
10
  */
11
- export declare function sdkProblems(backend: SdkBackend, frontendDir: string | undefined, needsSdk: boolean, cliVersion?: string): SdkProblem[];
11
+ export declare function sdkProblems(backend: SdkBackend, frontendDir: string | undefined, needsSdk: boolean, cliVersion?: string, env?: NodeJS.ProcessEnv, download?: (spec: string) => string): SdkProblem[];
@@ -1,16 +1,17 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
  import { CLI_VERSION } from "../cli-version.js";
4
- import { appSdkVersion, SDK_DEPENDENCY, SDK_FLOOR, SDK_PACKAGE, sdkBelowFloor } from "../runtime/sdk-source.js";
5
- const minorLine = (version) => version.split(".").slice(0, 2).join(".");
4
+ import { appSdkVersion, SDK_DEPENDENCY, SDK_FLOOR, SDK_PACKAGE, sdkBelowFloor, sdkSource, goModDownload } from "../runtime/sdk-source.js";
5
+ import { predatesExecutorManifest } from '../runtime/sdk-executor-manifest.js';
6
+ import { SHIPPED_EXECUTOR_DIGESTS } from '../runtime/executor-source.js';
6
7
  /** The file that names each backend kind's SDK version. */
7
8
  const SDK_VERSION_FILE = { go: "backend/go.mod", rust: "Cargo.lock", python: "backend.sdk", node: "backend.sdk" };
8
9
  /**
9
10
  * The backend's dependency files name the App's SDK version; the frontend
10
- * package and this CLI must agree with it. `needsSdk`: the App declares
11
+ * must agree, and the CLI must understand its manifest. `needsSdk`: the App declares
11
12
  * delegate or rootOps.
12
13
  */
13
- export function sdkProblems(backend, frontendDir, needsSdk, cliVersion = CLI_VERSION) {
14
+ export function sdkProblems(backend, frontendDir, needsSdk, cliVersion = CLI_VERSION, env = process.env, download) {
14
15
  let version;
15
16
  try {
16
17
  version = appSdkVersion(backend);
@@ -22,16 +23,29 @@ export function sdkProblems(backend, frontendDir, needsSdk, cliVersion = CLI_VER
22
23
  throw error;
23
24
  }
24
25
  if (!version) {
25
- return needsSdk ? [{ code: "SDK_REQUIRED", message: `Python, Node and Rust Apps, plus capabilities.delegate and rootOps, need the SDK (${minorLine(cliVersion)}.x): ${SDK_DEPENDENCY[backend.kind]}` }] : [];
26
+ return needsSdk ? [{ code: "SDK_REQUIRED", message: `Python, Node and Rust Apps, plus capabilities.delegate and rootOps, need the SDK: ${SDK_DEPENDENCY[backend.kind]}` }] : [];
26
27
  }
27
28
  const file = SDK_VERSION_FILE[backend.kind];
28
29
  const problems = [];
29
- if (minorLine(version) !== minorLine(cliVersion)) {
30
- problems.push({ code: "SDK_UNSUPPORTED", message: `${file} requires SDK ${version}; CLI ${cliVersion} supports only SDK ${minorLine(cliVersion)}.x. Use the matching CLI, or raise the SDK to ${minorLine(cliVersion)}.x.` });
31
- }
32
- else if (sdkBelowFloor(version)) {
30
+ if (sdkBelowFloor(version)) {
33
31
  problems.push({ code: "SDK_UNSUPPORTED", message: `${file} requires SDK ${version}; CLI ${cliVersion} supports SDK ${SDK_FLOOR} or later. Raise the SDK to ${SDK_FLOOR} or later.` });
34
32
  }
33
+ else if (predatesExecutorManifest(version)) {
34
+ if (!SHIPPED_EXECUTOR_DIGESTS[version])
35
+ problems.push({ code: 'SDK_UNSUPPORTED', message: `${file} requires SDK ${version}; this pre-manifest SDK has no pinned executor inventory. Use SDK 0.26.0, 0.27.0, or a release with executor manifest format 1.` });
36
+ }
37
+ else {
38
+ try {
39
+ sdkSource(version, env, download ?? (spec => goModDownload(spec, env, backend.kind === 'go' ? backend.dir : undefined)));
40
+ }
41
+ catch (error) {
42
+ const message = error.message;
43
+ const tagged = /^\[([A-Z_]+)\] ([\s\S]*)$/.exec(message);
44
+ if (!tagged)
45
+ throw error;
46
+ problems.push({ code: tagged[1], message: tagged[2] });
47
+ }
48
+ }
35
49
  const frontend = frontendDir === undefined ? undefined : frontendSdkVersion(frontendDir);
36
50
  if (frontend && frontend.version !== version) {
37
51
  problems.push({ code: "SDK_VERSION_MISMATCH", message: `frontend/package.json has ${SDK_PACKAGE} ${frontend.spec} (${frontend.version ?? "version unreadable"}), while ${file} requires ${version}; the two must match.` });
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The scaffold templates come from the exactly pinned @synopackageland/templates
2
+ * The scaffold templates come from the locally installed @synopackageland/templates
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;
@@ -2,7 +2,7 @@ import { createRequire } from "node:module";
2
2
  import { dirname, join } from "node:path";
3
3
  import { sdkRequirement } from "./runtime/sdk-source.js";
4
4
  /**
5
- * The scaffold templates come from the exactly pinned @synopackageland/templates
5
+ * The scaffold templates come from the locally installed @synopackageland/templates
6
6
  * package; SYNOPKGLAND_TEMPLATES_DIR points at a templates checkout instead.
7
7
  */
8
8
  export function templatesRoot(env = process.env, from = import.meta.url) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synopackageland/cli",
3
- "version": "0.26.0",
3
+ "version": "0.27.1",
4
4
  "description": "Syno Package Land Developer Kit CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,14 +20,14 @@
20
20
  "build": "rm -rf dist && tsc",
21
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
- "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",
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/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",
25
25
  "gates:fast": "bash scripts/ci/gates.sh --fast",
26
26
  "prepublishOnly": "npm run build"
27
27
  },
28
28
  "dependencies": {
29
29
  "@napi-rs/keyring": "^2.1.1",
30
- "@synopackageland/templates": "0.26.0",
30
+ "@synopackageland/templates": "~0.27.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
@@ -5,6 +5,17 @@ description: Initialize, extend, check, build, inspect, and deploy Synology appl
5
5
 
6
6
  # Independent Synology SPKs
7
7
 
8
+ CLI 0.27.x installs templates through `~0.27.0`. A templates patch needs only a
9
+ templates release; npm install/update selects a patch in that minor line. The CLI
10
+ uses the locally installed templates and reads their SDK pin for defaults; it
11
+ never downloads templates at runtime. Preserve the lockfile and use `npm ci` for
12
+ reproducible offline-ready installations. SDK minor releases are independent of
13
+ the CLI: executor manifest format 1 in the resolved SDK pins the executor release,
14
+ Go toolchain and six payload digests. Preserve npm integrity and Go module sums.
15
+ The CLI verifies bytes before packaging and never fetches a manifest at NAS runtime.
16
+ Unsupported formats require a CLI update; invalid manifests fail closed. Existing
17
+ SDK 0.26.0/0.27.0 use the frozen legacy inventory.
18
+
8
19
  Before releasing an executor App, read [reference/approval-upgrades.md](reference/approval-upgrades.md). Run build/inspect with the previous released SPK as `--baseline` and include the approval difference in release notes. Without a comparable baseline, retention is unknown.
9
20
 
10
21
  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,禁止發明新的身分平台。
@@ -25,6 +36,8 @@ Read [reference/lifecycle.md](reference/lifecycle.md) before editing `dsm-script
25
36
 
26
37
  Read [reference/ui-vue.md](reference/ui-vue.md) when editing Vue components in the native-go Vue template; use the installed SDK contracts, keep DSM embedding boundaries, and verify the actual app.
27
38
 
39
+ Buttons follow DSM's policy ([reference/ui-vue.md](reference/ui-vue.md#buttons), the UI package's `docs/button-policy.md`): only the bar that closes a dialog, wizard or form draws capsules, with Cancel/Reset first and one primary (or one danger to confirm a deletion) last; toolbar, page, section and row actions are grey tool buttons, Delete included; links go to `Link`. Choose the variant, never the shape.
40
+
28
41
  Read [reference/ui-json.md](reference/ui-json.md) when generating or editing JSON UI; discover the installed catalog, validate the spec, then render and fix layout issues.
29
42
 
30
43
  ## UI copywriting
@@ -232,7 +232,7 @@ declared availability readiness, LAN isolation/egress, protected grant lifecycle
232
232
  ACLs, two-user visibility and clean install/upgrade/reboot/stop/uninstall before
233
233
  calling the package supported on a NAS. No NAS is accessed by the build gate.
234
234
 
235
- The unified Compose contract uses SDK 0.17.3 and templates 0.17.5 with CLI 0.17.3. Use the published, matching versions together; the legacy packaging route and bridge publish mode are removed.
235
+ The unified Compose contract uses SDK 0.27.x with CLI 0.27.x and locally installed templates from `~0.27.0`. Templates patches can ship independently; npm install/update resolves them, and SDK defaults come from the installed templates. SDK minor upgrades do not require a CLI release when they declare supported executor manifest format 1; the resolved SDK owns the release mapping and verified payload digests. The CLI does not fetch templates at runtime. The legacy packaging route and bridge publish mode are removed.
236
236
 
237
237
  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.
238
238
 
@@ -61,6 +61,31 @@ Reuse the embedded theme's typography, spacing and public controls. Preserve
61
61
  loading, empty, error, disabled and permission states. Backend policies and DSM
62
62
  checks decide authority; component visibility cannot grant access.
63
63
 
64
+ ## Buttons
65
+
66
+ Follow DSM's button policy, in the installed
67
+ `node_modules/@synopackageland/ui/docs/button-policy.md`. Choose only the
68
+ `variant`; the container chooses the shape, so do not set `kind="action"`.
69
+
70
+ | Place | Use |
71
+ | --- | --- |
72
+ | Dialog, Sheet, Wizard footer; `FormActions`, `AppActionBar`, `Footbar`, Panel `#fbar` | capsule: Cancel or Reset `secondary`, then one `primary` last |
73
+ | Confirming a deletion | Dialog footer: Cancel, then one `danger` last |
74
+ | Toolbar, PageHeader, section or card body and `#footer`, beside a field | grey tool `Button` (Delete too) |
75
+ | List or grid row | `IconButton` with a `label` |
76
+ | Help, docs, external pages | `Link`, not a button |
77
+
78
+ - A capsule only ends something: it closes a dialog, applies a form or moves a
79
+ wizard. Disable this connection, Open or Check again stay tool buttons, even
80
+ in a `SettingsSection` `#footer`; put a section's Save / Cancel in `FormActions`.
81
+ - One main action per footer, placed last. Inside a page, `primary` only for
82
+ the one step that unblocks it (Sign in), at most one per section.
83
+ - Keep the default size; neighbouring buttons share one size. One refresh per
84
+ page, in the PageHeader.
85
+ - In development the UI warns in the console about a capsule outside a footer,
86
+ two main actions in a footer, or a main action that is not last. Clear these
87
+ warnings before handing over the app.
88
+
64
89
  ## Start fast
65
90
 
66
91
  Every api.cgi call pays DSM's session check (about 60–240 ms on a NAS), so