@synopackageland/cli 0.9.6 → 0.10.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 +144 -5
- package/README.md +5 -0
- package/dist/add.js +7 -8
- package/dist/build-spk/dsm-arch.js +1 -1
- package/dist/build-spk/dsm-icons.d.ts +8 -1
- package/dist/build-spk/dsm-icons.js +26 -12
- package/dist/build-spk/dsm-version.d.ts +1 -1
- package/dist/build-spk/dsm-version.js +4 -4
- package/dist/build-spk/gateway.js +2 -2
- package/dist/build-spk/generator.js +1 -1
- package/dist/build-spk/png.js +16 -16
- package/dist/capabilities.js +1 -1
- package/dist/capability-overlays.d.ts +16 -5
- package/dist/capability-overlays.js +30 -16
- package/dist/cli.js +107 -107
- package/dist/compose/analyze.js +23 -23
- package/dist/compose/privilege.js +12 -12
- package/dist/contract/i18n.js +3 -3
- package/dist/contract/icon.js +2 -2
- package/dist/contract/posix-all-shares.js +2 -2
- package/dist/contract/schema-validate.js +3 -3
- package/dist/contract/validate.js +16 -16
- package/dist/contract/web-launch.d.ts +2 -1
- package/dist/contract/web-launch.js +3 -2
- package/dist/deploy/deploy.js +8 -8
- package/dist/deploy/homes-values.js +1 -1
- package/dist/deploy/identity-shared.js +1 -1
- package/dist/deploy/native-install-values.js +1 -1
- package/dist/deploy/param-values.js +4 -4
- package/dist/deploy/plan.d.ts +1 -1
- package/dist/deploy/port-preflight.js +3 -3
- package/dist/deploy/run-as-values.js +2 -2
- package/dist/deploy/share-permission-preflight.js +6 -6
- package/dist/deploy/share-values.js +3 -3
- package/dist/error-registry.js +369 -346
- package/dist/errors.js +1 -1
- package/dist/host/dsm-port-profile.d.ts +1 -1
- package/dist/host/dsm-port-profile.js +1 -1
- package/dist/info.js +11 -11
- package/dist/init.d.ts +2 -0
- package/dist/init.js +85 -38
- package/dist/inspect.js +5 -5
- package/dist/portal/alias.d.ts +1 -1
- package/dist/portal/alias.js +4 -4
- package/dist/runtime/executor-build.js +12 -1
- package/dist/runtime/executor-digests.json +8 -85
- package/dist/runtime/executor-inspect.js +11 -4
- package/dist/runtime/executor-policy.d.ts +9 -2
- package/dist/runtime/executor-policy.js +27 -8
- package/dist/runtime/root-programs.d.ts +47 -0
- package/dist/runtime/root-programs.js +153 -0
- package/dist/runtime/sdk-source.d.ts +5 -4
- package/dist/runtime/sdk-source.js +8 -4
- package/dist/skill.js +14 -14
- package/dist/spk/check-info.js +17 -17
- package/dist/spk/check-metadata.js +18 -18
- package/dist/spk/check-payload.js +10 -10
- package/dist/spk/rules.d.ts +57 -57
- package/dist/spk/rules.js +50 -50
- package/dist/spk/validate-spk.js +5 -5
- package/dist/standalone/api-permissions.js +1 -1
- package/dist/standalone/background.d.ts +3 -1
- package/dist/standalone/background.js +8 -4
- package/dist/standalone/binary.js +7 -7
- package/dist/standalone/bridge-app.d.ts +23 -0
- package/dist/standalone/bridge-app.js +141 -0
- package/dist/standalone/bridge-backend.d.ts +40 -0
- package/dist/standalone/bridge-backend.js +95 -0
- package/dist/standalone/bridge-build.d.ts +21 -0
- package/dist/standalone/bridge-build.js +62 -0
- package/dist/standalone/bridge-json.d.ts +16 -0
- package/dist/standalone/bridge-json.js +59 -0
- package/dist/standalone/bridge-runtime.d.ts +16 -0
- package/dist/standalone/bridge-runtime.js +46 -0
- package/dist/standalone/bridge-rust.d.ts +16 -0
- package/dist/standalone/bridge-rust.js +89 -0
- package/dist/standalone/build.js +18 -14
- package/dist/standalone/daemon.js +6 -6
- package/dist/standalone/dsm-window.d.ts +10 -5
- package/dist/standalone/dsm-window.js +64 -16
- package/dist/standalone/i18n.js +18 -18
- package/dist/standalone/inspect-bridge.d.ts +31 -0
- package/dist/standalone/inspect-bridge.js +111 -0
- package/dist/standalone/inspect-spk.d.ts +1 -1
- package/dist/standalone/inspect-spk.js +77 -69
- package/dist/standalone/lifecycle.js +22 -3
- package/dist/standalone/manifest.d.ts +17 -9
- package/dist/standalone/manifest.js +143 -65
- package/dist/standalone/portal-window.js +14 -7
- package/dist/standalone/request-timeout.d.ts +9 -0
- package/dist/standalone/request-timeout.js +13 -0
- package/dist/standalone/root-ops.d.ts +23 -2
- package/dist/standalone/root-ops.js +118 -13
- package/dist/standalone/sdk-check.js +4 -4
- package/dist/standalone/shared-folder.js +7 -7
- package/dist/standalone/spk.d.ts +2 -0
- package/dist/standalone/spk.js +30 -11
- package/dist/standalone/uninstall-hook.js +22 -4
- package/dist/standalone/uninstall.js +7 -7
- package/dist/standalone/window-size.d.ts +17 -0
- package/dist/standalone/window-size.js +31 -0
- package/dist/types.d.ts +1 -1
- package/dist/validate.js +13 -13
- package/package.json +2 -2
- package/schema/bridge-manifest.v1.schema.json +86 -0
- package/skills/reference/create.md +76 -2
- package/skills/reference/ui-json.md +2 -1
- package/skills/reference/ui-vue.md +9 -3
- package/skills/reference/wrap.md +1 -1
- package/dist/standalone/rust-build.d.ts +0 -35
- package/dist/standalone/rust-build.js +0 -127
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import type { StandaloneError } from "./manifest.js";
|
|
2
|
-
|
|
2
|
+
import { type NativeArchFamily } from "../build-spk/dsm-arch.js";
|
|
3
|
+
import { type RootSettings } from "../runtime/root-programs.js";
|
|
4
|
+
/** Fixed root programs, run only by the executor: a script, or a binary per architecture. */
|
|
3
5
|
export interface RootOp {
|
|
4
6
|
name: string;
|
|
5
|
-
|
|
7
|
+
/** root/<name>.sh */
|
|
8
|
+
body?: string;
|
|
9
|
+
/** root/<arch>/<name>, declared `kind: binary` in root.programs. */
|
|
10
|
+
binary?: Partial<Record<NativeArchFamily, {
|
|
11
|
+
path: string;
|
|
12
|
+
digest: string;
|
|
13
|
+
}>>;
|
|
6
14
|
}
|
|
7
15
|
export type RootOpsResult = {
|
|
8
16
|
ok: true;
|
|
@@ -15,3 +23,16 @@ export declare const ROOT_OPS_DIR = "root";
|
|
|
15
23
|
/** Shell that tries to become root: sudo, chown root or setuid/setgid modes. */
|
|
16
24
|
export declare const ROOT_SCRIPT_HINTS: RegExp[];
|
|
17
25
|
export declare function loadRootOps(dir: string): RootOpsResult;
|
|
26
|
+
/** Every binary root program is declared kind: binary and built for each architecture the package ships. */
|
|
27
|
+
export declare function rootBinaryProblem(ops: readonly RootOp[], settings: RootSettings | undefined, arches: readonly NativeArchFamily[]): string | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* dsm-package.yaml `root`: `programs.<name>` gives a root/ program its kind,
|
|
30
|
+
* params schema and run or job limits; `stop` names the program the executor
|
|
31
|
+
* runs at package stop and upgrade; `state: true` gives root programs a
|
|
32
|
+
* directory only root can read. Returns why the settings are refused.
|
|
33
|
+
*/
|
|
34
|
+
export declare function readRootSettings(value: unknown, ops: readonly RootOp[] | undefined): {
|
|
35
|
+
settings: RootSettings;
|
|
36
|
+
} | {
|
|
37
|
+
error: string;
|
|
38
|
+
};
|
|
@@ -1,46 +1,151 @@
|
|
|
1
1
|
import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { isNativeArchFamily } from "../build-spk/dsm-arch.js";
|
|
5
|
+
import { ELF_MACHINE } from "./binary.js";
|
|
6
|
+
import { rootLimitsProblem, rootSchemaProblem } from "../runtime/root-programs.js";
|
|
3
7
|
export const ROOT_OPS_DIR = "root";
|
|
4
8
|
/** Shell that tries to become root: sudo, chown root or setuid/setgid modes. */
|
|
5
9
|
export const ROOT_SCRIPT_HINTS = [/\bsudo\b/, /\bchown\s+(-\w+\s+)*root\b/, /chmod\s+[ugo]*\+s\b/, /chmod\s+[2467][0-7]{3}\b/];
|
|
6
10
|
const MAX_ROOT_OP_BYTES = 16 * 1024;
|
|
11
|
+
const MAX_ROOT_BINARY_BYTES = 64 * 1024 * 1024;
|
|
12
|
+
const ELF_MAGIC = Buffer.from([0x7f, 0x45, 0x4c, 0x46]);
|
|
7
13
|
const OP_FILE = /^([a-z][a-z0-9_]{0,31})\.sh$/;
|
|
8
14
|
export function loadRootOps(dir) {
|
|
9
15
|
const names = existsSync(dir) ? readdirSync(dir).sort() : [];
|
|
10
16
|
if (names.length === 0) {
|
|
11
17
|
return {
|
|
12
18
|
ok: false,
|
|
13
|
-
errors: [{ code: "STANDALONE_ROOT_OPS_MISSING", message: `capabilities.rootOps: true
|
|
19
|
+
errors: [{ code: "STANDALONE_ROOT_OPS_MISSING", message: `capabilities.rootOps: true needs at least one root operation at ${ROOT_OPS_DIR}/<name>.sh.` }],
|
|
14
20
|
};
|
|
15
21
|
}
|
|
16
22
|
const errors = [];
|
|
17
|
-
const
|
|
23
|
+
const invalid = (file, error) => errors.push({ code: "STANDALONE_ROOT_OP_INVALID", message: `${ROOT_OPS_DIR}/${file}: ${error}` });
|
|
24
|
+
const ops = new Map();
|
|
18
25
|
for (const file of names) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
if (isNativeArchFamily(file) && lstatSync(join(dir, file)).isDirectory()) {
|
|
27
|
+
for (const name of readdirSync(join(dir, file)).sort()) {
|
|
28
|
+
const binary = readRootBinary(join(dir, file, name), name, file);
|
|
29
|
+
if ("error" in binary)
|
|
30
|
+
invalid(`${file}/${name}`, binary.error);
|
|
31
|
+
else if (ops.get(name)?.body !== undefined)
|
|
32
|
+
invalid(`${file}/${name}`, `has the same name as ${ROOT_OPS_DIR}/${name}.sh.`);
|
|
33
|
+
else
|
|
34
|
+
ops.set(name, { name, binary: { ...ops.get(name)?.binary, [file]: binary } });
|
|
35
|
+
}
|
|
36
|
+
continue;
|
|
25
37
|
}
|
|
38
|
+
const op = readRootOp(join(dir, file), file);
|
|
39
|
+
if ("error" in op)
|
|
40
|
+
invalid(file, op.error);
|
|
41
|
+
else if (ops.has(op.name))
|
|
42
|
+
invalid(file, `has the same name as ${ROOT_OPS_DIR}/<arch>/${op.name}.`);
|
|
43
|
+
else
|
|
44
|
+
ops.set(op.name, op);
|
|
45
|
+
}
|
|
46
|
+
const sorted = [...ops.values()].sort((a, b) => a.name < b.name ? -1 : 1);
|
|
47
|
+
return errors.length === 0 ? { ok: true, ops: sorted } : { ok: false, errors };
|
|
48
|
+
}
|
|
49
|
+
/** root/<arch>/<name>: an ELF executable for that architecture. */
|
|
50
|
+
function readRootBinary(path, name, arch) {
|
|
51
|
+
if (!/^[a-z][a-z0-9_]{0,31}$/.test(name))
|
|
52
|
+
return { error: "A binary file name must consist of lowercase alphanumerics and underscores (at most 32 characters)." };
|
|
53
|
+
const stat = lstatSync(path);
|
|
54
|
+
if (!stat.isFile())
|
|
55
|
+
return { error: "It must be a regular file (not a symlink or directory)." };
|
|
56
|
+
if (stat.size > MAX_ROOT_BINARY_BYTES)
|
|
57
|
+
return { error: `It must not exceed ${MAX_ROOT_BINARY_BYTES} bytes.` };
|
|
58
|
+
const bytes = readFileSync(path);
|
|
59
|
+
if (bytes.length < 20 || !bytes.subarray(0, 4).equals(ELF_MAGIC) || bytes.readUInt16LE(18) !== ELF_MACHINE[arch])
|
|
60
|
+
return { error: `It must be an ${arch} ELF executable.` };
|
|
61
|
+
return { path, digest: createHash("sha256").update(bytes).digest("hex") };
|
|
62
|
+
}
|
|
63
|
+
/** Every binary root program is declared kind: binary and built for each architecture the package ships. */
|
|
64
|
+
export function rootBinaryProblem(ops, settings, arches) {
|
|
65
|
+
for (const op of ops) {
|
|
66
|
+
if (!op.binary)
|
|
67
|
+
continue;
|
|
68
|
+
if (settings?.programs?.[op.name]?.kind !== "binary")
|
|
69
|
+
return `root: ${ROOT_OPS_DIR}/<arch>/${op.name} needs root.programs.${op.name}.kind: binary.`;
|
|
70
|
+
const missing = arches.filter(arch => !op.binary[arch]);
|
|
71
|
+
if (missing.length)
|
|
72
|
+
return `root: binary program ${op.name} is missing ${missing.map(arch => `${ROOT_OPS_DIR}/${arch}/${op.name}`).join(", ")}.`;
|
|
26
73
|
}
|
|
27
|
-
return
|
|
74
|
+
return undefined;
|
|
28
75
|
}
|
|
29
76
|
function readRootOp(path, file) {
|
|
30
77
|
const name = OP_FILE.exec(file)?.[1];
|
|
31
78
|
if (!name) {
|
|
32
|
-
return { error: "
|
|
79
|
+
return { error: "The file name must be <name>.sh with lowercase alphanumerics and underscores (at most 32 characters); this directory holds nothing else." };
|
|
33
80
|
}
|
|
34
81
|
const stat = lstatSync(path);
|
|
35
82
|
if (!stat.isFile()) {
|
|
36
|
-
return { error: "
|
|
83
|
+
return { error: "It must be a regular file (not a symlink or directory)." };
|
|
37
84
|
}
|
|
38
85
|
if (stat.size > MAX_ROOT_OP_BYTES) {
|
|
39
|
-
return { error:
|
|
86
|
+
return { error: `It must not exceed ${MAX_ROOT_OP_BYTES} bytes.` };
|
|
40
87
|
}
|
|
41
88
|
const body = readFileSync(path, "utf8");
|
|
42
89
|
if (!body.startsWith("#!/bin/sh\n") || body.includes("\0") || body.includes("�")) {
|
|
43
|
-
return { error: "
|
|
90
|
+
return { error: "It must be a UTF-8 text script starting with #!/bin/sh." };
|
|
44
91
|
}
|
|
45
92
|
return { name, body };
|
|
46
93
|
}
|
|
94
|
+
const ROOT_SETTINGS_FIELDS = ["programs", "stop", "state"];
|
|
95
|
+
const PROGRAM_FIELDS = ["kind", "schema", "run", "job"];
|
|
96
|
+
/**
|
|
97
|
+
* dsm-package.yaml `root`: `programs.<name>` gives a root/ program its kind,
|
|
98
|
+
* params schema and run or job limits; `stop` names the program the executor
|
|
99
|
+
* runs at package stop and upgrade; `state: true` gives root programs a
|
|
100
|
+
* directory only root can read. Returns why the settings are refused.
|
|
101
|
+
*/
|
|
102
|
+
export function readRootSettings(value, ops) {
|
|
103
|
+
const fail = (why) => ({ error: `root: ${why}` });
|
|
104
|
+
if (!ops)
|
|
105
|
+
return fail("capabilities.rootOps: true is required.");
|
|
106
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
107
|
+
return fail(`Only ${ROOT_SETTINGS_FIELDS.join(", ")} are accepted.`);
|
|
108
|
+
const raw = value;
|
|
109
|
+
const other = Object.keys(raw).find(key => !ROOT_SETTINGS_FIELDS.includes(key));
|
|
110
|
+
if (other)
|
|
111
|
+
return fail(`Unsupported ${other}; only ${ROOT_SETTINGS_FIELDS.join(", ")} are accepted (the uninstall root program is written in uninstall.purge.root).`);
|
|
112
|
+
const names = new Set(ops.map(op => op.name));
|
|
113
|
+
const programs = raw.programs ?? {};
|
|
114
|
+
if (typeof programs !== "object" || programs === null || Array.isArray(programs))
|
|
115
|
+
return fail("programs is <name>: {kind, schema, run, job}.");
|
|
116
|
+
for (const [name, setting] of Object.entries(programs)) {
|
|
117
|
+
const problem = programProblem(name, setting, ops.find(op => op.name === name));
|
|
118
|
+
if (problem)
|
|
119
|
+
return fail(`programs.${name}: ${problem}`);
|
|
120
|
+
}
|
|
121
|
+
if (raw.stop !== undefined && (typeof raw.stop !== "string" || !names.has(raw.stop)))
|
|
122
|
+
return fail(`stop must be a program name in root/, not ${JSON.stringify(raw.stop)}.`);
|
|
123
|
+
if (raw.state !== undefined && raw.state !== true)
|
|
124
|
+
return fail("state can only be true (the state directory of root programs).");
|
|
125
|
+
return { settings: raw };
|
|
126
|
+
}
|
|
127
|
+
function programProblem(name, value, op) {
|
|
128
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
129
|
+
return "It must be {kind, schema, run, job}.";
|
|
130
|
+
const setting = value;
|
|
131
|
+
const other = Object.keys(setting).find(key => !PROGRAM_FIELDS.includes(key));
|
|
132
|
+
if (other)
|
|
133
|
+
return `Unsupported ${other}; only ${PROGRAM_FIELDS.join(", ")} are accepted.`;
|
|
134
|
+
const kind = setting.kind ?? "script";
|
|
135
|
+
if (kind !== "script" && kind !== "binary")
|
|
136
|
+
return "kind is script or binary.";
|
|
137
|
+
if (!op || (kind === "script") !== (op.body !== undefined))
|
|
138
|
+
return `There is no matching ${kind === "script" ? `${ROOT_OPS_DIR}/${name}.sh` : `${ROOT_OPS_DIR}/<arch>/${name}`}.`;
|
|
139
|
+
if (setting.schema !== undefined) {
|
|
140
|
+
const problem = typeof setting.schema === "object" && setting.schema !== null && setting.schema.type === "object"
|
|
141
|
+
? rootSchemaProblem(setting.schema) : "schema must be a JSON Schema with type: object";
|
|
142
|
+
if (problem)
|
|
143
|
+
return `schema: ${problem}.`;
|
|
144
|
+
}
|
|
145
|
+
for (const which of ["run", "job"]) {
|
|
146
|
+
const problem = setting[which] === undefined ? undefined : rootLimitsProblem(setting[which], which);
|
|
147
|
+
if (problem)
|
|
148
|
+
return `${problem}.`;
|
|
149
|
+
}
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
@@ -4,7 +4,7 @@ import { CLI_VERSION } from "../cli-version.js";
|
|
|
4
4
|
import { appSdkVersion, SDK_DEPENDENCY, SDK_PACKAGE } from "../runtime/sdk-source.js";
|
|
5
5
|
const minorLine = (version) => version.split(".").slice(0, 2).join(".");
|
|
6
6
|
/** The file that names each backend kind's SDK version. */
|
|
7
|
-
const SDK_VERSION_FILE = { go: "backend/go.mod", rust: "
|
|
7
|
+
const SDK_VERSION_FILE = { go: "backend/go.mod", rust: "Cargo.lock", python: "backend.sdk", node: "backend.sdk" };
|
|
8
8
|
/**
|
|
9
9
|
* The backend's dependency files name the App's SDK version; the frontend
|
|
10
10
|
* package and this CLI must agree with it. `needsSdk`: the App declares
|
|
@@ -22,16 +22,16 @@ export function sdkProblems(backend, frontendDir, needsSdk, cliVersion = CLI_VER
|
|
|
22
22
|
throw error;
|
|
23
23
|
}
|
|
24
24
|
if (!version) {
|
|
25
|
-
return needsSdk ? [{ code: "SDK_REQUIRED", message: `capabilities.delegate
|
|
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
26
|
}
|
|
27
27
|
const file = SDK_VERSION_FILE[backend.kind];
|
|
28
28
|
const problems = [];
|
|
29
29
|
if (minorLine(version) !== minorLine(cliVersion)) {
|
|
30
|
-
problems.push({ code: "SDK_UNSUPPORTED", message: `${file}
|
|
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
31
|
}
|
|
32
32
|
const frontend = frontendDir === undefined ? undefined : frontendSdkVersion(frontendDir);
|
|
33
33
|
if (frontend && frontend.version !== version) {
|
|
34
|
-
problems.push({ code: "SDK_VERSION_MISMATCH", message: `frontend/package.json
|
|
34
|
+
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.` });
|
|
35
35
|
}
|
|
36
36
|
return problems;
|
|
37
37
|
}
|
|
@@ -14,25 +14,25 @@ export function readSharedFolder(value, fail) {
|
|
|
14
14
|
return undefined;
|
|
15
15
|
};
|
|
16
16
|
if (!isRecord(value) || Object.keys(value).some((key) => key !== "sharedFolder")) {
|
|
17
|
-
return refuse("
|
|
17
|
+
return refuse(": only sharedFolder is allowed.");
|
|
18
18
|
}
|
|
19
19
|
const folder = value.sharedFolder;
|
|
20
20
|
if (!isRecord(folder) || Object.keys(folder).some((key) => !FIELDS.has(key))) {
|
|
21
|
-
return refuse("
|
|
21
|
+
return refuse(" accepts only mode, key, name and permission.");
|
|
22
22
|
}
|
|
23
23
|
const { mode, key, name, permission } = folder;
|
|
24
24
|
if (mode !== "select" && mode !== "create")
|
|
25
|
-
return refuse("
|
|
25
|
+
return refuse(" mode must be select or create.");
|
|
26
26
|
if (permission !== "ro" && permission !== "rw")
|
|
27
|
-
return refuse("
|
|
27
|
+
return refuse(" permission must be ro or rw.");
|
|
28
28
|
if (typeof key !== "string" || !KEY.test(key) || RESERVED_KEY.test(key)) {
|
|
29
|
-
return refuse("
|
|
29
|
+
return refuse(" key must start with a lowercase letter, contain only lowercase alphanumerics and underscores (at most 32 characters), and must not start with pkgwizard_, synopkg or change_.");
|
|
30
30
|
}
|
|
31
31
|
if (mode === "select") {
|
|
32
|
-
return name === undefined ? { mode, key, permission } : refuse("
|
|
32
|
+
return name === undefined ? { mode, key, permission } : refuse(" name applies only to create; select is chosen by the install wizard.");
|
|
33
33
|
}
|
|
34
34
|
if (typeof name !== "string" || !SHARE_NAME.test(name) || SYSTEM_SHARE.test(name)) {
|
|
35
|
-
return refuse("create
|
|
35
|
+
return refuse("create needs name: start with an alphanumeric, contain only alphanumerics, underscores and hyphens (at most 32 characters), and not be a DSM system shared folder name.");
|
|
36
36
|
}
|
|
37
37
|
return { mode, key, name, permission };
|
|
38
38
|
}
|
package/dist/standalone/spk.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface AssembleOptions {
|
|
|
16
16
|
daemonControl?: string;
|
|
17
17
|
/** The compiled background worker, required exactly when capabilities.jobs is enabled. */
|
|
18
18
|
backgroundWorker?: string;
|
|
19
|
+
/** A Python, Node or Rust App's finished directory, installed as target/app. */
|
|
20
|
+
app?: string;
|
|
19
21
|
}
|
|
20
22
|
/**
|
|
21
23
|
* Packs an unsigned Standalone Package SPK. DSM runs installation/lifecycle
|
package/dist/standalone/spk.js
CHANGED
|
@@ -17,9 +17,11 @@ import { DSM_LANGS, LOCALES } from "./i18n.js";
|
|
|
17
17
|
import { appTitle, CGI_PAYLOAD_PATH, renderUiConfig, renderWindowScript, windowScriptName } from "./dsm-window.js";
|
|
18
18
|
import { writeUiTexts } from "../build-spk/ui-texts.js";
|
|
19
19
|
import { copyStandaloneNotices } from "../build-spk/notices.js";
|
|
20
|
+
import { copyAppTree } from "./bridge-app.js";
|
|
21
|
+
import { isBridgeBackend } from "./bridge-backend.js";
|
|
20
22
|
import { hostScriptName, renderHostAdapterScript } from "./host-adapter.js";
|
|
21
23
|
import { copyDaemonPayload } from "./daemon.js";
|
|
22
|
-
import { BACKGROUND_UNIT_DIR, BACKGROUND_WORKER_PAYLOAD, backgroundUnitName, renderBackgroundUnit } from "./background.js";
|
|
24
|
+
import { BACKGROUND_UNIT_DIR, BACKGROUND_WORKER_PAYLOAD, backgroundUnitName, renderBackgroundUnit, BRIDGE_WORKER_ARGS } from "./background.js";
|
|
23
25
|
import { renderStandaloneResource } from "./resource.js";
|
|
24
26
|
import { nativePortal } from "./portal.js";
|
|
25
27
|
import { portalByExecutor } from "../build-spk/dsm-version.js";
|
|
@@ -63,8 +65,8 @@ export async function assembleStandaloneSpk(options) {
|
|
|
63
65
|
const appDir = join(uiRoot, "app");
|
|
64
66
|
copyUiTree(manifest.ui.dir, appDir);
|
|
65
67
|
const assetVersion = digestTree(appDir).slice(0, 12);
|
|
66
|
-
writeFile(join(uiRoot, "config"), renderUiConfig(manifest));
|
|
67
|
-
writeFile(join(uiRoot, windowScriptName(manifest)), renderWindowScript(manifest, assetVersion));
|
|
68
|
+
writeFile(join(uiRoot, "config"), renderUiConfig(manifest, icons));
|
|
69
|
+
writeFile(join(uiRoot, windowScriptName(manifest)), renderWindowScript(manifest, assetVersion, icons));
|
|
68
70
|
writeFile(join(uiRoot, hostScriptName(manifest.build)), renderHostAdapterScript(manifest.package));
|
|
69
71
|
writeDsmWindowIcons(uiRoot, icons);
|
|
70
72
|
writeUiTexts(uiRoot, appTitle(manifest));
|
|
@@ -86,9 +88,14 @@ export async function assembleStandaloneSpk(options) {
|
|
|
86
88
|
chmodSync(worker, EXEC_MODE);
|
|
87
89
|
}
|
|
88
90
|
const backend = manifest.backend;
|
|
91
|
+
if (isBridgeBackend(backend)) {
|
|
92
|
+
if (!options.app)
|
|
93
|
+
throw new StandaloneBuildError("STANDALONE_BRIDGE_APP_INVALID", "App directory required.");
|
|
94
|
+
copyAppTree(options.app, join(staging, "package", "app"));
|
|
95
|
+
}
|
|
89
96
|
// A daemon is a Go program of the project's backend directory.
|
|
90
97
|
const sdk = backend.kind !== 'binary' ? backend : manifest.daemon ? { kind: 'go', dir: join(manifest.root, 'backend') } : undefined;
|
|
91
|
-
copyStandaloneNotices(manifest.root, manifest.ui.dir, join(staging, 'package'), { go: backend.kind
|
|
98
|
+
copyStandaloneNotices(manifest.root, manifest.ui.dir, join(staging, 'package'), { go: backend.kind !== 'binary' || Boolean(manifest.daemon), rust: backend.kind === 'rust', sdk });
|
|
92
99
|
if (manifest.daemon)
|
|
93
100
|
copyFileSync(new URL('../../assets/licenses/TEMPLATE.txt', import.meta.url), join(staging, 'package', 'LICENSES', 'DAEMON-LICENSE.txt'));
|
|
94
101
|
const portal = nativePortal(manifest);
|
|
@@ -105,8 +112,12 @@ export async function assembleStandaloneSpk(options) {
|
|
|
105
112
|
chmodSync(join(executorDir, "lifecycle"), EXEC_MODE);
|
|
106
113
|
writeFile(join(executorDir, "policy.json"), options.executor.policyText);
|
|
107
114
|
writeFile(join(executorDir, "setup.json"), JSON.stringify(executorPlan(options.executor)));
|
|
108
|
-
for (const op of manifest.rootOps ?? [])
|
|
109
|
-
|
|
115
|
+
for (const op of manifest.rootOps ?? []) {
|
|
116
|
+
if (op.body !== undefined)
|
|
117
|
+
writeFile(join(staging, "package", "root", op.name + ".sh"), op.body);
|
|
118
|
+
else
|
|
119
|
+
copyExecutable(op.binary[options.arch].path, join(staging, "package", "root", op.name));
|
|
120
|
+
}
|
|
110
121
|
}
|
|
111
122
|
const hook = manifest.uninstall && uninstallExecProblem(join(staging, "package"), manifest.uninstall.purge);
|
|
112
123
|
if (hook)
|
|
@@ -124,7 +135,7 @@ export async function assembleStandaloneSpk(options) {
|
|
|
124
135
|
writeFile(join(staging, "conf", "resource"), resource);
|
|
125
136
|
if (manifest.capabilities.jobs) {
|
|
126
137
|
mkdirSync(join(staging, BACKGROUND_UNIT_DIR), { recursive: true });
|
|
127
|
-
writeFile(join(staging, BACKGROUND_UNIT_DIR, backgroundUnitName(manifest.package)), renderBackgroundUnit(manifest.package));
|
|
138
|
+
writeFile(join(staging, BACKGROUND_UNIT_DIR, backgroundUnitName(manifest.package)), renderBackgroundUnit(manifest.package, isBridgeBackend(manifest.backend) ? BRIDGE_WORKER_ARGS : []));
|
|
128
139
|
}
|
|
129
140
|
mkdirSync(join(staging, "scripts"));
|
|
130
141
|
for (const name of LIFECYCLE_SCRIPTS) {
|
|
@@ -153,6 +164,11 @@ export async function assembleStandaloneSpk(options) {
|
|
|
153
164
|
rmSync(staging, { recursive: true, force: true });
|
|
154
165
|
}
|
|
155
166
|
}
|
|
167
|
+
function copyExecutable(source, target) {
|
|
168
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
169
|
+
copyFileSync(source, target);
|
|
170
|
+
chmodSync(target, EXEC_MODE);
|
|
171
|
+
}
|
|
156
172
|
function writeFile(path, content, mode = FILE_MODE) {
|
|
157
173
|
mkdirSync(dirname(path), { recursive: true });
|
|
158
174
|
writeFileSync(path, content, "utf8");
|
|
@@ -180,6 +196,9 @@ function renderInfo(manifest, arch, checksum, executorDigest) {
|
|
|
180
196
|
// Package Center writes INFO as root; root tasks accept only this executor.
|
|
181
197
|
if (executorDigest)
|
|
182
198
|
fields.push([EXECUTOR_ANCHOR, executorDigest]);
|
|
199
|
+
const backend = manifest.backend;
|
|
200
|
+
if (isBridgeBackend(backend) && backend.node)
|
|
201
|
+
fields.push(["install_dep_packages", backend.node.package]);
|
|
183
202
|
fields.push(...metadataInfoFields(manifest.metadata));
|
|
184
203
|
// Background packages need prestart: it runs before DSM's systemd-user-unit acquire.
|
|
185
204
|
if (manifest.daemon || manifest.capabilities.jobs)
|
|
@@ -196,7 +215,7 @@ function renderInfo(manifest, arch, checksum, executorDigest) {
|
|
|
196
215
|
function copyUiTree(source, target) {
|
|
197
216
|
for (const entry of ["app.js", "app.css"]) {
|
|
198
217
|
if (!existsSync(join(source, entry))) {
|
|
199
|
-
throw new StandaloneBuildError("STANDALONE_UI_ENTRY_MISSING", `UI
|
|
218
|
+
throw new StandaloneBuildError("STANDALONE_UI_ENTRY_MISSING", `The UI directory is missing ${entry}: ${source}`);
|
|
200
219
|
}
|
|
201
220
|
}
|
|
202
221
|
let total = 0;
|
|
@@ -207,7 +226,7 @@ function copyUiTree(source, target) {
|
|
|
207
226
|
const src = join(from, name);
|
|
208
227
|
const stat = lstatSync(src);
|
|
209
228
|
if (stat.isSymbolicLink()) {
|
|
210
|
-
throw new StandaloneBuildError("STANDALONE_UI_SYMLINK", `UI
|
|
229
|
+
throw new StandaloneBuildError("STANDALONE_UI_SYMLINK", `The UI directory must not contain a symlink: ${src}`);
|
|
211
230
|
}
|
|
212
231
|
if (stat.isDirectory()) {
|
|
213
232
|
walk(src, join(to, name));
|
|
@@ -215,13 +234,13 @@ function copyUiTree(source, target) {
|
|
|
215
234
|
else if (stat.isFile()) {
|
|
216
235
|
total += stat.size;
|
|
217
236
|
if (total > MAX_UI_BYTES) {
|
|
218
|
-
throw new StandaloneBuildError("STANDALONE_UI_TOO_LARGE", "UI
|
|
237
|
+
throw new StandaloneBuildError("STANDALONE_UI_TOO_LARGE", "The UI directory exceeds 64 MiB.");
|
|
219
238
|
}
|
|
220
239
|
copyFileSync(src, join(to, name));
|
|
221
240
|
chmodSync(join(to, name), FILE_MODE);
|
|
222
241
|
}
|
|
223
242
|
else {
|
|
224
|
-
throw new StandaloneBuildError("STANDALONE_UI_SPECIAL_FILE", `UI
|
|
243
|
+
throw new StandaloneBuildError("STANDALONE_UI_SPECIAL_FILE", `The UI directory may contain only regular files and directories: ${src}`);
|
|
225
244
|
}
|
|
226
245
|
}
|
|
227
246
|
};
|
|
@@ -44,16 +44,34 @@ ${manifest.uninstall?.state ? wizardFlag("delete_state", ["pkgwizard_delete_stat
|
|
|
44
44
|
(cd "$hook_var" 2>/dev/null || cd /; exec /usr/bin/env -i ${env} ${argv}) </dev/null >"$hook_var/uninstall-hook.log" 2>&1 &
|
|
45
45
|
hook_pid=$!
|
|
46
46
|
set +m
|
|
47
|
+
# DSM's bash logs a failed kill to syslog even with 2>/dev/null, so a
|
|
48
|
+
# finished hook is looked up in /proc wherever there is one.
|
|
49
|
+
hook_running() {
|
|
50
|
+
if [ -d /proc/self ]; then [ -d "/proc/$hook_pid" ]; else kill -0 "$hook_pid" 2>/dev/null; fi
|
|
51
|
+
}
|
|
52
|
+
# A process in /proc whose group is the hook's ("pid (comm) state ppid pgrp ...").
|
|
53
|
+
hook_group_running() {
|
|
54
|
+
cat /proc/[0-9]*/stat 2>/dev/null | awk -v group="$hook_pid" '{ sub(/.*\) /, ""); if ($3 == group) found = 1 } END { exit !found }'
|
|
55
|
+
}
|
|
56
|
+
hook_signal() {
|
|
57
|
+
if [ ! -d /proc/self ]; then
|
|
58
|
+
kill -"$1" -"$hook_pid" 2>/dev/null || kill -"$1" "$hook_pid" 2>/dev/null
|
|
59
|
+
elif hook_group_running; then
|
|
60
|
+
kill -"$1" -"$hook_pid"
|
|
61
|
+
elif hook_running; then
|
|
62
|
+
kill -"$1" "$hook_pid"
|
|
63
|
+
fi
|
|
64
|
+
}
|
|
47
65
|
hook_waited=0
|
|
48
|
-
while
|
|
66
|
+
while hook_running && [ "$hook_waited" -lt ${purge.timeout} ]; do
|
|
49
67
|
sleep 1
|
|
50
68
|
hook_waited=$((hook_waited + 1))
|
|
51
69
|
done
|
|
52
70
|
hook_failure=
|
|
53
|
-
if
|
|
54
|
-
|
|
71
|
+
if hook_running; then
|
|
72
|
+
hook_signal TERM
|
|
55
73
|
sleep 1
|
|
56
|
-
|
|
74
|
+
hook_signal KILL
|
|
57
75
|
wait "$hook_pid" 2>/dev/null
|
|
58
76
|
hook_failure="timeout after ${purge.timeout} s"
|
|
59
77
|
else
|
|
@@ -16,7 +16,7 @@ export function readUninstall(value, context, fail) {
|
|
|
16
16
|
if (value === undefined)
|
|
17
17
|
return undefined;
|
|
18
18
|
const invalid = () => {
|
|
19
|
-
fail({ code: "UNINSTALL_INVALID", message: "uninstall
|
|
19
|
+
fail({ code: "UNINSTALL_INVALID", message: "uninstall accepts only purge {exec, args, env, timeout, root} and state {items: [{key}]}; exec must be a fixed executable inside the package (not in executor/ or root/), and timeout is an integer from 2 to 120 seconds." });
|
|
20
20
|
return undefined;
|
|
21
21
|
};
|
|
22
22
|
if (!record(value) || Object.keys(value).some(k => !FIELDS.has(k)))
|
|
@@ -24,7 +24,7 @@ export function readUninstall(value, context, fail) {
|
|
|
24
24
|
if (value.purge === undefined) {
|
|
25
25
|
if (value.state === undefined)
|
|
26
26
|
return invalid();
|
|
27
|
-
fail({ code: "UNINSTALL_STATE_REQUIRES_PURGE", message: "uninstall.state
|
|
27
|
+
fail({ code: "UNINSTALL_STATE_REQUIRES_PURGE", message: "uninstall.state needs uninstall.purge: only the App knows where the state lives." });
|
|
28
28
|
return undefined;
|
|
29
29
|
}
|
|
30
30
|
const purge = readPurge(value.purge, context, fail);
|
|
@@ -35,7 +35,7 @@ export function readUninstall(value, context, fail) {
|
|
|
35
35
|
return invalid();
|
|
36
36
|
const missing = (state?.items ?? []).map(item => stateTextKey(item.key)).filter(key => context.en?.[key] === undefined);
|
|
37
37
|
if (missing.length) {
|
|
38
|
-
fail({ code: "UNINSTALL_STATE_TEXT_MISSING", message: `uninstall.state
|
|
38
|
+
fail({ code: "UNINSTALL_STATE_TEXT_MISSING", message: `The uninstall.state items need i18n en.json text: ${missing.join(", ")}.` });
|
|
39
39
|
return undefined;
|
|
40
40
|
}
|
|
41
41
|
return purge && { purge, ...(state ? { state } : {}) };
|
|
@@ -61,11 +61,11 @@ function readPurge(value, context, fail) {
|
|
|
61
61
|
if (typeof root !== "string")
|
|
62
62
|
return "invalid";
|
|
63
63
|
if (!context.rootOps) {
|
|
64
|
-
fail({ code: "UNINSTALL_ROOT_REQUIRES_ROOT_OPS", message: "uninstall.purge.root
|
|
64
|
+
fail({ code: "UNINSTALL_ROOT_REQUIRES_ROOT_OPS", message: "uninstall.purge.root needs capabilities.rootOps: true." });
|
|
65
65
|
return undefined;
|
|
66
66
|
}
|
|
67
67
|
if (context.ops && !context.ops.some(op => op.name === root)) {
|
|
68
|
-
fail({ code: "UNINSTALL_ROOT_OP_MISSING", message: `uninstall.purge.root
|
|
68
|
+
fail({ code: "UNINSTALL_ROOT_OP_MISSING", message: `uninstall.purge.root "${root}" needs root/${root}.sh.` });
|
|
69
69
|
return undefined;
|
|
70
70
|
}
|
|
71
71
|
}
|
|
@@ -99,11 +99,11 @@ export function uninstallExecProblem(packageDir, purge) {
|
|
|
99
99
|
stat = undefined;
|
|
100
100
|
}
|
|
101
101
|
if (!stat?.isFile() || (stat.mode & 0o111) === 0) {
|
|
102
|
-
return { code: "UNINSTALL_EXEC_MISSING", message: `uninstall.purge.exec
|
|
102
|
+
return { code: "UNINSTALL_EXEC_MISSING", message: `uninstall.purge.exec "${purge.exec}" is not a regular executable file inside the package.` };
|
|
103
103
|
}
|
|
104
104
|
const text = readFileSync(path).toString("latin1");
|
|
105
105
|
if (text.startsWith("#!") && ROOT_SCRIPT_HINTS.some(hint => hint.test(text))) {
|
|
106
|
-
return { code: "UNINSTALL_EXEC_UNSAFE", message: `uninstall.purge.exec
|
|
106
|
+
return { code: "UNINSTALL_EXEC_UNSAFE", message: `uninstall.purge.exec "${purge.exec}" tries to gain root (sudo, chown root or setuid); root is only available through uninstall.purge.root.` };
|
|
107
107
|
}
|
|
108
108
|
return undefined;
|
|
109
109
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** The DSM AppWindow's size and the desktop's minimum size, in pixels. */
|
|
2
|
+
export interface WindowSize {
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
minWidth: number;
|
|
6
|
+
minHeight: number;
|
|
7
|
+
}
|
|
8
|
+
export declare const DEFAULT_WINDOW: WindowSize;
|
|
9
|
+
/** A Portal/standalone launch is the whole page, a phone's as well. */
|
|
10
|
+
export declare const FULL_PAGE_MIN_WIDTH = 320;
|
|
11
|
+
export declare const WINDOW_USAGE = "ui.window takes width and height (whole numbers, 320\u20133840 by 240\u20132160) and minWidth and minHeight (no larger than width and height; 700 and 420 by default); omitted fields keep 960\u00D7640, at least 700\u00D7420.";
|
|
12
|
+
/** `ui.window` over the defaults, or why it is refused. */
|
|
13
|
+
export declare function readWindowSize(value: unknown): {
|
|
14
|
+
size: WindowSize;
|
|
15
|
+
} | {
|
|
16
|
+
error: string;
|
|
17
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const DEFAULT_WINDOW = { width: 960, height: 640, minWidth: 700, minHeight: 420 };
|
|
2
|
+
/** A Portal/standalone launch is the whole page, a phone's as well. */
|
|
3
|
+
export const FULL_PAGE_MIN_WIDTH = 320;
|
|
4
|
+
const LIMITS = {
|
|
5
|
+
width: [320, 3840], height: [240, 2160], minWidth: [320, 3840], minHeight: [240, 2160],
|
|
6
|
+
};
|
|
7
|
+
export const WINDOW_USAGE = "ui.window takes width and height (whole numbers, 320–3840 by 240–2160) and minWidth and minHeight (no larger than width and height; 700 and 420 by default); omitted fields keep 960×640, at least 700×420.";
|
|
8
|
+
/** `ui.window` over the defaults, or why it is refused. */
|
|
9
|
+
export function readWindowSize(value) {
|
|
10
|
+
const keys = Object.keys(LIMITS);
|
|
11
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || Object.keys(value).length === 0)
|
|
12
|
+
return { error: WINDOW_USAGE };
|
|
13
|
+
const raw = value;
|
|
14
|
+
const unknown = Object.keys(raw).filter(key => !keys.includes(key));
|
|
15
|
+
if (unknown.length)
|
|
16
|
+
return { error: `${WINDOW_USAGE} Unsupported: ${unknown.map(key => `ui.window.${key}`).join(", ")}.` };
|
|
17
|
+
const size = { ...DEFAULT_WINDOW };
|
|
18
|
+
for (const key of keys) {
|
|
19
|
+
if (raw[key] === undefined)
|
|
20
|
+
continue;
|
|
21
|
+
const [min, max] = LIMITS[key];
|
|
22
|
+
const n = raw[key];
|
|
23
|
+
if (!Number.isInteger(n) || n < min || n > max)
|
|
24
|
+
return { error: `${WINDOW_USAGE} ui.window.${key} must be a whole number from ${min} to ${max}.` };
|
|
25
|
+
size[key] = n;
|
|
26
|
+
}
|
|
27
|
+
if (size.minWidth > size.width || size.minHeight > size.height) {
|
|
28
|
+
return { error: `${WINDOW_USAGE} The minimum ${size.minWidth}×${size.minHeight} is larger than the window ${size.width}×${size.height}.` };
|
|
29
|
+
}
|
|
30
|
+
return { size };
|
|
31
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -56,4 +56,4 @@ export interface ComposeDocument {
|
|
|
56
56
|
secrets?: Record<string, unknown>;
|
|
57
57
|
[key: string]: unknown;
|
|
58
58
|
}
|
|
59
|
-
export type TemplateName = "compose" | "binary" | "native-go" | "native-rust";
|
|
59
|
+
export type TemplateName = "compose" | "binary" | "native-go" | "native-python" | "native-node" | "native-rust";
|
package/dist/validate.js
CHANGED
|
@@ -23,37 +23,37 @@ export function validateProject(project, options = {}) {
|
|
|
23
23
|
return { ok: false, errors: [{
|
|
24
24
|
code: "PROJECT_RUNTIME_EXCLUSIVE",
|
|
25
25
|
span: { file: DSM_PACKAGE_FILENAME, line: 1, column: 1 },
|
|
26
|
-
message: "
|
|
27
|
-
suggestion: "
|
|
26
|
+
message: "The project has both compose.yaml and dsm-package.yaml, which are mutually exclusive.",
|
|
27
|
+
suggestion: "Choose compose.yaml (container App) or dsm-package.yaml (native App) and delete the other file.",
|
|
28
28
|
}] };
|
|
29
29
|
}
|
|
30
30
|
if (hasStandalone) {
|
|
31
31
|
const loaded = isNativeLayout(project.root) ? loadNativeLayout(project.root) : loadStandaloneManifest(project.root);
|
|
32
32
|
return loaded.ok ? { ok: true, errors: [] } : { ok: false, errors: loaded.errors.map(error => ({
|
|
33
33
|
...error, span: { file: DSM_PACKAGE_FILENAME, line: 1, column: 1 },
|
|
34
|
-
suggestion: getErrorCode(error.code)?.suggestion ?? "
|
|
34
|
+
suggestion: getErrorCode(error.code)?.suggestion ?? "Fix the schema 2 fields and capability declarations in dsm-package.yaml.",
|
|
35
35
|
})) };
|
|
36
36
|
}
|
|
37
37
|
const parsed = loadContract(project.contractPath);
|
|
38
38
|
errors.push(...validateContract(parsed, contractFile));
|
|
39
39
|
const capabilities = parsed.contract.capabilities;
|
|
40
40
|
if (capabilities !== undefined) {
|
|
41
|
-
errors.push(makeError("CAPABILITY_UNSUPPORTED", contractSpan(contractFile, 1, 1, "capabilities"), "compose
|
|
41
|
+
errors.push(makeError("CAPABILITY_UNSUPPORTED", contractSpan(contractFile, 1, 1, "capabilities"), "A compose project does not support capabilities.", "Remove capabilities, or use a dsm-package.yaml native-go project."));
|
|
42
42
|
}
|
|
43
43
|
if (profile === "listing") {
|
|
44
44
|
for (const { name, type, code } of findListingUnsupportedInstallTypes(parsed.contract)) {
|
|
45
45
|
const pos = getValuePosition(parsed.doc, ["install", name, "type"]) ??
|
|
46
46
|
getValuePosition(parsed.doc, ["install", name]) ??
|
|
47
47
|
{ line: 1, column: 1 };
|
|
48
|
-
errors.push(makeError(code, contractSpan(contractFile, pos.line, pos.column, `install.${name}.type`), `Install Parameter
|
|
48
|
+
errors.push(makeError(code, contractSpan(contractFile, pos.line, pos.column, `install.${name}.type`), `The Install Parameter "${name}" with type: ${type} can only be snapshotted by synopkgland deploy with an administrator session at install time; Package Center and Package Source listing do not support it.`, "Use synopkgland deploy --host <name> so an administrator session takes the Share list snapshot at install time; this type cannot ship through Package Center or Package Source listing."));
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
if (!hasCompose) {
|
|
52
52
|
errors.push({
|
|
53
53
|
code: "COMPOSE_MISSING",
|
|
54
54
|
span: { file: composeFile, line: 1, column: 1 },
|
|
55
|
-
message: "
|
|
56
|
-
suggestion: "
|
|
55
|
+
message: "compose.yaml or dsm-package.yaml was not found.",
|
|
56
|
+
suggestion: "Create compose.yaml or a schema 2 dsm-package.yaml in the project root.",
|
|
57
57
|
});
|
|
58
58
|
return { ok: false, errors };
|
|
59
59
|
}
|
|
@@ -95,16 +95,16 @@ export function validateProject(project, options = {}) {
|
|
|
95
95
|
errors.push({
|
|
96
96
|
code: "CONTRACT_DSM_USER_DUPLICATE",
|
|
97
97
|
span: { file: contractFile, line: 1, column: 1, path: "install" },
|
|
98
|
-
message: "
|
|
99
|
-
suggestion: "
|
|
98
|
+
message: "Each App has at most one dsm_user.",
|
|
99
|
+
suggestion: "Keep only one Container Run-as parameter.",
|
|
100
100
|
});
|
|
101
101
|
}
|
|
102
102
|
if (hasSharedFolder && dsmUserParams.length === 0) {
|
|
103
103
|
errors.push({
|
|
104
104
|
code: "DSM_USER_REQUIRED",
|
|
105
105
|
span: { file: contractFile, line: 1, column: 1, path: "install" },
|
|
106
|
-
message: "
|
|
107
|
-
suggestion: "
|
|
106
|
+
message: "With a Shared Folder, declare exactly one dsm_user as the Container Run-as.",
|
|
107
|
+
suggestion: "Add install.<NAME>.type: dsm_user and reference ${NAME_UID}:${NAME_GID} in Compose.",
|
|
108
108
|
severity: profile === "listing" ? "error" : "warning",
|
|
109
109
|
});
|
|
110
110
|
}
|
|
@@ -132,8 +132,8 @@ export function validateProject(project, options = {}) {
|
|
|
132
132
|
column: 1,
|
|
133
133
|
path: `install.${name}`,
|
|
134
134
|
},
|
|
135
|
-
message: `Install Parameter
|
|
136
|
-
suggestion:
|
|
135
|
+
message: `The Install Parameter "${name}" is not referenced in Compose.`,
|
|
136
|
+
suggestion: `Reference this parameter in compose.yaml as \${${name}}.`,
|
|
137
137
|
});
|
|
138
138
|
}
|
|
139
139
|
}
|