@synopackageland/cli 0.10.2 → 0.10.4
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 +259 -1696
- package/README.md +21 -2
- package/assets/login-style/mask.png +0 -0
- package/assets/login-style/wallpaper-thumbnail.jpg +0 -0
- package/assets/login-style/wallpaper.jpg +0 -0
- package/dist/build-spk/generator.js +10 -1
- package/dist/build-spk/login-style.d.ts +55 -0
- package/dist/build-spk/login-style.js +115 -0
- package/dist/cli.js +22 -1
- package/dist/error-registry.js +37 -1
- package/dist/host/dsm-client.js +6 -9
- package/dist/init.js +9 -5
- package/dist/runtime/delegate-contract.d.ts +2 -92
- package/dist/runtime/delegate-contract.js +2 -24
- package/dist/runtime/executor-digests.json +22 -0
- package/dist/runtime/executor-policy-check.d.ts +1 -1
- package/dist/runtime/executor-policy-check.js +5 -7
- package/dist/runtime/executor-policy.d.ts +2 -0
- package/dist/runtime/executor-policy.js +5 -1
- package/dist/runtime/root-programs.d.ts +2 -0
- package/dist/runtime/sdk-source.d.ts +4 -0
- package/dist/runtime/sdk-source.js +7 -0
- package/dist/runtime/sdk-timeouts.d.ts +23 -0
- package/dist/runtime/sdk-timeouts.js +44 -0
- package/dist/skill.js +1 -0
- package/dist/standalone/app-scripts.d.ts +26 -0
- package/dist/standalone/app-scripts.js +141 -0
- package/dist/standalone/bridge-app.d.ts +2 -0
- package/dist/standalone/bridge-app.js +9 -1
- package/dist/standalone/bridge-backend.d.ts +1 -1
- package/dist/standalone/build.js +5 -2
- package/dist/standalone/dsm-window.d.ts +14 -3
- package/dist/standalone/dsm-window.js +58 -12
- package/dist/standalone/host-adapter.d.ts +1 -1
- package/dist/standalone/host-adapter.js +1 -1
- package/dist/standalone/host-file-navigation.d.ts +6 -2
- package/dist/standalone/host-file-navigation.js +8 -2
- package/dist/standalone/inspect-spk.d.ts +9 -0
- package/dist/standalone/inspect-spk.js +60 -6
- package/dist/standalone/lifecycle.js +3 -1
- package/dist/standalone/manifest.d.ts +6 -0
- package/dist/standalone/manifest.js +51 -3
- package/dist/standalone/portal-window.d.ts +6 -6
- package/dist/standalone/portal-window.js +12 -32
- package/dist/standalone/request-timeout.d.ts +6 -3
- package/dist/standalone/request-timeout.js +10 -4
- package/dist/standalone/root-ops.d.ts +3 -2
- package/dist/standalone/root-ops.js +9 -3
- package/dist/standalone/root-window.d.ts +2 -2
- package/dist/standalone/root-window.js +191 -53
- package/dist/standalone/sdk-check.js +4 -1
- package/dist/standalone/spk.d.ts +3 -0
- package/dist/standalone/spk.js +10 -3
- package/dist/validate.js +2 -0
- package/package.json +5 -5
- package/skills/SKILL.md +3 -1
- package/skills/reference/create.md +5 -1
- package/skills/reference/identity.md +1 -1
- package/skills/reference/lifecycle.md +110 -0
- package/skills/reference/portal.md +12 -0
- package/dist/authorization-sdk.d.ts +0 -6
- package/dist/authorization-sdk.js +0 -6
- package/dist/standalone/native-authorization-window.d.ts +0 -2
- package/dist/standalone/native-authorization-window.js +0 -36
- package/dist/standalone/native-root-window.d.ts +0 -29
- package/dist/standalone/native-root-window.js +0 -307
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import {
|
|
2
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { PINNED_GO_HINT, PINNED_GO_TOOLCHAIN } from '../build-spk/go-toolchain.js';
|
|
6
6
|
import { sdkBackend } from '../standalone/manifest.js';
|
|
7
7
|
import { executorPolicy, executorPolicyText } from './executor-policy.js';
|
|
8
8
|
import { requiredSdk } from './sdk-source.js';
|
|
9
|
-
/** The executor subcommand that validates a policy on stdin as load() does
|
|
9
|
+
/** The executor subcommand that validates a policy on stdin as load() does. */
|
|
10
10
|
const CHECK_POLICY = 'check-policy';
|
|
11
11
|
/**
|
|
12
12
|
* Why the SDK executor would refuse the policy, such as `invalid policy: policy root`;
|
|
13
|
-
* undefined when it accepts it
|
|
13
|
+
* undefined when it accepts it. The executor is
|
|
14
14
|
* built for this host from executorDir, so its own rules decide, not a copy in the CLI.
|
|
15
15
|
*/
|
|
16
16
|
export function executorPolicyRefusal(executorDir, pkg, policyText, env = process.env) {
|
|
17
|
-
const main = join(executorDir, 'main.go');
|
|
18
|
-
if (!existsSync(main) || !readFileSync(main, 'utf8').includes(`"${CHECK_POLICY}"`))
|
|
19
|
-
return undefined;
|
|
20
17
|
const dir = mkdtempSync(join(tmpdir(), 'synopkgland-check-policy-'));
|
|
21
18
|
try {
|
|
22
19
|
const binary = join(dir, 'executor');
|
|
@@ -38,7 +35,8 @@ export function executorPolicyRefusal(executorDir, pkg, policyText, env = proces
|
|
|
38
35
|
}
|
|
39
36
|
/** Where in dsm-package.yaml each executor refusal comes from. */
|
|
40
37
|
const POLICY_FIX = [
|
|
41
|
-
[/policy (
|
|
38
|
+
[/policy (start|stop|purge): .*cannot also declare run or job/, 'root.start, root.stop and uninstall.purge name a program of their own: give them another program name in root.programs, one without run or job, and keep run and job on the session program'],
|
|
39
|
+
[/policy (root|start|stop|purge)/, 'fix root.programs (schema, run, job), root.start, root.stop or uninstall.purge in dsm-package.yaml'],
|
|
42
40
|
[/policy (scope|operation|destination|move|lifetime)/, 'fix capabilities.delegate in dsm-package.yaml'],
|
|
43
41
|
[/policy portal/, 'fix portal in dsm-package.yaml'],
|
|
44
42
|
[/policy entry/, 'fix entry in dsm-package.yaml'],
|
|
@@ -32,6 +32,8 @@ export interface ExecutorPolicy {
|
|
|
32
32
|
purge?: string;
|
|
33
33
|
/** The root entry the executor runs when the package stops or upgrades. */
|
|
34
34
|
stop?: string;
|
|
35
|
+
/** The root entry the executor runs once after each install or upgrade; a lifecycle step only. */
|
|
36
|
+
start?: string;
|
|
35
37
|
root: Record<string, RootPolicyEntry>;
|
|
36
38
|
/** Root programs get a directory only root can read, kept across upgrades. */
|
|
37
39
|
root_state?: true;
|
|
@@ -50,6 +50,7 @@ export function executorPolicy(m, arch) {
|
|
|
50
50
|
return { v: 1, package: m.package, app: `SYNO.SDS.${m.package}.Instance`, ...(m.entry ? { entry: m.entry } : {}), ...(d ? { scope: d.scope } : {}),
|
|
51
51
|
...(d?.lifetime ? { destinations: [...d.destinations].sort(), lifetime: d.lifetime } : {}),
|
|
52
52
|
operations: [...new Set(operations)].sort(), ...(portal ? { portal } : {}), ...(purge ? { purge } : {}), ...(settings?.stop ? { stop: settings.stop } : {}),
|
|
53
|
+
...(settings?.start ? { start: settings.start } : {}),
|
|
53
54
|
root, ...(settings?.state ? { root_state: true } : {}) };
|
|
54
55
|
}
|
|
55
56
|
/** A script without settings keeps the bare digest; a binary pins the bytes built for this architecture. */
|
|
@@ -72,18 +73,21 @@ function executorPortal(m) {
|
|
|
72
73
|
throw new Error(`[EXECUTOR_POLICY] ${problem}`);
|
|
73
74
|
return entry;
|
|
74
75
|
}
|
|
76
|
+
/** A root entry a session may run or start; the executor refuses it as a lifecycle step. */
|
|
77
|
+
const sessionEntry = (entry) => typeof entry !== 'string' && (entry.run !== undefined || entry.job !== undefined);
|
|
75
78
|
/** Inventory rejects malformed extensions even if a caller recomputes every pin. */
|
|
76
79
|
export function validExecutorPolicy(value, pkg) {
|
|
77
80
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
78
81
|
return false;
|
|
79
82
|
const p = value;
|
|
80
|
-
if (Object.keys(p).some(key => !['v', 'package', 'app', 'entry', 'scope', 'destinations', 'lifetime', 'operations', 'portal', 'purge', 'stop', 'root', 'root_state'].includes(key))
|
|
83
|
+
if (Object.keys(p).some(key => !['v', 'package', 'app', 'entry', 'scope', 'destinations', 'lifetime', 'operations', 'portal', 'purge', 'stop', 'start', 'root', 'root_state'].includes(key))
|
|
81
84
|
|| p.v !== 1 || p.package !== pkg || p.app !== `SYNO.SDS.${pkg}.Instance`
|
|
82
85
|
|| 'entry' in p && p.entry !== 'admin-only' && p.entry !== 'app-privilege'
|
|
83
86
|
|| !Array.isArray(p.operations) || !p.operations.includes('status') || new Set(p.operations).size !== p.operations.length
|
|
84
87
|
|| !p.root || typeof p.root !== 'object' || Array.isArray(p.root)
|
|
85
88
|
|| 'purge' in p && (typeof p.purge !== 'string' || !Object.hasOwn(p.root, p.purge))
|
|
86
89
|
|| 'stop' in p && (typeof p.stop !== 'string' || !Object.hasOwn(p.root, p.stop))
|
|
90
|
+
|| 'start' in p && (typeof p.start !== 'string' || !Object.hasOwn(p.root, p.start) || sessionEntry(p.root[p.start]))
|
|
87
91
|
|| 'root_state' in p && p.root_state !== true
|
|
88
92
|
|| Object.entries(p.root).some(([name, entry]) => !ROOT_PROGRAM_NAME.test(name) || rootEntryProblem(entry) !== undefined))
|
|
89
93
|
return false;
|
|
@@ -19,6 +19,8 @@ export interface RootProgramSettings {
|
|
|
19
19
|
/** dsm-package.yaml `root`: settings of root/ programs, the stop step and the root state directory. */
|
|
20
20
|
export interface RootSettings {
|
|
21
21
|
programs?: Record<string, RootProgramSettings>;
|
|
22
|
+
/** The program the executor runs once after each install or upgrade. */
|
|
23
|
+
start?: string;
|
|
22
24
|
stop?: string;
|
|
23
25
|
state?: true;
|
|
24
26
|
}
|
|
@@ -4,6 +4,10 @@ export declare const EXECUTOR_MODULE = "github.com/synopackageland/syno-pkg-land
|
|
|
4
4
|
/** The thin Rust App crate (spec/bridge/v1), a git dependency on a release tag of the SDK repository. */
|
|
5
5
|
export declare const SDK_CRATE = "syno-pkg-land-app";
|
|
6
6
|
export declare const SDK_GIT_URL = "https://github.com/synopackageland/syno-pkg-land-sdk";
|
|
7
|
+
/** The oldest SDK this CLI supports in its minor line; older patches lack spec/protocol/v1/timeouts.json. */
|
|
8
|
+
export declare const SDK_FLOOR = "0.10.3";
|
|
9
|
+
/** Whether version is an SDK patch older than SDK_FLOOR in the floor's minor line. */
|
|
10
|
+
export declare function sdkBelowFloor(version: string): boolean;
|
|
7
11
|
/** Where one SDK version's Go client and executor sources are read from. */
|
|
8
12
|
export interface SdkSource {
|
|
9
13
|
version: string;
|
|
@@ -8,6 +8,13 @@ export const EXECUTOR_MODULE = `${SDK_MODULE}/executor`;
|
|
|
8
8
|
/** The thin Rust App crate (spec/bridge/v1), a git dependency on a release tag of the SDK repository. */
|
|
9
9
|
export const SDK_CRATE = 'syno-pkg-land-app';
|
|
10
10
|
export const SDK_GIT_URL = `https://${SDK_MODULE}`;
|
|
11
|
+
/** The oldest SDK this CLI supports in its minor line; older patches lack spec/protocol/v1/timeouts.json. */
|
|
12
|
+
export const SDK_FLOOR = '0.10.3';
|
|
13
|
+
/** Whether version is an SDK patch older than SDK_FLOOR in the floor's minor line. */
|
|
14
|
+
export function sdkBelowFloor(version) {
|
|
15
|
+
const [major, minor, patch] = version.split('.').map(Number), [floorMajor, floorMinor, floorPatch] = SDK_FLOOR.split('.').map(Number);
|
|
16
|
+
return major === floorMajor && minor === floorMinor && patch < floorPatch;
|
|
17
|
+
}
|
|
11
18
|
/** How each backend declares the SDK. */
|
|
12
19
|
export const SDK_DEPENDENCY = {
|
|
13
20
|
go: `add \`require ${SDK_MODULE} v<version>\` to backend/go.mod.`,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** The SDK's timeouts on the call chain to the executor (spec/protocol/v1, Timeouts). */
|
|
2
|
+
export declare const SDK_TIMEOUTS_FILE: string;
|
|
3
|
+
/** The waits the CLI generates into an App: its request default, the window's margin, and root.run's needs. */
|
|
4
|
+
export interface SdkTimeouts {
|
|
5
|
+
/** The request deadline unless the App sets backend.timeout_seconds. */
|
|
6
|
+
requestDefaultSeconds: number;
|
|
7
|
+
/** How long the window waits past the request deadline. */
|
|
8
|
+
browserMarginMs: number;
|
|
9
|
+
/** A root.run program's timeout_s unless root.programs sets it. */
|
|
10
|
+
rootRunDefaultSeconds: number;
|
|
11
|
+
/** How much longer than a program's timeout_s a request awaiting root.run must last. */
|
|
12
|
+
rootRunRequestMarginSeconds: number;
|
|
13
|
+
}
|
|
14
|
+
/** What an App without the SDK waits. */
|
|
15
|
+
export declare const NO_SDK_TIMEOUTS: SdkTimeouts;
|
|
16
|
+
/** The timeouts of the SDK checkout or module at goDir. */
|
|
17
|
+
export declare function sdkTimeouts(goDir: string): SdkTimeouts;
|
|
18
|
+
/**
|
|
19
|
+
* The timeouts of the SDK an App's backend requires. A backend without the
|
|
20
|
+
* SDK, or one whose SDK cannot be resolved or is older than the CLI supports
|
|
21
|
+
* (sdkProblems and the API permissions report that), gets NO_SDK_TIMEOUTS.
|
|
22
|
+
*/
|
|
23
|
+
export declare function appSdkTimeouts(backend: object, env?: NodeJS.ProcessEnv): SdkTimeouts;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { appSdkVersion, sdkBelowFloor, sdkSource } from './sdk-source.js';
|
|
4
|
+
/** The SDK's timeouts on the call chain to the executor (spec/protocol/v1, Timeouts). */
|
|
5
|
+
export const SDK_TIMEOUTS_FILE = join('spec', 'protocol', 'v1', 'timeouts.json');
|
|
6
|
+
/** What an App without the SDK waits. */
|
|
7
|
+
export const NO_SDK_TIMEOUTS = { requestDefaultSeconds: 60, browserMarginMs: 2000, rootRunDefaultSeconds: 20, rootRunRequestMarginSeconds: 20 };
|
|
8
|
+
const read = new Map();
|
|
9
|
+
/** The timeouts of the SDK checkout or module at goDir. */
|
|
10
|
+
export function sdkTimeouts(goDir) {
|
|
11
|
+
const cached = read.get(goDir);
|
|
12
|
+
if (cached)
|
|
13
|
+
return cached;
|
|
14
|
+
const spec = JSON.parse(readFileSync(join(goDir, SDK_TIMEOUTS_FILE), 'utf8'));
|
|
15
|
+
const timeouts = {
|
|
16
|
+
requestDefaultSeconds: spec.request.default_s,
|
|
17
|
+
browserMarginMs: spec.browser.margin_ms,
|
|
18
|
+
rootRunDefaultSeconds: spec.executor.root_run_program_default_s,
|
|
19
|
+
// The executor answers root.run this long after the program's timeout, and its client waits margin_s more.
|
|
20
|
+
rootRunRequestMarginSeconds: spec.executor.root_run_answer_s - spec.executor.root_run_program_max_s + spec.margin_s,
|
|
21
|
+
};
|
|
22
|
+
read.set(goDir, timeouts);
|
|
23
|
+
return timeouts;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The timeouts of the SDK an App's backend requires. A backend without the
|
|
27
|
+
* SDK, or one whose SDK cannot be resolved or is older than the CLI supports
|
|
28
|
+
* (sdkProblems and the API permissions report that), gets NO_SDK_TIMEOUTS.
|
|
29
|
+
*/
|
|
30
|
+
export function appSdkTimeouts(backend, env = process.env) {
|
|
31
|
+
if (backend.kind === 'binary')
|
|
32
|
+
return NO_SDK_TIMEOUTS;
|
|
33
|
+
let goDir;
|
|
34
|
+
try {
|
|
35
|
+
const version = appSdkVersion(backend);
|
|
36
|
+
if (!version || sdkBelowFloor(version))
|
|
37
|
+
return NO_SDK_TIMEOUTS;
|
|
38
|
+
goDir = sdkSource(version, env).goDir;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return NO_SDK_TIMEOUTS;
|
|
42
|
+
}
|
|
43
|
+
return sdkTimeouts(goDir);
|
|
44
|
+
}
|
package/dist/skill.js
CHANGED
|
@@ -10,6 +10,7 @@ const SKILL_CATALOG = [
|
|
|
10
10
|
{ id: "portal", file: "reference/portal.md", when: "Needs a DSM Application Portal alias link" },
|
|
11
11
|
{ id: "ui-vue", file: "reference/ui-vue.md", when: "Write or check a Vue UI" },
|
|
12
12
|
{ id: "ui-json", file: "reference/ui-json.md", when: "Generate or check a JSON UI" },
|
|
13
|
+
{ id: "lifecycle", file: "reference/lifecycle.md", when: "Add work to install, upgrade, uninstall, start or stop (dsm-scripts/)" },
|
|
13
14
|
{ id: "deploy", file: "reference/deploy.md", when: "Deploy to a NAS" },
|
|
14
15
|
{ id: "diagnose", file: "reference/diagnose.md", when: "check/deploy/Open failed" },
|
|
15
16
|
];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { LIFECYCLE_SCRIPTS } from "../spk/dsm-spec.js";
|
|
2
|
+
import type { StandaloneError } from "./manifest.js";
|
|
3
|
+
/** The project folder whose files carry DSM's own script names; `scripts/` stays free for the App's tools. */
|
|
4
|
+
export declare const APP_SCRIPTS_DIR = "dsm-scripts";
|
|
5
|
+
/** Where the SPK carries them: next to the generated scripts, so postuninst still finds them after the payload is gone. */
|
|
6
|
+
export declare const APP_SCRIPTS_SPK_DIR = "scripts/app";
|
|
7
|
+
export type AppScriptName = (typeof LIFECYCLE_SCRIPTS)[number];
|
|
8
|
+
/**
|
|
9
|
+
* The project's `dsm-scripts/<name>` files, one per DSM lifecycle script name.
|
|
10
|
+
* prereplace/postreplace are refused: DSM runs them only for
|
|
11
|
+
* install_replace_packages, which the SDK never declares.
|
|
12
|
+
*/
|
|
13
|
+
export declare function readAppScripts(root: string, fail: (error: StandaloneError) => void): AppScriptName[] | undefined;
|
|
14
|
+
/** Wraps the SDK's script so it runs in a subshell, then calls the App's script of the same name. */
|
|
15
|
+
export declare function composeAppScript(name: string, sdkScript: string): string;
|
|
16
|
+
/** The SDK script inside `script`, or undefined when the App wrapper was edited. */
|
|
17
|
+
export declare function sdkScriptOf(name: string, script: string): {
|
|
18
|
+
sdk: string;
|
|
19
|
+
app: boolean;
|
|
20
|
+
} | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* inspect: checks every App wrapper and App script, then writes the bare SDK
|
|
23
|
+
* script back into the extracted `scripts/` so the fixed-lifecycle checks
|
|
24
|
+
* compare the SDK steps alone. Returns the App scripts DSM will reach.
|
|
25
|
+
*/
|
|
26
|
+
export declare function unwrapAppScripts(scriptsDir: string, findings: string[]): AppScriptName[];
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { LIFECYCLE_SCRIPTS } from "../spk/dsm-spec.js";
|
|
4
|
+
/** The project folder whose files carry DSM's own script names; `scripts/` stays free for the App's tools. */
|
|
5
|
+
export const APP_SCRIPTS_DIR = "dsm-scripts";
|
|
6
|
+
/** Where the SPK carries them: next to the generated scripts, so postuninst still finds them after the payload is gone. */
|
|
7
|
+
export const APP_SCRIPTS_SPK_DIR = "scripts/app";
|
|
8
|
+
/**
|
|
9
|
+
* The project's `dsm-scripts/<name>` files, one per DSM lifecycle script name.
|
|
10
|
+
* prereplace/postreplace are refused: DSM runs them only for
|
|
11
|
+
* install_replace_packages, which the SDK never declares.
|
|
12
|
+
*/
|
|
13
|
+
export function readAppScripts(root, fail) {
|
|
14
|
+
const dir = join(root, APP_SCRIPTS_DIR);
|
|
15
|
+
let entries;
|
|
16
|
+
try {
|
|
17
|
+
entries = readdirSync(dir);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const names = [];
|
|
23
|
+
for (const name of entries.filter(entry => !entry.startsWith(".")).sort()) {
|
|
24
|
+
const path = `${APP_SCRIPTS_DIR}/${name}`;
|
|
25
|
+
if (!LIFECYCLE_SCRIPTS.includes(name)) {
|
|
26
|
+
fail({ code: "APP_SCRIPT_UNKNOWN", message: `${path} is not a DSM package script name; use one of ${LIFECYCLE_SCRIPTS.join(", ")}.` });
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const stat = lstatSync(join(dir, name));
|
|
30
|
+
if (!stat.isFile()) {
|
|
31
|
+
fail({ code: "APP_SCRIPT_INVALID", message: `${path} must be a regular file, not a symlink or directory.` });
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (!(stat.mode & 0o100)) {
|
|
35
|
+
fail({ code: "APP_SCRIPT_NOT_EXECUTABLE", message: `${path} is not executable; run chmod +x ${path}.` });
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (!readFileSync(join(dir, name)).subarray(0, 2).equals(Buffer.from("#!"))) {
|
|
39
|
+
fail({ code: "APP_SCRIPT_NO_SHEBANG", message: `${path} must start with #! (for example #!/bin/sh); DSM runs it directly.` });
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
names.push(name);
|
|
43
|
+
}
|
|
44
|
+
return names.length ? names : undefined;
|
|
45
|
+
}
|
|
46
|
+
const SHEBANG = "#!/bin/sh\n";
|
|
47
|
+
const SDK_OPEN = "synopkgland_sdk() (\n";
|
|
48
|
+
const SDK_CLOSE = ")\n";
|
|
49
|
+
/** Hooks that tear down: the App script runs first, while the SDK's services still exist. */
|
|
50
|
+
const APP_FIRST = new Set(["preupgrade", "preuninst"]);
|
|
51
|
+
function head(name) {
|
|
52
|
+
return `${SHEBANG}# synopkgland: the SDK steps run in synopkgland_sdk; the App's dsm-scripts/${name} runs from scripts/app/${name}.\n${SDK_OPEN}`;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Exit codes keep DSM's meaning. The SDK step is never skipped once it is
|
|
56
|
+
* due: start-stop-status stop always stops the SDK even when the App's stop
|
|
57
|
+
* fails, and a failed App start stops the SDK again, because DSM then treats
|
|
58
|
+
* the package as stopped and must not leave its ingress or workers running.
|
|
59
|
+
*/
|
|
60
|
+
function tail(name) {
|
|
61
|
+
const app = `synopkgland_app="$(dirname "$0")/app/${name}"\n`;
|
|
62
|
+
if (APP_FIRST.has(name))
|
|
63
|
+
return `${SDK_CLOSE}${app}"$synopkgland_app" "$@" || exit $?\nsynopkgland_sdk "$@"\n`;
|
|
64
|
+
if (name !== "start-stop-status")
|
|
65
|
+
return `${SDK_CLOSE}${app}synopkgland_sdk "$@" || exit $?\nexec "$synopkgland_app" "$@"\n`;
|
|
66
|
+
return `${SDK_CLOSE}${app}case "$1" in
|
|
67
|
+
start)
|
|
68
|
+
synopkgland_sdk "$@" || exit $?
|
|
69
|
+
"$synopkgland_app" "$@" && exit 0
|
|
70
|
+
synopkgland_result=$?
|
|
71
|
+
synopkgland_sdk stop
|
|
72
|
+
exit "$synopkgland_result" ;;
|
|
73
|
+
stop|prestop)
|
|
74
|
+
synopkgland_result=0
|
|
75
|
+
"$synopkgland_app" "$@" || synopkgland_result=$?
|
|
76
|
+
synopkgland_sdk "$@" || exit $?
|
|
77
|
+
exit "$synopkgland_result" ;;
|
|
78
|
+
restart)
|
|
79
|
+
"$0" stop || exit $?
|
|
80
|
+
exec "$0" start ;;
|
|
81
|
+
*)
|
|
82
|
+
synopkgland_sdk "$@" || exit $?
|
|
83
|
+
exec "$synopkgland_app" "$@" ;;
|
|
84
|
+
esac
|
|
85
|
+
`;
|
|
86
|
+
}
|
|
87
|
+
/** Wraps the SDK's script so it runs in a subshell, then calls the App's script of the same name. */
|
|
88
|
+
export function composeAppScript(name, sdkScript) {
|
|
89
|
+
if (!sdkScript.startsWith(SHEBANG))
|
|
90
|
+
throw new Error(`SDK script ${name} must start with ${SHEBANG.trim()}`);
|
|
91
|
+
return head(name) + sdkScript.slice(SHEBANG.length) + tail(name);
|
|
92
|
+
}
|
|
93
|
+
/** The SDK script inside `script`, or undefined when the App wrapper was edited. */
|
|
94
|
+
export function sdkScriptOf(name, script) {
|
|
95
|
+
if (!script.startsWith(`${SHEBANG}# synopkgland: the SDK steps`))
|
|
96
|
+
return { sdk: script, app: false };
|
|
97
|
+
const start = head(name), end = tail(name);
|
|
98
|
+
if (!script.startsWith(start) || !script.endsWith(end) || script.length < start.length + end.length)
|
|
99
|
+
return undefined;
|
|
100
|
+
return { sdk: SHEBANG + script.slice(start.length, script.length - end.length), app: true };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* inspect: checks every App wrapper and App script, then writes the bare SDK
|
|
104
|
+
* script back into the extracted `scripts/` so the fixed-lifecycle checks
|
|
105
|
+
* compare the SDK steps alone. Returns the App scripts DSM will reach.
|
|
106
|
+
*/
|
|
107
|
+
export function unwrapAppScripts(scriptsDir, findings) {
|
|
108
|
+
const appDir = join(scriptsDir, "app");
|
|
109
|
+
const appFiles = new Set(existsSync(appDir) ? readdirSync(appDir) : []);
|
|
110
|
+
const called = [];
|
|
111
|
+
for (const name of LIFECYCLE_SCRIPTS) {
|
|
112
|
+
const path = join(scriptsDir, name);
|
|
113
|
+
if (!existsSync(path))
|
|
114
|
+
continue;
|
|
115
|
+
const found = sdkScriptOf(name, readFileSync(path, "utf8"));
|
|
116
|
+
if (!found) {
|
|
117
|
+
findings.push(`scripts/${name} wraps an App script but differs from the wrapper synopkgland build writes.`);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (!found.app)
|
|
121
|
+
continue;
|
|
122
|
+
if (!appFiles.has(name))
|
|
123
|
+
findings.push(`scripts/${name} calls scripts/app/${name}, which is missing.`);
|
|
124
|
+
else
|
|
125
|
+
called.push(name);
|
|
126
|
+
writeFileSync(path, found.sdk);
|
|
127
|
+
}
|
|
128
|
+
for (const name of [...appFiles].sort()) {
|
|
129
|
+
const path = join(appDir, name);
|
|
130
|
+
if (!called.includes(name)) {
|
|
131
|
+
findings.push(`scripts/app/${name} is not called by a generated lifecycle script.`);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const stat = lstatSync(path);
|
|
135
|
+
if (!stat.isFile() || (stat.mode & 0o777) !== 0o755)
|
|
136
|
+
findings.push(`scripts/app/${name} must be a regular file with mode 0755.`);
|
|
137
|
+
else if (!readFileSync(path).subarray(0, 2).equals(Buffer.from("#!")))
|
|
138
|
+
findings.push(`scripts/app/${name} does not start with #!.`);
|
|
139
|
+
}
|
|
140
|
+
return called;
|
|
141
|
+
}
|
|
@@ -17,6 +17,8 @@ export declare function stageScriptApp(m: StandaloneManifest, work: string): Pro
|
|
|
17
17
|
* (spec/bridge/v1 SDKs). Returns the methods, sorted.
|
|
18
18
|
*/
|
|
19
19
|
export declare function describeMethods(command: string, args: string[], cwd: string, env: Record<string, string>, timeoutMs?: number): Promise<string[]>;
|
|
20
|
+
/** The Python SDK package of an SDK release or checkout (its root directory). */
|
|
21
|
+
export declare function copyPythonSdk(sdkRoot: string, target: string): void;
|
|
20
22
|
/** The Node SDK as npm would install it: its package.json and its compiled dist/. */
|
|
21
23
|
export declare function copyNodeSdk(source: string, target: string): void;
|
|
22
24
|
/** Copies an App tree: regular files and directories only, without dot entries and Python bytecode. */
|
|
@@ -24,7 +24,7 @@ export async function stageScriptApp(m, work) {
|
|
|
24
24
|
if (backend.kind === "python") {
|
|
25
25
|
const vendor = join(dir, PYTHON_VENDOR_DIR, PYTHON_SDK);
|
|
26
26
|
rmSync(vendor, { recursive: true, force: true });
|
|
27
|
-
|
|
27
|
+
copyPythonSdk(sdk.goDir, vendor);
|
|
28
28
|
}
|
|
29
29
|
else {
|
|
30
30
|
const modules = join(dir, "node_modules", NODE_SDK_PACKAGE);
|
|
@@ -91,6 +91,14 @@ export function describeMethods(command, args, cwd, env, timeoutMs = DESCRIBE_TI
|
|
|
91
91
|
});
|
|
92
92
|
});
|
|
93
93
|
}
|
|
94
|
+
/** The Python SDK package of an SDK release or checkout (its root directory). */
|
|
95
|
+
export function copyPythonSdk(sdkRoot, target) {
|
|
96
|
+
const source = join(sdkRoot, "python", PYTHON_SDK);
|
|
97
|
+
if (!existsSync(join(source, "__init__.py"))) {
|
|
98
|
+
throw new StandaloneBuildError("STANDALONE_PYTHON_SDK_MISSING", `The SDK has no Python SDK: ${source}. Point SYNOPKGLAND_SDK_DIR at a syno-pkg-land-sdk checkout that has python/${PYTHON_SDK}, or use an SDK release that ships it.`);
|
|
99
|
+
}
|
|
100
|
+
copyAppTree(source, target);
|
|
101
|
+
}
|
|
94
102
|
/** The Node SDK as npm would install it: its package.json and its compiled dist/. */
|
|
95
103
|
export function copyNodeSdk(source, target) {
|
|
96
104
|
if (!existsSync(join(source, "dist/index.js"))) {
|
|
@@ -23,7 +23,7 @@ export interface BridgeBackend {
|
|
|
23
23
|
transfers?: Record<string, "upload" | "download">;
|
|
24
24
|
/** The upload limit; absent, the runtime's 16 GiB. */
|
|
25
25
|
maxUploadBytes?: number;
|
|
26
|
-
/** The request deadline in seconds; absent,
|
|
26
|
+
/** The request deadline in seconds; absent, the SDK's request default. */
|
|
27
27
|
timeoutSeconds?: number;
|
|
28
28
|
}
|
|
29
29
|
export declare function isBridgeBackend(backend: {
|
package/dist/standalone/build.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { goModulePath, privateSdkHint, SDK_MODULE } from "../runtime/sdk-source.js";
|
|
2
|
+
import { goModulePath, privateSdkHint, SDK_MODULE, sdkSource } from "../runtime/sdk-source.js";
|
|
3
3
|
import { existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
@@ -16,6 +16,7 @@ import { checkJobs, commitOutputs, forEachArch } from "./arch-jobs.js";
|
|
|
16
16
|
import { isBridgeBackend } from "./bridge-backend.js";
|
|
17
17
|
import { requestTimeoutSeconds } from "./request-timeout.js";
|
|
18
18
|
import { buildBridge, prepareBridge } from "./bridge-build.js";
|
|
19
|
+
import { portalCatalogs } from "./portal-window.js";
|
|
19
20
|
/** Argv only, never a shell; output streams to the terminal except stderr, which is also kept, and captured stdout. */
|
|
20
21
|
export const runCommand = (command, args, options) => new Promise((done) => {
|
|
21
22
|
const child = spawn(command, args, {
|
|
@@ -109,7 +110,9 @@ export async function buildStandalone(options) {
|
|
|
109
110
|
const { cgi: cgiPath, worker: backgroundWorker, app } = await backendFor(builtManifest, arch, archWork, run, bridge);
|
|
110
111
|
const outputPath = join(stage, `${manifest.package}-${manifest.spkVersion}-${arch}.spk`);
|
|
111
112
|
const daemonControl = await buildDaemonControl(manifest, arch, archWork, run);
|
|
112
|
-
|
|
113
|
+
// A portal needs the executor, so the executor's SDK carries the panel text.
|
|
114
|
+
const portalText = manifest.portal && executor ? portalCatalogs(sdkSource(executor.sdk).goDir) : undefined;
|
|
115
|
+
await assembleStandaloneSpk({ manifest: builtManifest, arch, cgiPath, outputPath, daemonControl, backgroundWorker, executor, app, portalText });
|
|
113
116
|
return outputPath;
|
|
114
117
|
}));
|
|
115
118
|
return { outputs, warnings: [...warnings] };
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import type { EntryMode, StandaloneManifest } from "./manifest.js";
|
|
2
|
+
import { type PortalCatalogs } from "./portal-window.js";
|
|
2
3
|
import { type AppTitle } from "../build-spk/ui-texts.js";
|
|
3
4
|
import { type PackageIcons } from "../build-spk/dsm-icons.js";
|
|
4
5
|
/** Same path the generated wrapper POSTs to, and the payload location of the CGI. */
|
|
5
6
|
export declare const CGI_PAYLOAD_PATH = "ui/cgi/api.cgi";
|
|
7
|
+
/**
|
|
8
|
+
* Relative to the page, as loadApp's assets and DSM's own WebAPI calls are: an
|
|
9
|
+
* Application Portal alias (https://nas/<alias>/) routes only its own paths,
|
|
10
|
+
* and /webman/... there answers 403.
|
|
11
|
+
*/
|
|
6
12
|
export declare function cgiUrl(packageId: string): string;
|
|
7
13
|
/**
|
|
8
14
|
* `createI18n(dsmLang, catalogs)` → `{ locale, t }`, plain ES5 so it runs in
|
|
@@ -19,8 +25,12 @@ export declare function windowScriptName(manifest: Pick<StandaloneManifest, "bui
|
|
|
19
25
|
/** The launcher fields an entry mode fixes; inspect checks a built SPK against them. */
|
|
20
26
|
export declare function entryLauncherAccess(entry: EntryMode): Record<string, unknown>;
|
|
21
27
|
export declare function appTitle(manifest: StandaloneManifest): AppTitle;
|
|
22
|
-
/**
|
|
23
|
-
|
|
28
|
+
/**
|
|
29
|
+
* `ui/config`: registers the DSM main-menu app and its window class. A portal
|
|
30
|
+
* App's launcher also carries the Login Portal style and its icon as the
|
|
31
|
+
* login page icon (DSM shows `loginIcon`, never `icon`, there).
|
|
32
|
+
*/
|
|
33
|
+
export declare function renderUiConfig(manifest: StandaloneManifest, icons?: PackageIcons, loginStyle?: Record<string, string>): string;
|
|
24
34
|
/**
|
|
25
35
|
* The DSM AppWindow that hosts the project's UI. It loads app/app.js and
|
|
26
36
|
* app/app.css once, then calls the app's mount(root, ctx). `ctx.call(method,
|
|
@@ -31,5 +41,6 @@ export declare function renderUiConfig(manifest: StandaloneManifest, icons?: Pac
|
|
|
31
41
|
* launch owns the whole page: `ctx.fullPage` tells the app to lay out full-page
|
|
32
42
|
* (and add a device-width viewport), and `ctx.appIcon` is the package icon a
|
|
33
43
|
* full-page shell shows beside the name, where a DSM window has a title bar.
|
|
44
|
+
* A portal App's `ctx.portal` shows `portalText`, the SDK's panel text.
|
|
34
45
|
*/
|
|
35
|
-
export declare function renderWindowScript(manifest: StandaloneManifest, assetVersion: string, icons?: PackageIcons): string;
|
|
46
|
+
export declare function renderWindowScript(manifest: StandaloneManifest, assetVersion: string, icons?: PackageIcons, portalText?: PortalCatalogs): string;
|
|
@@ -3,18 +3,22 @@ import { DSM_LANGS } from "./i18n.js";
|
|
|
3
3
|
import { PASSWORD_CONFIRM, ROOT_PANEL, ROOT_RUNTIME, rootPanelCatalogs } from "./root-window.js";
|
|
4
4
|
import { HOST_ADAPTER_RUNTIME } from "./host-adapter.js";
|
|
5
5
|
import { permitsOrdinaryUsers } from "./api-permissions.js";
|
|
6
|
-
import {
|
|
6
|
+
import { portalRuntime } from "./portal-window.js";
|
|
7
7
|
import { portalByExecutor } from "../build-spk/dsm-version.js";
|
|
8
8
|
import { uiTitleFields } from "../build-spk/ui-texts.js";
|
|
9
9
|
import { packageIcons, windowIconFile, windowIconTemplate } from "../build-spk/dsm-icons.js";
|
|
10
10
|
import { DEFAULT_WINDOW, FULL_PAGE_MIN_WIDTH } from "./window-size.js";
|
|
11
11
|
import { requestTimeoutSeconds } from "./request-timeout.js";
|
|
12
|
+
import { appSdkTimeouts } from "../runtime/sdk-timeouts.js";
|
|
12
13
|
/** Same path the generated wrapper POSTs to, and the payload location of the CGI. */
|
|
13
14
|
export const CGI_PAYLOAD_PATH = "ui/cgi/api.cgi";
|
|
14
|
-
/**
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Relative to the page, as loadApp's assets and DSM's own WebAPI calls are: an
|
|
17
|
+
* Application Portal alias (https://nas/<alias>/) routes only its own paths,
|
|
18
|
+
* and /webman/... there answers 403.
|
|
19
|
+
*/
|
|
16
20
|
export function cgiUrl(packageId) {
|
|
17
|
-
return
|
|
21
|
+
return `webman/3rdparty/${packageId}/cgi/api.cgi`;
|
|
18
22
|
}
|
|
19
23
|
const LOCALE_BY_DSM_LANG = Object.fromEntries(Object.entries(DSM_LANGS).map(([locale, lang]) => [lang, locale]));
|
|
20
24
|
/**
|
|
@@ -81,8 +85,12 @@ function displayNameCatalogs(manifest) {
|
|
|
81
85
|
}
|
|
82
86
|
return catalogs;
|
|
83
87
|
}
|
|
84
|
-
/**
|
|
85
|
-
|
|
88
|
+
/**
|
|
89
|
+
* `ui/config`: registers the DSM main-menu app and its window class. A portal
|
|
90
|
+
* App's launcher also carries the Login Portal style and its icon as the
|
|
91
|
+
* login page icon (DSM shows `loginIcon`, never `icon`, there).
|
|
92
|
+
*/
|
|
93
|
+
export function renderUiConfig(manifest, icons = packageIcons(manifest.icon), loginStyle) {
|
|
86
94
|
const icon = windowIconTemplate(icons);
|
|
87
95
|
const ns = `SYNO.SDS.${manifest.package}`;
|
|
88
96
|
const titleFields = uiTitleFields(appTitle(manifest));
|
|
@@ -95,6 +103,7 @@ export function renderUiConfig(manifest, icons = packageIcons(manifest.icon)) {
|
|
|
95
103
|
appWindow: `${ns}.MainWindow`,
|
|
96
104
|
allowMultiInstance: false,
|
|
97
105
|
...launcherAccess(manifest),
|
|
106
|
+
...(loginStyle ? { loginIcon: icon, loginStyle } : {}),
|
|
98
107
|
depend: [`${ns}.MainWindow`],
|
|
99
108
|
},
|
|
100
109
|
[`${ns}.MainWindow`]: {
|
|
@@ -117,21 +126,25 @@ export function renderUiConfig(manifest, icons = packageIcons(manifest.icon)) {
|
|
|
117
126
|
* launch owns the whole page: `ctx.fullPage` tells the app to lay out full-page
|
|
118
127
|
* (and add a device-width viewport), and `ctx.appIcon` is the package icon a
|
|
119
128
|
* full-page shell shows beside the name, where a DSM window has a title bar.
|
|
129
|
+
* A portal App's `ctx.portal` shows `portalText`, the SDK's panel text.
|
|
120
130
|
*/
|
|
121
|
-
export function renderWindowScript(manifest, assetVersion, icons = packageIcons(manifest.icon)) {
|
|
131
|
+
export function renderWindowScript(manifest, assetVersion, icons = packageIcons(manifest.icon), portalText) {
|
|
122
132
|
const pkg = manifest.package;
|
|
123
133
|
const ns = `SYNO.SDS.${pkg}`;
|
|
124
134
|
const p = JSON.stringify(pkg);
|
|
125
135
|
const root = manifest.executor ? renderRootParts(ns, manifest) : undefined;
|
|
126
136
|
const size = manifest.ui.window ?? DEFAULT_WINDOW;
|
|
127
137
|
const requestTimeout = requestTimeoutSeconds(manifest.backend) * 1000;
|
|
138
|
+
const clientMargin = appSdkTimeouts(manifest.backend).browserMarginMs;
|
|
139
|
+
if (manifest.portal && !portalText)
|
|
140
|
+
throw new Error("A portal App's window needs the SDK's portal panel text.");
|
|
128
141
|
return `/* Generated by synopkgland build. Edit your UI in the project's ui folder. */
|
|
129
142
|
Ext.ns(${JSON.stringify(ns)});
|
|
130
143
|
|
|
131
144
|
// The SDK's clientTimeout: past the backend's request deadline
|
|
132
145
|
// (ctx.requestTimeout, from backend.timeout_seconds), so its request_timeout
|
|
133
146
|
// reply arrives first. options.signal aborts the fetch, for a page the user leaves.
|
|
134
|
-
${ns}.
|
|
147
|
+
${ns}.callOnce = function (method, params, options, requestTimeout) {
|
|
135
148
|
var signal = options && options.signal;
|
|
136
149
|
var canceled = { code: "request_canceled", message: "The request was canceled" };
|
|
137
150
|
if (signal && signal.aborted) {
|
|
@@ -151,7 +164,7 @@ ${ns}.call = function (method, params, options, requestTimeout) {
|
|
|
151
164
|
var leave = function () { stop(canceled); };
|
|
152
165
|
var timer = setTimeout(function () {
|
|
153
166
|
stop({ code: "request_timeout", message: "The NAS did not answer in time" });
|
|
154
|
-
}, (requestTimeout || ${requestTimeout}) + ${
|
|
167
|
+
}, (requestTimeout || ${requestTimeout}) + ${clientMargin});
|
|
155
168
|
if (signal) signal.addEventListener("abort", leave);
|
|
156
169
|
var done = function () {
|
|
157
170
|
clearTimeout(timer);
|
|
@@ -178,6 +191,39 @@ ${ns}.call = function (method, params, options, requestTimeout) {
|
|
|
178
191
|
});
|
|
179
192
|
};
|
|
180
193
|
|
|
194
|
+
// APP_STOPPED, APP_STARTING and EXECUTOR_STARTING mean the executor op was not
|
|
195
|
+
// sent: the package is stopped, or starting. The call repeats on its own, half a
|
|
196
|
+
// second, then doubling up to 8 s, for 30 s of waiting in all; each attempt keeps
|
|
197
|
+
// its own deadline. Past that it rejects with the last refusal, and leaving the
|
|
198
|
+
// page during a wait rejects request_canceled at once.
|
|
199
|
+
${ns}.retryCodes = ["APP_STOPPED", "APP_STARTING", "EXECUTOR_STARTING"];
|
|
200
|
+
${ns}.call = function (method, params, options, requestTimeout) {
|
|
201
|
+
var signal = options && options.signal;
|
|
202
|
+
var left = 30000;
|
|
203
|
+
var delay = 500;
|
|
204
|
+
var attempt = function () {
|
|
205
|
+
return ${ns}.callOnce(method, params, options, requestTimeout).catch(function (error) {
|
|
206
|
+
if (left <= 0 || !error || ${ns}.retryCodes.indexOf(error.code) < 0) throw error;
|
|
207
|
+
var wait = Math.min(delay, left);
|
|
208
|
+
left -= wait;
|
|
209
|
+
delay = Math.min(delay * 2, 8000);
|
|
210
|
+
return new Promise(function (resolve, reject) {
|
|
211
|
+
var leave = function () {
|
|
212
|
+
clearTimeout(timer);
|
|
213
|
+
reject({ code: "request_canceled", message: "The request was canceled" });
|
|
214
|
+
};
|
|
215
|
+
var timer = setTimeout(function () {
|
|
216
|
+
if (signal) signal.removeEventListener("abort", leave);
|
|
217
|
+
resolve(attempt());
|
|
218
|
+
}, wait);
|
|
219
|
+
if (signal && signal.aborted) leave();
|
|
220
|
+
else if (signal) signal.addEventListener("abort", leave);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
};
|
|
224
|
+
return attempt();
|
|
225
|
+
};
|
|
226
|
+
|
|
181
227
|
${ns}.messages = ${JSON.stringify(manifest.i18n?.catalogs ?? { en: {} })};
|
|
182
228
|
${ns}.createI18n = (function () {
|
|
183
229
|
${I18N_RUNTIME}
|
|
@@ -201,7 +247,7 @@ ${ns}.sessionUser = function () {
|
|
|
201
247
|
return { name: typeof session.user === "string" ? session.user : "", isAdmin: session.is_admin === true };
|
|
202
248
|
};
|
|
203
249
|
|
|
204
|
-
${root ? root.definitions : ""}${manifest.portal ? renderPortalDefinition(ns) : ""}${ns}.loadApp = function (done) {
|
|
250
|
+
${root ? root.definitions : ""}${manifest.portal ? renderPortalDefinition(ns, portalText) : ""}${ns}.loadApp = function (done) {
|
|
205
251
|
var apps = window.DsmPackageApps || {};
|
|
206
252
|
if (apps[${p}]) {
|
|
207
253
|
done(apps[${p}]);
|
|
@@ -321,9 +367,9 @@ ${root ? root.mount : ""} ${ns}.loadApp(function (app) {
|
|
|
321
367
|
});
|
|
322
368
|
`;
|
|
323
369
|
}
|
|
324
|
-
function renderPortalDefinition(ns) {
|
|
370
|
+
function renderPortalDefinition(ns, text) {
|
|
325
371
|
return `${ns}.createPortalAccess = (function () {
|
|
326
|
-
${
|
|
372
|
+
${portalRuntime(text)}
|
|
327
373
|
return createPortalAccess;
|
|
328
374
|
})();
|
|
329
375
|
|