@synopackageland/cli 0.38.0 → 0.39.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 +17 -0
- package/README.md +4 -0
- package/dist/cli.js +1 -0
- package/dist/error-registry.js +5 -0
- package/dist/standalone/inspect-spk.d.ts +2 -0
- package/dist/standalone/inspect-spk.js +29 -6
- package/dist/standalone/manifest.d.ts +2 -0
- package/dist/standalone/manifest.js +16 -0
- package/dist/standalone/native-layout.d.ts +2 -0
- package/dist/standalone/native-layout.js +14 -4
- package/dist/standalone/package-dependencies.d.ts +17 -0
- package/dist/standalone/package-dependencies.js +57 -0
- package/dist/standalone/spk.js +6 -6
- package/package.json +1 -1
- package/skills/SKILL.md +17 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.39.0 (2026-10-29)
|
|
4
|
+
|
|
5
|
+
- Add schema-2 `dependencies.packages` for DSM package dependencies independent
|
|
6
|
+
of the App language or daemon interpreter. Each entry has a validated `name`
|
|
7
|
+
and optional quoted numeric `min_version`. Build merges, sorts and
|
|
8
|
+
deduplicates these with Node backend and `daemon.node` requirements into
|
|
9
|
+
one INFO `install_dep_packages` field; distinct specified minimum versions
|
|
10
|
+
for one package fail validation. Daemon executable, arguments, environment
|
|
11
|
+
and supervision are unchanged. Native layouts and Compose are supported.
|
|
12
|
+
`check --json` reports normalized dependencies, build metadata records them,
|
|
13
|
+
and `inspect` verifies normalization, INFO agreement and inferred Node
|
|
14
|
+
requirements. Document `/var/packages/<name>/target`, the manifest field
|
|
15
|
+
and the difference from `daemon.node` in the manifest/daemon guides and App
|
|
16
|
+
skill. Add parser, merge/conflict, compiled CLI, artifact and mutation tests.
|
|
17
|
+
- Make the native gateway listener-wait test assert both listeners close,
|
|
18
|
+
without assuming a scheduling-dependent minimum number of polls.
|
|
19
|
+
|
|
3
20
|
## 0.38.0 (2026-10-29)
|
|
4
21
|
|
|
5
22
|
- Audit direct `ipc.StartPeerUnix` calls and reject constructor aliases.
|
package/README.md
CHANGED
|
@@ -49,6 +49,10 @@ See the [upstream scaffold and release checklist](skills/reference/upstream-fork
|
|
|
49
49
|
Existing native products with their own payload layout use `backend: {kind: native}` in
|
|
50
50
|
`dsm-package.yaml`.
|
|
51
51
|
|
|
52
|
+
Declare language-independent DSM installation requirements with
|
|
53
|
+
[`dependencies.packages`](docs/manifest.md). They merge with inferred Node
|
|
54
|
+
requirements without changing the App's executable or runtime.
|
|
55
|
+
|
|
52
56
|
A backend in Python, Node or Rust uses `backend: {runtime: python|node|rust, ...}`: the CGI is
|
|
53
57
|
the SDK's Go runtime, which checks the DSM session and `permissions.json` and starts the App
|
|
54
58
|
from `target/app` as the package account for each request (see `skills/reference/create.md`).
|
package/dist/cli.js
CHANGED
|
@@ -405,6 +405,7 @@ async function runCliWithin(argv) {
|
|
|
405
405
|
emit("check", {
|
|
406
406
|
package: loaded.manifest.package,
|
|
407
407
|
warnings: validation.warnings ?? [],
|
|
408
|
+
...("dependencies" in loaded.manifest && loaded.manifest.dependencies ? { dependencies: loaded.manifest.dependencies } : {}),
|
|
408
409
|
...("installSettings" in loaded.manifest && loaded.manifest.installSettings ? { installSettings: loaded.manifest.installSettings } : {}),
|
|
409
410
|
...("hyperBackup" in loaded.manifest && loaded.manifest.hyperBackup ? { hyperBackup: backupRecord(loaded.manifest.hyperBackup) } : {}),
|
|
410
411
|
...("identityGateway" in loaded.manifest && gatewayAdmissionReport(loaded.manifest.identityGateway, loaded.manifest.entry) ? { identityGateway: gatewayAdmissionReport(loaded.manifest.identityGateway, loaded.manifest.entry) } : {}),
|
package/dist/error-registry.js
CHANGED
|
@@ -283,6 +283,11 @@ export const ERROR_REGISTRY = {
|
|
|
283
283
|
description: "dsm-package.yaml contains an unsupported field.",
|
|
284
284
|
suggestion: "Remove the field; a Standalone Package does not accept custom lifecycle scripts or DSM conf.",
|
|
285
285
|
},
|
|
286
|
+
STANDALONE_DEPENDENCIES_INVALID: {
|
|
287
|
+
code: "STANDALONE_DEPENDENCIES_INVALID",
|
|
288
|
+
description: "The DSM package dependencies are invalid or conflict with an inferred dependency.",
|
|
289
|
+
suggestion: "Use dependencies.packages entries with only name and optional quoted numeric min_version; use one identical minimum version per package, including daemon.node.",
|
|
290
|
+
},
|
|
286
291
|
STANDALONE_SCHEMA_INVALID: {
|
|
287
292
|
code: "STANDALONE_SCHEMA_INVALID",
|
|
288
293
|
description: "The schema version in dsm-package.yaml is not supported.",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PackageDependencies } from "./package-dependencies.js";
|
|
1
2
|
import { type InstallSettings } from './install-settings.js';
|
|
2
3
|
import { type WebLaunchMode } from "../contract/web-launch.js";
|
|
3
4
|
import { type ComposeApp } from "./compose.js";
|
|
@@ -9,6 +10,7 @@ import type { EntryMode } from "./manifest.js";
|
|
|
9
10
|
import { type UninstallCleanupItem } from "../runtime/uninstall-cleanup.js";
|
|
10
11
|
export type SpkKind = "native-go" | "native-python" | "native-node" | "native-rust" | "binary" | "container" | "native-layout";
|
|
11
12
|
export interface SpkInspection {
|
|
13
|
+
dependencies?: PackageDependencies;
|
|
12
14
|
installSettings?: InstallSettings;
|
|
13
15
|
/** Hyper Backup application backup, as sdk-package.json records it. */
|
|
14
16
|
hyperBackup?: HyperBackup;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { inspectPackageDependencies, dependencyInfo } from "./package-dependencies.js";
|
|
1
2
|
import { readInstallSettings, settingsInstallSteps, settingsConfig, renderSettingsHook } from './install-settings.js';
|
|
2
3
|
import { MODULE_UI_ENTRY, uiTreeDigest } from "./ui-assets.js";
|
|
3
4
|
import { nativePortal } from "./portal.js";
|
|
@@ -66,7 +67,12 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
66
67
|
await tar.x({ file: spkPath, cwd: dir });
|
|
67
68
|
const findings = [];
|
|
68
69
|
if (!expectedKind && existsSync(join(dir, "native-layout.json"))) {
|
|
69
|
-
|
|
70
|
+
let dependencies;
|
|
71
|
+
try {
|
|
72
|
+
dependencies = inspectPackageDependencies(JSON.parse(readFileSync(join(dir, "native-layout.json"), "utf8")).dependencies, findings);
|
|
73
|
+
}
|
|
74
|
+
catch { /* inspectNativeLayout reports a malformed inventory. */ }
|
|
75
|
+
const info = readInfo(join(dir, "INFO"), findings, "native-layout", false, undefined, dependencies);
|
|
70
76
|
const payload = await listPayload(join(dir, "package.tgz"), findings);
|
|
71
77
|
const target = join(dir, "layout-inspection");
|
|
72
78
|
mkdirSync(target);
|
|
@@ -83,6 +89,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
83
89
|
findings.push(`Missing INFO ${key}`);
|
|
84
90
|
const icon = inspectPackageIcons(dir, findings);
|
|
85
91
|
return {
|
|
92
|
+
...(dependencies ? { dependencies } : {}),
|
|
86
93
|
kind: "native-layout",
|
|
87
94
|
package: info.package,
|
|
88
95
|
version: info.version,
|
|
@@ -134,6 +141,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
134
141
|
findings.push("sdk-package.json does not match the schema 2 capability inventory.");
|
|
135
142
|
}
|
|
136
143
|
}
|
|
144
|
+
const dependencies = inspectPackageDependencies(metadata.dependencies, findings);
|
|
137
145
|
const daemonRestart = recoverDaemonRestart(metadata.daemon_restart, metadata.daemon_sdk, findings);
|
|
138
146
|
await inspectModuleUi(dir, paths, metadata.ui_module, findings);
|
|
139
147
|
if (metadata.daemon_restart !== undefined && (metadata.compose !== undefined || !paths.has("daemon/control")))
|
|
@@ -152,6 +160,9 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
152
160
|
const integrations = metadata.integrations ?? [];
|
|
153
161
|
if (metadata.compose !== undefined) {
|
|
154
162
|
const result = await inspectUnifiedCompose(dir, payload, { ...metadata, installSettings }, findings);
|
|
163
|
+
if (dependencies) {
|
|
164
|
+
result.dependencies = dependencies;
|
|
165
|
+
}
|
|
155
166
|
if (installSettings)
|
|
156
167
|
result.installSettings = installSettings;
|
|
157
168
|
if (hyperBackup)
|
|
@@ -181,7 +192,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
181
192
|
? undefined
|
|
182
193
|
: await recoverCompose(dir, metadata.compose, findings);
|
|
183
194
|
const daemonNode = recoverDaemonNode(metadata.daemon_node, findings);
|
|
184
|
-
const info = readInfo(join(dir, "INFO"), findings, kind, Boolean(combined), daemonNode);
|
|
195
|
+
const info = readInfo(join(dir, "INFO"), findings, kind, Boolean(combined), daemonNode, dependencies);
|
|
185
196
|
if (combined &&
|
|
186
197
|
(!info.os_min_ver || !isDsmAtLeast(info.os_min_ver, CONTAINER_MIN_DSM)))
|
|
187
198
|
findings.push("compose identity_gateway requires DSM >= 7.2.1");
|
|
@@ -230,6 +241,8 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
230
241
|
const bridge = compiledKind && kind !== "native-go"
|
|
231
242
|
? await checkBridgeApp(dir, metadata.backend, info, payload, findings)
|
|
232
243
|
: undefined;
|
|
244
|
+
if (dependencies && bridge?.node?.package && !dependencies.packages.some(d => d.name === bridge.node.package))
|
|
245
|
+
findings.push("DEPENDENCIES_INVALID build metadata dependencies omit the Node backend package.");
|
|
233
246
|
const hasExecutor = (metadata.capabilities ?? []).some((name) => name === "rootOps" || name === "delegate" || name === "webapi") || paths.has("executor/executor");
|
|
234
247
|
if (standalone && portal)
|
|
235
248
|
findings.push(...inspectPortalInfo(info, hasExecutor));
|
|
@@ -315,6 +328,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
315
328
|
findings.push(`${CGI_PAYLOAD_PATH} has mode ${cgiEntry.mode}; it should be 0755`);
|
|
316
329
|
}
|
|
317
330
|
return {
|
|
331
|
+
...(dependencies ? { dependencies } : {}),
|
|
318
332
|
kind,
|
|
319
333
|
...(identityGateway ? { identityGateway } : {}),
|
|
320
334
|
...(installSettings ? { installSettings } : {}),
|
|
@@ -691,7 +705,7 @@ async function checkArch(dir, info, kind, hasCgi, findings) {
|
|
|
691
705
|
}
|
|
692
706
|
/** Replay the SDK planner over archived source bytes, not merely a mutable lock. */
|
|
693
707
|
async function inspectUnifiedCompose(dir, payload, metadata, findings) {
|
|
694
|
-
const info = readInfo(join(dir, "INFO"), findings, "binary", true);
|
|
708
|
+
const info = readInfo(join(dir, "INFO"), findings, "binary", true, undefined, inspectPackageDependencies(metadata.dependencies, findings));
|
|
695
709
|
const base = {
|
|
696
710
|
kind: "container",
|
|
697
711
|
package: info.package,
|
|
@@ -1064,7 +1078,7 @@ function archivedInfoTranslations(info) {
|
|
|
1064
1078
|
}
|
|
1065
1079
|
return translations;
|
|
1066
1080
|
}
|
|
1067
|
-
function readInfo(path, findings, kind, compose = false, daemonNode) {
|
|
1081
|
+
function readInfo(path, findings, kind, compose = false, daemonNode, dependencies) {
|
|
1068
1082
|
if (!existsSync(path)) {
|
|
1069
1083
|
findings.push("INFO is missing");
|
|
1070
1084
|
return {};
|
|
@@ -1073,6 +1087,8 @@ function readInfo(path, findings, kind, compose = false, daemonNode) {
|
|
|
1073
1087
|
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
1074
1088
|
const match = line.match(/^([a-z_]+)="(.*)"$/);
|
|
1075
1089
|
if (match) {
|
|
1090
|
+
if (match[1] === "install_dep_packages" && Object.hasOwn(info, match[1]))
|
|
1091
|
+
findings.push("INFO install_dep_packages must appear only once");
|
|
1076
1092
|
info[match[1]] = match[2];
|
|
1077
1093
|
}
|
|
1078
1094
|
}
|
|
@@ -1098,7 +1114,14 @@ function readInfo(path, findings, kind, compose = false, daemonNode) {
|
|
|
1098
1114
|
: kind === "native-node"
|
|
1099
1115
|
? /^Node\.js_v[0-9]{2}([><=].*)?$/
|
|
1100
1116
|
: undefined;
|
|
1101
|
-
if (
|
|
1117
|
+
if (dependencies) {
|
|
1118
|
+
if ((info.install_dep_packages ?? "") !== dependencyInfo(dependencies))
|
|
1119
|
+
findings.push("INFO install_dep_packages does not match build metadata dependencies");
|
|
1120
|
+
const inferred = daemonNode && dependencies.packages.find(d => d.name === daemonNode.package);
|
|
1121
|
+
if (daemonNode && (!inferred || daemonNode.min_version !== undefined && inferred.min_version !== daemonNode.min_version))
|
|
1122
|
+
findings.push("DEPENDENCIES_INVALID build metadata dependencies conflict with daemon.node");
|
|
1123
|
+
}
|
|
1124
|
+
else if (daemonNode && info.install_dep_packages !== nodeDependency(daemonNode))
|
|
1102
1125
|
findings.push(`daemon.node requires INFO install_dep_packages="${nodeDependency(daemonNode)}"`);
|
|
1103
1126
|
else if (!daemonNode &&
|
|
1104
1127
|
info.install_dep_packages &&
|
|
@@ -1453,7 +1476,7 @@ async function inspectInstallSettings(dir, payload, metadata, findings) {
|
|
|
1453
1476
|
const root = join(dir, 'install-settings-inspection');
|
|
1454
1477
|
mkdirSync(root);
|
|
1455
1478
|
await tar.x({ file: join(dir, 'package.tgz'), cwd: root });
|
|
1456
|
-
const info = readInfo(join(dir, 'INFO'), findings, 'binary', Boolean(metadata.compose));
|
|
1479
|
+
const info = readInfo(join(dir, 'INFO'), findings, 'binary', Boolean(metadata.compose), undefined, inspectPackageDependencies(metadata.dependencies, findings));
|
|
1457
1480
|
const gateway = metadata.compose?.gateway ?? metadata.native_gateway;
|
|
1458
1481
|
const m = { package: info.package, os_min_ver: info.os_min_ver, capabilities: {}, installSettings: settings, identityGateway: gateway };
|
|
1459
1482
|
try {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PackageDependencies } from "./package-dependencies.js";
|
|
1
2
|
import { type InstallSettings } from './install-settings.js';
|
|
2
3
|
import { type WebLaunchMode } from "../contract/web-launch.js";
|
|
3
4
|
import { type WebAPIProfiles } from "../runtime/webapi-profile.js";
|
|
@@ -51,6 +52,7 @@ export interface StandaloneManifest {
|
|
|
51
52
|
commands: Record<string, import("../ctl/descriptor.js").Operation>;
|
|
52
53
|
};
|
|
53
54
|
metadata?: PackageMetadata;
|
|
55
|
+
dependencies?: PackageDependencies;
|
|
54
56
|
root: string;
|
|
55
57
|
package: string;
|
|
56
58
|
version: string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readPackageDependencies, packageDependencies, dependencyError } from "./package-dependencies.js";
|
|
1
2
|
import { readInstallSettings } from './install-settings.js';
|
|
2
3
|
import { auditGoRoutes } from './route-audit.js';
|
|
3
4
|
import { BACKGROUND_WORKER_MAIN } from './background.js';
|
|
@@ -55,6 +56,7 @@ export const KNOWN_FIELDS = new Set([
|
|
|
55
56
|
"capabilities",
|
|
56
57
|
"i18n",
|
|
57
58
|
"daemon",
|
|
59
|
+
"dependencies",
|
|
58
60
|
"identity_gateway",
|
|
59
61
|
"compose",
|
|
60
62
|
"install",
|
|
@@ -247,6 +249,19 @@ function readManifest(raw, c) {
|
|
|
247
249
|
const lifecycleTimeouts = timeoutOnly ? readLifecycleTimeouts(raw.daemon.timeouts, (e) => c.fail(e.code, e.message)) : undefined;
|
|
248
250
|
const daemon = arches &&
|
|
249
251
|
readDaemon(timeoutOnly ? undefined : raw.daemon, c.root, arches, (e) => c.fail(e.code, e.message));
|
|
252
|
+
let dependencies;
|
|
253
|
+
try {
|
|
254
|
+
const explicit = readPackageDependencies(raw.dependencies);
|
|
255
|
+
if (backend) {
|
|
256
|
+
const effective = packageDependencies({ backend, daemon, dependencies: explicit });
|
|
257
|
+
if (explicit || effective.packages.length)
|
|
258
|
+
dependencies = effective;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
const problem = dependencyError(error);
|
|
263
|
+
c.fail(problem.code, problem.message);
|
|
264
|
+
}
|
|
250
265
|
const gatewayRaw = isRecord(raw.identity_gateway) ? raw.identity_gateway : {};
|
|
251
266
|
if (raw.compose !== undefined && isRecord(raw.ctl) && raw.ctl.enabled === true)
|
|
252
267
|
c.fail("CTL_UNSUPPORTED_BACKEND", "Compose does not support automatic ctl descriptions; disable ctl.");
|
|
@@ -507,6 +522,7 @@ function readManifest(raw, c) {
|
|
|
507
522
|
...(rootOps ? { rootOps } : {}),
|
|
508
523
|
...(rootPrograms ? { rootPrograms } : {}),
|
|
509
524
|
...(daemon ? { daemon } : {}),
|
|
525
|
+
...(dependencies ? { dependencies } : {}),
|
|
510
526
|
...(lifecycleTimeouts ? { lifecycleTimeouts } : {}),
|
|
511
527
|
...(identityGateway ? { identityGateway } : {}),
|
|
512
528
|
...(compose ? { compose } : {}),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PackageDependencies } from "./package-dependencies.js";
|
|
1
2
|
import { type PackageMetadata } from "../package-metadata.js";
|
|
2
3
|
import type { CommandRunner } from './build.js';
|
|
3
4
|
import { type Toolkit } from './toolkit.js';
|
|
@@ -21,6 +22,7 @@ type Binary = {
|
|
|
21
22
|
};
|
|
22
23
|
export interface NativeLayout {
|
|
23
24
|
metadata?: PackageMetadata;
|
|
25
|
+
dependencies?: PackageDependencies;
|
|
24
26
|
root: string;
|
|
25
27
|
package: string;
|
|
26
28
|
version: string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readPackageDependencies, dependencyInfo, inspectPackageDependencies } from "./package-dependencies.js";
|
|
1
2
|
import { portalInfoFields } from "../portal/info.js";
|
|
2
3
|
import { validateMetadata, metadataInfoFields } from "../package-metadata.js";
|
|
3
4
|
/** Existing native products: the CLI owns archives/INFO, the project owns its layout. */
|
|
@@ -106,13 +107,15 @@ export function isNativeLayout(root) {
|
|
|
106
107
|
}
|
|
107
108
|
export function loadNativeLayout(root) {
|
|
108
109
|
try {
|
|
109
|
-
const m = object(parse(readFileSync(join(root, 'dsm-package.yaml'), 'utf8')), ['schema', 'backend', 'package', 'version', 'build', 'displayname', 'description', 'maintainer', 'metadata', 'os_min_ver', 'privilege', 'arches', 'layout', 'native', 'icon', 'reproducible', 'portal']);
|
|
110
|
+
const m = object(parse(readFileSync(join(root, 'dsm-package.yaml'), 'utf8')), ['schema', 'backend', 'package', 'version', 'build', 'displayname', 'description', 'maintainer', 'metadata', 'os_min_ver', 'privilege', 'arches', 'layout', 'native', 'icon', 'reproducible', 'portal', 'dependencies']);
|
|
110
111
|
if (m.schema !== 2)
|
|
111
112
|
fail('schema 2 required; migrate schema: 1 to schema: 2 and add backend: {kind: native}');
|
|
112
113
|
const backend = object(m.backend, ['kind']);
|
|
113
114
|
if (backend.kind !== 'native' || (m.privilege !== undefined && m.privilege !== 'none'))
|
|
114
115
|
fail('backend.kind: native and package-only privilege required');
|
|
115
116
|
m.privilege = 'none';
|
|
117
|
+
if (m.dependencies !== undefined)
|
|
118
|
+
m.dependencies = readPackageDependencies(m.dependencies);
|
|
116
119
|
if (m.metadata !== undefined)
|
|
117
120
|
m.metadata = validateMetadata(m.metadata);
|
|
118
121
|
if (existsSync(join(root, 'compose.yaml')))
|
|
@@ -330,11 +333,12 @@ export async function buildNativeLayout(options) {
|
|
|
330
333
|
await createReproducibleTar(target, join(stage, 'package.tgz'), { gzip: true });
|
|
331
334
|
rmSync(target, { recursive: true });
|
|
332
335
|
const fields = { package: m.package, version, arch: layoutInfoArch(arch), os_min_ver: m.os_min_ver, displayname: m.displayname, description: m.description, maintainer: m.maintainer, dsmuidir: 'ui', dsmappname: `SYNO.SDS.${m.package}.Instance`, startable: 'yes', thirdparty: 'yes', checksum: createHash('md5').update(readFileSync(join(stage, 'package.tgz'))).digest('hex') };
|
|
333
|
-
const
|
|
336
|
+
const deps = dependencyInfo(m.dependencies ?? { packages: [] });
|
|
337
|
+
const info = portalInfoFields([...Object.entries(fields), ...(deps ? [["install_dep_packages", deps]] : []), ...metadataInfoFields(m.metadata), ...anchorFields(m.layout.anchors ?? {}, files), ...portalFields(m.portal)], Boolean(m.portal));
|
|
334
338
|
if (new Set(info.map(([k]) => k)).size !== info.length)
|
|
335
339
|
fail('INFO anchor collides with an INFO field');
|
|
336
340
|
writeFileSync(join(stage, 'INFO'), info.map(([k, v]) => `${k}="${v.replace(/([\\"$`])/g, '\\$1')}"\n`).join(''));
|
|
337
|
-
writeFileSync(join(stage, 'native-layout.json'), JSON.stringify({ format: 1, arch, binaries: binaries.map(({ path, linkage }) => ({ path, linkage })), files, ...(m.layout.anchors ? { anchors: m.layout.anchors } : {}), ...(m.portal ? { portal: m.portal } : {}) }));
|
|
341
|
+
writeFileSync(join(stage, 'native-layout.json'), JSON.stringify({ format: 1, arch, dependencies: m.dependencies ?? { packages: [] }, binaries: binaries.map(({ path, linkage }) => ({ path, linkage })), files, ...(m.layout.anchors ? { anchors: m.layout.anchors } : {}), ...(m.portal ? { portal: m.portal } : {}) }));
|
|
338
342
|
const output = join(outDir, `${m.package}-${version}-${arch}.spk`);
|
|
339
343
|
await createReproducibleTar(stage, output);
|
|
340
344
|
return output;
|
|
@@ -351,9 +355,15 @@ export function inspectNativeLayout(dir, payload, info) {
|
|
|
351
355
|
try {
|
|
352
356
|
if (!/^[A-Za-z][A-Za-z0-9]{1,63}$/.test(info.package ?? '') || !/^\d+(\.\d+){0,3}-\d+$/.test(info.version ?? '') || !/^7\.\d+(\.\d+)?-\d+$/.test(info.os_min_ver ?? ''))
|
|
353
357
|
fail('invalid native INFO identity/version/DSM');
|
|
354
|
-
const inv = object(JSON.parse(readFileSync(join(dir, 'native-layout.json'), 'utf8')), ['format', 'arch', 'binaries', 'files', 'anchors', 'portal']);
|
|
358
|
+
const inv = object(JSON.parse(readFileSync(join(dir, 'native-layout.json'), 'utf8')), ['format', 'arch', 'binaries', 'files', 'anchors', 'portal', 'dependencies']);
|
|
355
359
|
if (inv.format !== 1 || !Object.hasOwn(LAYOUT_ARCHES, inv.arch) || layoutInfoArch(inv.arch) !== info.arch)
|
|
356
360
|
fail('invalid native layout inventory');
|
|
361
|
+
const errors = [];
|
|
362
|
+
const dependencies = inspectPackageDependencies(inv.dependencies, errors);
|
|
363
|
+
if (errors.length)
|
|
364
|
+
fail(errors.join("; "));
|
|
365
|
+
if ((info.install_dep_packages ?? "") !== dependencyInfo(dependencies ?? { packages: [] }))
|
|
366
|
+
fail("INFO install_dep_packages differs from native layout dependencies");
|
|
357
367
|
checkLayoutMetadata(dir, true, info.package);
|
|
358
368
|
const actual = layoutFiles(payload);
|
|
359
369
|
if (JSON.stringify(actual) !== JSON.stringify(inv.files))
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { StandaloneManifest, StandaloneError } from "./manifest.js";
|
|
2
|
+
export interface PackageDependency {
|
|
3
|
+
name: string;
|
|
4
|
+
min_version?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface PackageDependencies {
|
|
7
|
+
packages: PackageDependency[];
|
|
8
|
+
}
|
|
9
|
+
/** Fixed INFO tokens, never shell expressions or dependency operators. */
|
|
10
|
+
export declare function readPackageDependencies(value: unknown): PackageDependencies | undefined;
|
|
11
|
+
/** Unversioned requirements accept a specified minimum; distinct minima conflict. */
|
|
12
|
+
export declare function mergePackageDependencies(...groups: PackageDependency[][]): PackageDependency[];
|
|
13
|
+
export declare function packageDependencies(manifest: Pick<StandaloneManifest, "backend" | "daemon" | "dependencies">): PackageDependencies;
|
|
14
|
+
export declare function dependencyInfo(dependencies: PackageDependencies): string;
|
|
15
|
+
/** Old build records have no field; new records must already be normalized. */
|
|
16
|
+
export declare function inspectPackageDependencies(value: unknown, findings: string[]): PackageDependencies | undefined;
|
|
17
|
+
export declare function dependencyError(error: unknown): StandaloneError;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
const record = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
3
|
+
const NAME = /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/;
|
|
4
|
+
const VERSION = /^\d+(?:\.\d+){0,3}(?:-\d+)?$/;
|
|
5
|
+
/** Fixed INFO tokens, never shell expressions or dependency operators. */
|
|
6
|
+
export function readPackageDependencies(value) {
|
|
7
|
+
if (value === undefined)
|
|
8
|
+
return undefined;
|
|
9
|
+
if (!record(value) || Object.keys(value).some(k => k !== "packages") || !Array.isArray(value.packages))
|
|
10
|
+
throw new Error("dependencies accepts only packages: an array of {name, min_version?}.");
|
|
11
|
+
const packages = value.packages.map((entry, i) => {
|
|
12
|
+
if (!record(entry) || Object.keys(entry).some(k => k !== "name" && k !== "min_version") ||
|
|
13
|
+
typeof entry.name !== "string" || !NAME.test(entry.name))
|
|
14
|
+
throw new Error(`dependencies.packages[${i}].name must start with a letter and contain only ASCII letters, digits, _, . or - (1-64 characters); only name and min_version are accepted.`);
|
|
15
|
+
if (entry.min_version !== undefined && (typeof entry.min_version !== "string" || entry.min_version.length > 64 || !VERSION.test(entry.min_version)))
|
|
16
|
+
throw new Error(`dependencies.packages[${i}].min_version must be a quoted numeric version with 1-4 components and an optional numeric -build (at most 64 characters).`);
|
|
17
|
+
return { name: entry.name, ...(entry.min_version !== undefined ? { min_version: entry.min_version } : {}) };
|
|
18
|
+
});
|
|
19
|
+
return { packages: mergePackageDependencies(packages) };
|
|
20
|
+
}
|
|
21
|
+
/** Unversioned requirements accept a specified minimum; distinct minima conflict. */
|
|
22
|
+
export function mergePackageDependencies(...groups) {
|
|
23
|
+
const byName = new Map();
|
|
24
|
+
for (const dep of groups.flat()) {
|
|
25
|
+
const prev = byName.get(dep.name);
|
|
26
|
+
if (prev?.min_version !== undefined && dep.min_version !== undefined && prev.min_version !== dep.min_version)
|
|
27
|
+
throw new Error(`Conflicting min_version for package ${dep.name}: ${prev.min_version} and ${dep.min_version}. Use one identical minimum version.`);
|
|
28
|
+
byName.set(dep.name, { name: dep.name, ...(prev?.min_version !== undefined ? { min_version: prev.min_version } : dep.min_version !== undefined ? { min_version: dep.min_version } : {}) });
|
|
29
|
+
}
|
|
30
|
+
return [...byName.values()].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
31
|
+
}
|
|
32
|
+
export function packageDependencies(manifest) {
|
|
33
|
+
const node = "node" in manifest.backend ? manifest.backend.node : undefined;
|
|
34
|
+
const daemonNode = manifest.daemon?.node;
|
|
35
|
+
return { packages: mergePackageDependencies(readPackageDependencies(manifest.dependencies)?.packages ?? [], node ? [{ name: node.package }] : [], daemonNode ? [{ name: daemonNode.package, ...(daemonNode.min_version ? { min_version: daemonNode.min_version } : {}) }] : []) };
|
|
36
|
+
}
|
|
37
|
+
export function dependencyInfo(dependencies) {
|
|
38
|
+
return dependencies.packages.map(d => d.name + (d.min_version === undefined ? "" : `>=${d.min_version}`)).join(":");
|
|
39
|
+
}
|
|
40
|
+
/** Old build records have no field; new records must already be normalized. */
|
|
41
|
+
export function inspectPackageDependencies(value, findings) {
|
|
42
|
+
if (value === undefined)
|
|
43
|
+
return undefined;
|
|
44
|
+
try {
|
|
45
|
+
const normalized = readPackageDependencies(value);
|
|
46
|
+
if (!isDeepStrictEqual(normalized, value))
|
|
47
|
+
throw new Error("dependencies must be sorted, deduplicated and normalized.");
|
|
48
|
+
return normalized;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
findings.push(`DEPENDENCIES_INVALID build metadata dependencies: ${error.message}`);
|
|
52
|
+
return { packages: [] };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function dependencyError(error) {
|
|
56
|
+
return { code: "STANDALONE_DEPENDENCIES_INVALID", message: error.message };
|
|
57
|
+
}
|
package/dist/standalone/spk.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { packageDependencies, dependencyInfo } from "./package-dependencies.js";
|
|
1
2
|
import { settingsInstallSteps, settingsConfig } from './install-settings.js';
|
|
2
3
|
import { backupConfig, backupHookProblem, backupRecord, renderBackupScripts, BACKUP_PAYLOAD_DIR, BACKUP_SCRIPTS_DIR, BACKUP_SCRIPT_MODES } from './hyper-backup.js';
|
|
3
4
|
import { nativeGatewaySupported, nativeGatewaySdk, renderNativeGatewayConfig, NATIVE_GATEWAY_CONFIG, legacyGatewayNotice, gatewayEntryPath } from './native-gateway.js';
|
|
@@ -31,7 +32,7 @@ import { copyAppTree } from "./bridge-app.js";
|
|
|
31
32
|
import { isBridgeBackend } from "./bridge-backend.js";
|
|
32
33
|
import { hostScriptName, renderHostAdapterScript } from "./host-adapter.js";
|
|
33
34
|
import { writeComposePayload } from "./compose.js";
|
|
34
|
-
import { copyDaemonPayload, daemonSdkVersion
|
|
35
|
+
import { copyDaemonPayload, daemonSdkVersion } from "./daemon.js";
|
|
35
36
|
import { DAEMON_INGRESS_CONFIG, renderDaemonIngressConfig } from "./daemon-ingress.js";
|
|
36
37
|
import { gatewayCgiPaths, gatewayExternalAccess, GATEWAY_CONFIG_PATH, GATEWAY_EXEC, identityGatewayConfig, identityGatewayDeclaration, } from "./identity-gateway.js";
|
|
37
38
|
import { BACKGROUND_UNIT_DIR, BACKGROUND_WORKER_PAYLOAD, backgroundUnitName, renderBackgroundUnit, BRIDGE_WORKER_ARGS, } from "./background.js";
|
|
@@ -244,6 +245,7 @@ export async function assembleStandaloneSpk(options) {
|
|
|
244
245
|
.digest("hex");
|
|
245
246
|
writeFile(join(staging, "sdk-package.json"), JSON.stringify({
|
|
246
247
|
schema: 2,
|
|
248
|
+
dependencies: packageDependencies(manifest),
|
|
247
249
|
...(moduleVersion ? { ui_module: moduleVersion } : {}),
|
|
248
250
|
...(manifest.installSettings ? { installSettings: manifest.installSettings } : {}),
|
|
249
251
|
...(backup ? { hyperBackup: backupRecord(backup) } : {}),
|
|
@@ -389,11 +391,9 @@ export function renderInfo(manifest, arch, checksum, executorDigest) {
|
|
|
389
391
|
"adminurl",
|
|
390
392
|
gatewayEntryPath(manifest),
|
|
391
393
|
]);
|
|
392
|
-
const
|
|
393
|
-
if (
|
|
394
|
-
fields.push(["install_dep_packages",
|
|
395
|
-
if (manifest.daemon?.node)
|
|
396
|
-
fields.push(["install_dep_packages", nodeDependency(manifest.daemon.node)]);
|
|
394
|
+
const dependencies = dependencyInfo(packageDependencies(manifest));
|
|
395
|
+
if (dependencies)
|
|
396
|
+
fields.push(["install_dep_packages", dependencies]);
|
|
397
397
|
fields.push(...metadataInfoFields(manifest.metadata));
|
|
398
398
|
// Background packages need prestart: it runs before DSM's systemd-user-unit acquire.
|
|
399
399
|
if (manifest.daemon || manifest.compose || manifest.capabilities.jobs)
|
package/package.json
CHANGED
package/skills/SKILL.md
CHANGED
|
@@ -99,6 +99,23 @@ Never install, uninstall, publish or modify NAS data without task authorization.
|
|
|
99
99
|
|
|
100
100
|
Native projects use schema 2 capabilities and `--with files,jobs,login,root-ops,delegate`; use `add` for existing projects. Prefer `check --json`, `build --json`, `inspect <spk> --json`, `deploy --dry-run --json`, and `status --host <profile> --json` for structured results. Read the conflict policy in reference/create.md before adding overlays.
|
|
101
101
|
|
|
102
|
+
For DSM Package Center dependencies independent of the App language, declare
|
|
103
|
+
`dependencies: {packages: [{name: Node.js_v22, min_version: "22.19.0"}]}` in
|
|
104
|
+
`dsm-package.yaml`; `min_version` is optional. Names are 1-64 ASCII letters,
|
|
105
|
+
digits, `_`, `.` or `-`, starting with a letter. Versions are quoted numeric
|
|
106
|
+
strings with 1-4 dotted components and optional numeric `-build` (64 characters
|
|
107
|
+
maximum); other keys and unsafe INFO syntax fail validation. Requirements
|
|
108
|
+
merge with a Node backend or `daemon.node`, sort and deduplicate; two distinct
|
|
109
|
+
specified minimum versions for one package fail. `check --json` reports them;
|
|
110
|
+
build records them and `inspect` verifies exact agreement with INFO. This
|
|
111
|
+
works with native layouts and Compose too (host packages are not container
|
|
112
|
+
mounts). It leaves daemon exec/args/env/supervision unchanged. Use
|
|
113
|
+
`daemon.node` only for a JavaScript daemon: it replaces exec with the Node
|
|
114
|
+
launcher. For a Go daemon spawning Node children, keep the Go exec and use
|
|
115
|
+
`dependencies.packages`; call `/var/packages/Node.js_v22/target/usr/local/bin/node`.
|
|
116
|
+
Any dependency's target is `/var/packages/<name>/target`; no env vars or PATH
|
|
117
|
+
entries are injected, and the declaration grants no private-data access.
|
|
118
|
+
|
|
102
119
|
Read reference/portal.md before declaring or changing a DSM Application Portal alias (`add portal`); DSM owns the saved setting; DSM 7.2-64570+ supports schema-2 Portal without an executor, while DSM 7.1 requires executor registration. Read reference/files.md and reference/identity.md for capability boundaries. RootOps and delegate share one-time protected executor boot/control setup with digest approval. Existing native products can explicitly use the separate native runtimes in @synopackageland/sdk/native; do not mix its tasks with generated executor capabilities. App sessions alone do not grant NAS file authority.
|
|
103
120
|
|
|
104
121
|
Read [reference/lifecycle.md](reference/lifecycle.md) before editing `dsm-scripts/` (preinst, postinst, preupgrade, postupgrade, preuninst, postuninst, start-stop-status): the SDK runs them inside its generated scripts as the package account; never add root work or repeat SDK steps there.
|