@synopackageland/cli 0.24.0 → 0.25.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 +12 -0
- package/README.md +10 -0
- package/dist/build-spk/ui-texts.d.ts +14 -2
- package/dist/build-spk/ui-texts.js +16 -4
- package/dist/contract/app-launch.d.ts +4 -1
- package/dist/contract/app-launch.js +2 -1
- package/dist/error-registry.js +20 -0
- package/dist/runtime/executor-digests.json +1 -1
- package/dist/runtime/sdk-source.js +1 -1
- package/dist/standalone/dsm-window.d.ts +6 -1
- package/dist/standalone/dsm-window.js +58 -10
- package/dist/standalone/inspect-spk.js +42 -7
- package/dist/standalone/lifecycle.d.ts +3 -0
- package/dist/standalone/lifecycle.js +18 -7
- package/dist/standalone/manifest.d.ts +9 -0
- package/dist/standalone/manifest.js +28 -0
- package/dist/standalone/spk.d.ts +5 -0
- package/dist/standalone/spk.js +13 -2
- package/package.json +2 -2
- package/skills/SKILL.md +21 -0
- package/skills/reference/portal.md +9 -0
- package/skills/reference/ui-json.md +29 -0
- package/skills/reference/ui-vue.md +41 -0
- package/skills/reference/upstream-fork.md +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.25.0 (2026-10-22)
|
|
4
|
+
|
|
5
|
+
- Add the opt-in `console` declaration for native Apps with `identity_gateway`: a native UI URL launcher and an administrator-only DSM console, selected by Package Center Open. Validate both entries and refuse incompatible launch declarations.
|
|
6
|
+
- Fix installation-settings inspection when native identity-gateway diagnostics precede the settings hook, retaining full lifecycle validation.
|
|
7
|
+
- Document punctuation rules for owned UI copy in the development skill.
|
|
8
|
+
- Support SDK/App 0.25.0 and templates 0.25.0, with UI 0.14.3 in Vue scaffolds. Reuse executor/v0.24.0 with unchanged protected executor/lifecycle payloads and Go 1.26.10. OIDC browser cold-login continuation requires SDK 0.25.0. No NAS acceptance is claimed.
|
|
9
|
+
|
|
10
|
+
## 0.24.1 (2026-10-22)
|
|
11
|
+
|
|
12
|
+
- Build SDK/App 0.24.1 with the verified immutable executor/v0.24.0 release and its exact executor/lifecycle digests; retain SDK/App 0.24.0 support. SDK 0.24.1 adds AppShell brand navigation to the first page and supports UI 0.14.3.
|
|
13
|
+
- Keep templates 0.24.0 and Go 1.26.10. Executor and lifecycle payloads remain unchanged.
|
|
14
|
+
|
|
3
15
|
## 0.24.0 (2026-10-22)
|
|
4
16
|
|
|
5
17
|
- Pin Go 1.26.10 for every SDK runtime build, addressing crypto/x509 GO-2026-6809. Ship the matching executor/lifecycle digest inventory for SDK 0.24.0; older release records are preserved in their release history.
|
package/README.md
CHANGED
|
@@ -268,6 +268,16 @@ A full page does not need Application Portal. Omit `portal` unless an alias is
|
|
|
268
268
|
explicitly wanted; only that opt-in adds `defaultAlias`, login styling, Portal
|
|
269
269
|
panels and capabilities. No Portal is a healthy default for inspect/info/doctor.
|
|
270
270
|
|
|
271
|
+
A native App with `identity_gateway` can declare `console` to get two main-menu
|
|
272
|
+
icons: the native UI (`displayname`, a `type: "url"` launcher to `sso/open.cgi`
|
|
273
|
+
that opens a new tab) and an administrator-only DSM window running the SDK UI
|
|
274
|
+
from `ui.dir`, named "<displayname> for DSM" by default. Package Center's Open
|
|
275
|
+
(INFO `dsmapplaunchname`) starts the console, and Application Privileges lists
|
|
276
|
+
only the native icon. `console: {}` keeps the default name; `console: { title }`
|
|
277
|
+
sets it for every language. Omit `console` to keep one icon. check refuses
|
|
278
|
+
`console` on Compose, without `identity_gateway`, or with `ui.launch` or
|
|
279
|
+
`portal`. A cold DSM sign-in resumes through the url launcher.
|
|
280
|
+
|
|
271
281
|
Compose uses schema-2 `dsm-package.yaml` and `ui.launch`; the default is new-page.
|
|
272
282
|
Classless gateways use type url via the shared contract/app-launch resolver;
|
|
273
283
|
AppWindow wrappers use standalone. Headless projects have no launcher.
|
|
@@ -18,6 +18,18 @@ export declare function uiTitleFields(app: AppTitle): {
|
|
|
18
18
|
texts?: string;
|
|
19
19
|
preloadTexts?: string[];
|
|
20
20
|
};
|
|
21
|
+
/** The console's name: the declared title in every language, else "<displayname> for DSM". */
|
|
22
|
+
export declare function consoleTitle(displayname: string, console: {
|
|
23
|
+
title?: string;
|
|
24
|
+
}): string;
|
|
25
|
+
/** {@link uiTitleFields} for the console window, which shares the App's description. */
|
|
26
|
+
export declare function uiConsoleTitleFields(app: AppTitle, console: {
|
|
27
|
+
title?: string;
|
|
28
|
+
}): ReturnType<typeof uiTitleFields>;
|
|
21
29
|
/** `ui/texts/<dsm lang>/strings` for English and every translated language; none without translations. */
|
|
22
|
-
export declare function renderUiTexts(app: AppTitle
|
|
23
|
-
|
|
30
|
+
export declare function renderUiTexts(app: AppTitle, console?: {
|
|
31
|
+
title?: string;
|
|
32
|
+
}): Map<string, string>;
|
|
33
|
+
export declare function writeUiTexts(uiRoot: string, app: AppTitle, console?: {
|
|
34
|
+
title?: string;
|
|
35
|
+
}): void;
|
|
@@ -4,6 +4,7 @@ import { DSM_LANGS, LOCALES } from "../standalone/i18n.js";
|
|
|
4
4
|
const SECTION = "synopkgland";
|
|
5
5
|
const TITLE = `${SECTION}:app_title`;
|
|
6
6
|
const DESC = `${SECTION}:app_desc`;
|
|
7
|
+
const CONSOLE_TITLE = `${SECTION}:console_title`;
|
|
7
8
|
/** `ui/` subdirectory with the strings; a `ui/config` entry's `texts` names it relative to `ui/`. */
|
|
8
9
|
const TEXTS_DIR = "texts";
|
|
9
10
|
function translated(app) {
|
|
@@ -21,8 +22,18 @@ export function uiTitleFields(app) {
|
|
|
21
22
|
? { title: TITLE, desc: DESC, texts: TEXTS_DIR, preloadTexts: [TITLE, DESC] }
|
|
22
23
|
: { title: app.displayname, desc: app.description };
|
|
23
24
|
}
|
|
25
|
+
/** The console's name: the declared title in every language, else "<displayname> for DSM". */
|
|
26
|
+
export function consoleTitle(displayname, console) {
|
|
27
|
+
return console.title ?? `${displayname} for DSM`;
|
|
28
|
+
}
|
|
29
|
+
/** {@link uiTitleFields} for the console window, which shares the App's description. */
|
|
30
|
+
export function uiConsoleTitleFields(app, console) {
|
|
31
|
+
return translated(app)
|
|
32
|
+
? { title: CONSOLE_TITLE, desc: DESC, texts: TEXTS_DIR, preloadTexts: [CONSOLE_TITLE, DESC] }
|
|
33
|
+
: { title: consoleTitle(app.displayname, console), desc: app.description };
|
|
34
|
+
}
|
|
24
35
|
/** `ui/texts/<dsm lang>/strings` for English and every translated language; none without translations. */
|
|
25
|
-
export function renderUiTexts(app) {
|
|
36
|
+
export function renderUiTexts(app, console) {
|
|
26
37
|
const files = new Map();
|
|
27
38
|
if (!translated(app))
|
|
28
39
|
return files;
|
|
@@ -32,12 +43,13 @@ export function renderUiTexts(app) {
|
|
|
32
43
|
continue;
|
|
33
44
|
const title = text?.displayname ?? app.displayname;
|
|
34
45
|
const desc = text?.description ?? app.description;
|
|
35
|
-
|
|
46
|
+
const consoleLine = console ? `console_title="${consoleTitle(title, console)}"\n` : "";
|
|
47
|
+
files.set(DSM_LANGS[locale], `[${SECTION}]\napp_title="${title}"\napp_desc="${desc}"\n${consoleLine}`);
|
|
36
48
|
}
|
|
37
49
|
return files;
|
|
38
50
|
}
|
|
39
|
-
export function writeUiTexts(uiRoot, app) {
|
|
40
|
-
for (const [lang, strings] of renderUiTexts(app)) {
|
|
51
|
+
export function writeUiTexts(uiRoot, app, console) {
|
|
52
|
+
for (const [lang, strings] of renderUiTexts(app, console)) {
|
|
41
53
|
mkdirSync(join(uiRoot, TEXTS_DIR, lang), { recursive: true });
|
|
42
54
|
const path = join(uiRoot, TEXTS_DIR, lang, "strings");
|
|
43
55
|
writeFileSync(path, strings, "utf8");
|
|
@@ -6,6 +6,9 @@ export declare const DEFAULT_WEB_LAUNCH: AppLaunchMode;
|
|
|
6
6
|
export declare function isAppLaunchMode(value: unknown): value is AppLaunchMode;
|
|
7
7
|
/** Both Compose routes use the same web default; a valid explicit choice wins. */
|
|
8
8
|
export declare function resolveAppLaunch(value: unknown, webService: boolean): AppLaunchMode | undefined;
|
|
9
|
-
|
|
9
|
+
/** A console always opens the SDK UI in a DSM window. */
|
|
10
|
+
export declare function resolvedSdkLaunch(manifest: Pick<StandaloneManifest, 'ui' | 'compose' | 'identityGateway'> & {
|
|
11
|
+
console?: unknown;
|
|
12
|
+
}): AppLaunchMode;
|
|
10
13
|
/** DSM rewrites standalone URLs to launchApp; only SDK wrappers provide that class. */
|
|
11
14
|
export declare function dsmLauncherType(launch: AppLaunchMode, sdkWindow: boolean): 'standalone' | 'app' | 'url';
|
|
@@ -9,8 +9,9 @@ export function resolveAppLaunch(value, webService) {
|
|
|
9
9
|
return value === undefined ? webService ? DEFAULT_WEB_LAUNCH : DEFAULT_NATIVE_LAUNCH
|
|
10
10
|
: isAppLaunchMode(value) ? value : undefined;
|
|
11
11
|
}
|
|
12
|
+
/** A console always opens the SDK UI in a DSM window. */
|
|
12
13
|
export function resolvedSdkLaunch(manifest) {
|
|
13
|
-
return resolveAppLaunch(manifest.ui.launch, Boolean(manifest.compose || manifest.identityGateway));
|
|
14
|
+
return resolveAppLaunch(manifest.console ? 'dsm-window' : manifest.ui.launch, Boolean(manifest.compose || manifest.identityGateway));
|
|
14
15
|
}
|
|
15
16
|
/** DSM rewrites standalone URLs to launchApp; only SDK wrappers provide that class. */
|
|
16
17
|
export function dsmLauncherType(launch, sdkWindow) {
|
package/dist/error-registry.js
CHANGED
|
@@ -532,6 +532,26 @@ export const ERROR_REGISTRY = {
|
|
|
532
532
|
description: "The portal format or alias in dsm-package.yaml is invalid.",
|
|
533
533
|
suggestion: "Use portal: {} or portal: { alias: my-app }; alias is 1-64 lowercase alphanumerics or -, and not a DSM reserved path.",
|
|
534
534
|
},
|
|
535
|
+
STANDALONE_CONSOLE_INVALID: {
|
|
536
|
+
code: "STANDALONE_CONSOLE_INVALID",
|
|
537
|
+
description: "The console format or title in dsm-package.yaml is invalid.",
|
|
538
|
+
suggestion: 'Use console: {} for "<displayname> for DSM", or console: { title: My App Admin } with 1-64 characters and no quotes, backslashes or control characters.',
|
|
539
|
+
},
|
|
540
|
+
STANDALONE_CONSOLE_REQUIRES_GATEWAY: {
|
|
541
|
+
code: "STANDALONE_CONSOLE_REQUIRES_GATEWAY",
|
|
542
|
+
description: "console is declared on a Compose App or an App without identity_gateway.",
|
|
543
|
+
suggestion: "Declare console only on a native App whose web UI the identity gateway serves; otherwise remove console and keep one icon.",
|
|
544
|
+
},
|
|
545
|
+
STANDALONE_CONSOLE_LAUNCH: {
|
|
546
|
+
code: "STANDALONE_CONSOLE_LAUNCH",
|
|
547
|
+
description: "console is declared together with ui.launch.",
|
|
548
|
+
suggestion: "Remove ui.launch: the console always opens in a DSM window and the native icon in a new tab.",
|
|
549
|
+
},
|
|
550
|
+
STANDALONE_CONSOLE_PORTAL: {
|
|
551
|
+
code: "STANDALONE_CONSOLE_PORTAL",
|
|
552
|
+
description: "console is declared together with portal.",
|
|
553
|
+
suggestion: "Declare either console or portal, not both.",
|
|
554
|
+
},
|
|
535
555
|
STANDALONE_PORTAL_REQUIRES_EXECUTOR: {
|
|
536
556
|
code: "STANDALONE_PORTAL_REQUIRES_EXECUTOR",
|
|
537
557
|
description: "An App targeting DSM below 7.2-64570 declares portal without an executor.",
|
|
@@ -76,7 +76,7 @@ function goMod(backendDir) {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
/** SDK releases that reuse an immutable executor release with identical payload bytes. */
|
|
79
|
-
const EXECUTOR_RELEASES = { '0.21.0': '0.20.0', '0.21.1': '0.20.0', '0.22.0': '0.20.0', '0.23.0': '0.20.0', '0.23.1': '0.20.0', '0.23.2': '0.20.0' };
|
|
79
|
+
const EXECUTOR_RELEASES = { '0.21.0': '0.20.0', '0.21.1': '0.20.0', '0.22.0': '0.20.0', '0.23.0': '0.20.0', '0.23.1': '0.20.0', '0.23.2': '0.20.0', '0.24.1': '0.24.0', '0.25.0': '0.24.0' };
|
|
80
80
|
/**
|
|
81
81
|
* The module directories of one SDK version. SYNOPKGLAND_SDK_DIR names a
|
|
82
82
|
* local sdk checkout (development and gates only); otherwise modules come from
|
|
@@ -25,8 +25,13 @@ export declare function windowScriptName(manifest: Pick<StandaloneManifest, "bui
|
|
|
25
25
|
/** The launcher fields an entry mode fixes; inspect checks a built SPK against them. */
|
|
26
26
|
export declare function entryLauncherAccess(entry: EntryMode): Record<string, unknown>;
|
|
27
27
|
export declare function appTitle(manifest: StandaloneManifest): AppTitle;
|
|
28
|
+
/** The DSM app class whose launch opens the SDK window: the console's when the App declares one. */
|
|
29
|
+
export declare function windowAppId(manifest: Pick<StandaloneManifest, "package" | "console">): string;
|
|
28
30
|
/**
|
|
29
|
-
* `ui/config`: registers the DSM main-menu app and its window class.
|
|
31
|
+
* `ui/config`: registers the DSM main-menu app and its window class. With a
|
|
32
|
+
* console, `.Instance` is the gateway's url launcher (new tab) and `.Console`
|
|
33
|
+
* the administrator-only window; DSM lists only `.Instance` under Application
|
|
34
|
+
* Privileges. A portal
|
|
30
35
|
* App's launcher also carries the Login Portal style and its icon as the
|
|
31
36
|
* login page icon (DSM shows `loginIcon`, never `icon`, there).
|
|
32
37
|
*/
|
|
@@ -9,7 +9,7 @@ import { HOST_ADAPTER_RUNTIME } from "./host-adapter.js";
|
|
|
9
9
|
import { permitsOrdinaryUsers } from "./api-permissions.js";
|
|
10
10
|
import { portalRuntime } from "./portal-window.js";
|
|
11
11
|
import { portalByExecutor } from "../build-spk/dsm-version.js";
|
|
12
|
-
import { uiTitleFields } from "../build-spk/ui-texts.js";
|
|
12
|
+
import { consoleTitle, uiConsoleTitleFields, uiTitleFields } from "../build-spk/ui-texts.js";
|
|
13
13
|
import { packageIcons, windowIconFile, windowIconTemplate, } from "../build-spk/dsm-icons.js";
|
|
14
14
|
import { DEFAULT_WINDOW, FULL_PAGE_MIN_WIDTH } from "./window-size.js";
|
|
15
15
|
import { requestTimeoutSeconds } from "./request-timeout.js";
|
|
@@ -74,7 +74,7 @@ function launcherAccess(manifest) {
|
|
|
74
74
|
return manifest.entry
|
|
75
75
|
? entryLauncherAccess(manifest.entry)
|
|
76
76
|
: {
|
|
77
|
-
allUsers: manifest.compose
|
|
77
|
+
allUsers: manifest.compose || manifest.console
|
|
78
78
|
? manifest.identityGateway?.access !== "dsm-admins"
|
|
79
79
|
: permitsOrdinaryUsers(manifest.apiPermissions),
|
|
80
80
|
};
|
|
@@ -92,19 +92,35 @@ export function appTitle(manifest) {
|
|
|
92
92
|
info: manifest.i18n?.info,
|
|
93
93
|
};
|
|
94
94
|
}
|
|
95
|
-
/**
|
|
95
|
+
/**
|
|
96
|
+
* The window's name by locale, as `ui/texts` names it: `createI18n(lang,
|
|
97
|
+
* ...).t("app_title")`. A console window carries the console's name.
|
|
98
|
+
*/
|
|
96
99
|
function displayNameCatalogs(manifest) {
|
|
100
|
+
const name = (displayname) => manifest.console ? consoleTitle(displayname, manifest.console) : displayname;
|
|
97
101
|
const catalogs = {
|
|
98
|
-
en: { app_title: manifest.displayname },
|
|
102
|
+
en: { app_title: name(manifest.displayname) },
|
|
99
103
|
};
|
|
100
104
|
for (const [locale, text] of Object.entries(manifest.i18n?.info ?? {})) {
|
|
101
105
|
if (text?.displayname)
|
|
102
|
-
catalogs[locale] = { app_title: text.displayname };
|
|
106
|
+
catalogs[locale] = { app_title: name(text.displayname) };
|
|
103
107
|
}
|
|
104
108
|
return catalogs;
|
|
105
109
|
}
|
|
110
|
+
/** The DSM app class whose launch opens the SDK window: the console's when the App declares one. */
|
|
111
|
+
export function windowAppId(manifest) {
|
|
112
|
+
return `SYNO.SDS.${manifest.package}.${manifest.console ? "Console" : "Instance"}`;
|
|
113
|
+
}
|
|
114
|
+
/** Whether the window itself resumes a cold DSM sign-in; with a console the url launcher does. */
|
|
115
|
+
function windowResumesOIDC(manifest) {
|
|
116
|
+
const oidc = manifest.identityGateway?.oidc;
|
|
117
|
+
return Boolean((oidc?.native_login || oidc?.browser) && !manifest.console);
|
|
118
|
+
}
|
|
106
119
|
/**
|
|
107
|
-
* `ui/config`: registers the DSM main-menu app and its window class.
|
|
120
|
+
* `ui/config`: registers the DSM main-menu app and its window class. With a
|
|
121
|
+
* console, `.Instance` is the gateway's url launcher (new tab) and `.Console`
|
|
122
|
+
* the administrator-only window; DSM lists only `.Instance` under Application
|
|
123
|
+
* Privileges. A portal
|
|
108
124
|
* App's launcher also carries the Login Portal style and its icon as the
|
|
109
125
|
* login page icon (DSM shows `loginIcon`, never `icon`, there).
|
|
110
126
|
*/
|
|
@@ -127,6 +143,38 @@ export function renderUiConfig(manifest, icons = packageIcons(manifest.icon), lo
|
|
|
127
143
|
},
|
|
128
144
|
}, null, 2) + "\n");
|
|
129
145
|
}
|
|
146
|
+
if (manifest.console) {
|
|
147
|
+
const consoleFields = uiConsoleTitleFields(appTitle(manifest), manifest.console);
|
|
148
|
+
return `${JSON.stringify({
|
|
149
|
+
".url": {
|
|
150
|
+
[`${ns}.Instance`]: {
|
|
151
|
+
type: "url",
|
|
152
|
+
...titleFields,
|
|
153
|
+
icon,
|
|
154
|
+
...launcherAccess(manifest),
|
|
155
|
+
url: gatewayEntryPath(manifest),
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
[windowScriptName(manifest)]: {
|
|
159
|
+
[`${ns}.Console`]: {
|
|
160
|
+
type: "app",
|
|
161
|
+
...consoleFields,
|
|
162
|
+
icon,
|
|
163
|
+
appWindow: `${ns}.MainWindow`,
|
|
164
|
+
allowMultiInstance: false,
|
|
165
|
+
allUsers: false,
|
|
166
|
+
depend: [`${ns}.MainWindow`],
|
|
167
|
+
},
|
|
168
|
+
[`${ns}.MainWindow`]: {
|
|
169
|
+
type: "lib",
|
|
170
|
+
title: consoleFields.title,
|
|
171
|
+
texts: consoleFields.texts,
|
|
172
|
+
icon,
|
|
173
|
+
depend: [],
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
}, null, 2)}\n`;
|
|
177
|
+
}
|
|
130
178
|
return `${JSON.stringify({
|
|
131
179
|
[windowScriptName(manifest)]: {
|
|
132
180
|
[`${ns}.Instance`]: {
|
|
@@ -379,7 +427,7 @@ ${ns}.prepareFullPage = function (title) {
|
|
|
379
427
|
favicon.href = ${ns}.appIcon;
|
|
380
428
|
};
|
|
381
429
|
|
|
382
|
-
${(manifest
|
|
430
|
+
${windowResumesOIDC(manifest) ? `${ns}.resumeOIDC = ${oidcResumeRuntime(pkg, Object.values(manifest.identityGateway.listen).filter((p) => typeof p === "number"))};` : ""}
|
|
383
431
|
|
|
384
432
|
// DSM reopens a window at the size it was last left at, even one saved on a
|
|
385
433
|
// larger screen, and only moves a window that does not fit: keep it inside.
|
|
@@ -390,7 +438,7 @@ ${ns}.fitDesktop = function (size) {
|
|
|
390
438
|
return size;
|
|
391
439
|
};
|
|
392
440
|
|
|
393
|
-
Ext.define(${JSON.stringify(
|
|
441
|
+
Ext.define(${JSON.stringify(windowAppId(manifest))}, {
|
|
394
442
|
extend: "SYNO.SDS.AppInstance",
|
|
395
443
|
appWindowName: ${JSON.stringify(`${ns}.MainWindow`)}
|
|
396
444
|
});
|
|
@@ -408,7 +456,7 @@ Ext.define(${JSON.stringify(`${ns}.MainWindow`)}, {
|
|
|
408
456
|
},
|
|
409
457
|
onOpen: function (param) {
|
|
410
458
|
this.callParent(arguments);
|
|
411
|
-
${(manifest
|
|
459
|
+
${windowResumesOIDC(manifest) ? `if (${ns}.resumeOIDC(param)) return;` : ""}
|
|
412
460
|
var me = this;
|
|
413
461
|
if (me.mounted) {
|
|
414
462
|
me.reopenEvents.emit();
|
|
@@ -471,7 +519,7 @@ ${root ? root.mount : ""} ${ns}.loadApp(function (app) {
|
|
|
471
519
|
// with that launch's param. Before the App mounts, it mounts with the latest.
|
|
472
520
|
onRequest: function (param) {
|
|
473
521
|
this.callParent(arguments);
|
|
474
|
-
${(manifest
|
|
522
|
+
${windowResumesOIDC(manifest) ? `if (${ns}.resumeOIDC(param)) return;` : ""}
|
|
475
523
|
var params = ${ns}.launchParams(param);
|
|
476
524
|
if (!this.mounted) {
|
|
477
525
|
if (this.mountPending && Object.keys(params).length) this.launchParams = params;
|
|
@@ -36,7 +36,7 @@ import { renderNativePrivilege } from "../build-spk/native-lifecycle.js";
|
|
|
36
36
|
import { renderUiConfig } from "./dsm-window.js";
|
|
37
37
|
import { identityGatewayConfig, gatewayAdmissionReport, gatewayAdmissionPolicy, readIdentityGateway, } from "./identity-gateway.js";
|
|
38
38
|
import { checkCgiBinary } from "./binary.js";
|
|
39
|
-
import { matchesStandaloneScript, renderStandaloneScript } from "./lifecycle.js";
|
|
39
|
+
import { matchesStandaloneScript, renderStandaloneScript, matchesStandaloneScriptPrefix } from "./lifecycle.js";
|
|
40
40
|
import { listPortExposures, loadCompose } from "../compose/analyze.js";
|
|
41
41
|
import { uninstallCleanup, } from "../runtime/uninstall-cleanup.js";
|
|
42
42
|
import { PORTAL_JSON_RELPATH, PORTAL_MUSTACHE_RELPATH, renderPortalJson, renderPortalLifecycle, renderPortalMustache, renderPortalWizardStep, } from "../portal/package.js";
|
|
@@ -112,7 +112,8 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
112
112
|
].includes(String(name))) ||
|
|
113
113
|
new Set(raw.capabilities).size !== raw.capabilities.length ||
|
|
114
114
|
(raw.entry !== undefined &&
|
|
115
|
-
!["admin-only", "app-privilege"].includes(raw.entry))
|
|
115
|
+
!["admin-only", "app-privilege"].includes(raw.entry)) ||
|
|
116
|
+
(raw.console !== undefined && raw.console !== true))
|
|
116
117
|
throw new Error();
|
|
117
118
|
if (raw.lifecycleTimeouts !== undefined && (!readLifecycleTimeouts(raw.lifecycleTimeouts, () => { }) || Object.keys(raw.lifecycleTimeouts).length !== 4))
|
|
118
119
|
throw new Error();
|
|
@@ -272,7 +273,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
272
273
|
}
|
|
273
274
|
if (metadata.entry)
|
|
274
275
|
await checkEntryLauncher(dir, info.package, metadata.entry, findings);
|
|
275
|
-
const launch = await checkLaunchConfiguration(dir, info, standalone, metadata.launch, portal?.alias ?? executor?.policy.portal?.alias, findings);
|
|
276
|
+
const launch = await checkLaunchConfiguration(dir, info, standalone, metadata.launch, portal?.alias ?? executor?.policy.portal?.alias, findings, metadata.console === true);
|
|
276
277
|
const loginStyle = standalone
|
|
277
278
|
? await inspectLoginStyle(dir, info.package, Boolean(portal || executor?.policy.portal), findings)
|
|
278
279
|
: undefined;
|
|
@@ -336,7 +337,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
336
337
|
const LAUNCHER_TYPE_MISMATCH = "LAUNCHER_TYPE_MISMATCH";
|
|
337
338
|
const LAUNCHER_PORTAL_MISMATCH = "LAUNCHER_PORTAL_MISMATCH";
|
|
338
339
|
/** Compare the actual launcher and Portal fields to the packaged declaration. */
|
|
339
|
-
async function checkLaunchConfiguration(dir, info, native, declared, alias, findings) {
|
|
340
|
+
async function checkLaunchConfiguration(dir, info, native, declared, alias, findings, console = false) {
|
|
340
341
|
const target = join(dir, "launch-inspection");
|
|
341
342
|
mkdirSync(target);
|
|
342
343
|
await tar.x({
|
|
@@ -387,6 +388,10 @@ async function checkLaunchConfiguration(dir, info, native, declared, alias, find
|
|
|
387
388
|
catch {
|
|
388
389
|
/* report the missing or malformed launcher below */
|
|
389
390
|
}
|
|
391
|
+
if (console) {
|
|
392
|
+
checkConsoleLaunchers(target, info, native, findings);
|
|
393
|
+
return launch;
|
|
394
|
+
}
|
|
390
395
|
const type = dsmLauncherType(launch, native);
|
|
391
396
|
if (!launcher ||
|
|
392
397
|
launcher.type !== type ||
|
|
@@ -406,6 +411,35 @@ async function checkLaunchConfiguration(dir, info, native, declared, alias, find
|
|
|
406
411
|
}
|
|
407
412
|
return launch;
|
|
408
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* A console App's ui/config: `.Instance` is the url launcher to the gateway,
|
|
416
|
+
* `.Console` the administrator-only SDK window Package Center's Open starts.
|
|
417
|
+
*/
|
|
418
|
+
function checkConsoleLaunchers(target, info, native, findings) {
|
|
419
|
+
const ns = `SYNO.SDS.${info.package}`;
|
|
420
|
+
let config = {};
|
|
421
|
+
try {
|
|
422
|
+
config = JSON.parse(readFileSync(join(target, "ui/config"), "utf8"));
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
/* reported below as a missing launcher */
|
|
426
|
+
}
|
|
427
|
+
const find = (id) => Object.values(config).map((entries) => entries?.[id]).find(Boolean);
|
|
428
|
+
const instance = find(`${ns}.Instance`);
|
|
429
|
+
const console = find(`${ns}.Console`);
|
|
430
|
+
if (!native ||
|
|
431
|
+
instance?.type !== "url" ||
|
|
432
|
+
instance.url !== `/webman/3rdparty/${info.package}/sso/open.cgi` ||
|
|
433
|
+
console?.type !== "app" ||
|
|
434
|
+
console.appWindow !== `${ns}.MainWindow` ||
|
|
435
|
+
console.allUsers !== false ||
|
|
436
|
+
console.grantPrivilege !== undefined ||
|
|
437
|
+
info.dsmuidir !== "ui" ||
|
|
438
|
+
info.dsmappname !== `${ns}.Instance ${ns}.Console` ||
|
|
439
|
+
info.dsmapplaunchname !== `${ns}.Console`) {
|
|
440
|
+
findings.push(`${LAUNCHER_TYPE_MISMATCH} console requires a url launcher at ${ns}.Instance to sso/open.cgi, an administrator-only app launcher at ${ns}.Console and INFO dsmapplaunchname=${ns}.Console; rebuild with synopkgland build`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
409
443
|
/** The launcher in ui/config must carry exactly the access the declared entry mode fixes. */
|
|
410
444
|
async function checkEntryLauncher(dir, pkg, entry, findings) {
|
|
411
445
|
const target = join(dir, "entry-inspection");
|
|
@@ -1229,8 +1263,8 @@ async function inspectInstallSettings(dir, payload, metadata, findings) {
|
|
|
1229
1263
|
mkdirSync(root);
|
|
1230
1264
|
await tar.x({ file: join(dir, 'package.tgz'), cwd: root });
|
|
1231
1265
|
const info = readInfo(join(dir, 'INFO'), findings, 'binary', Boolean(metadata.compose));
|
|
1232
|
-
const gateway = metadata.compose?.gateway ??
|
|
1233
|
-
const m = { package: info.package, installSettings: settings, identityGateway: gateway };
|
|
1266
|
+
const gateway = metadata.compose?.gateway ?? metadata.native_gateway;
|
|
1267
|
+
const m = { package: info.package, os_min_ver: info.os_min_ver, capabilities: {}, installSettings: settings, identityGateway: gateway };
|
|
1234
1268
|
try {
|
|
1235
1269
|
if (readOptional(join(root, 'install/settings.json')) !== JSON.stringify(settingsConfig(m), null, 2) + '\n')
|
|
1236
1270
|
throw new Error('settings inventory differs from its declaration or gateway port');
|
|
@@ -1243,7 +1277,8 @@ async function inspectInstallSettings(dir, payload, metadata, findings) {
|
|
|
1243
1277
|
// App wrappers remain intact here; remove exactly the SDK prefix within them.
|
|
1244
1278
|
for (const name of ['preinst', 'postinst', 'postupgrade', 'preuninst']) {
|
|
1245
1279
|
const file = join(dir, 'scripts', name), text = readOptional(file), prefix = renderSettingsHook(name, m);
|
|
1246
|
-
|
|
1280
|
+
const sdk = text && sdkScriptOf(name, text)?.sdk;
|
|
1281
|
+
if (!sdk || !matchesStandaloneScriptPrefix(name, m, sdk) || text.split(prefix).length !== 2)
|
|
1247
1282
|
throw new Error(`scripts/${name} lacks the exact SDK settings hook`);
|
|
1248
1283
|
writeFileSync(file, text.replace(prefix, ''));
|
|
1249
1284
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { StandaloneManifest } from "./manifest.js";
|
|
2
2
|
/** Package-account lifecycle hooks for CGI, daemon and background packages. */
|
|
3
3
|
export declare function renderStandaloneScript(name: string, manifest: StandaloneManifest): string;
|
|
4
|
+
/** Settings inspection peels its hook only at the position build's renderer writes.
|
|
5
|
+
* The backend inspector then replays the entire remaining lifecycle script. */
|
|
6
|
+
export declare function matchesStandaloneScriptPrefix(name: string, manifest: StandaloneManifest, actual: string): boolean;
|
|
4
7
|
/** Accept the exact current, diagnostic-only, and released lifecycle variants. */
|
|
5
8
|
export declare function matchesStandaloneScript(name: string, manifest: StandaloneManifest, actual: string | undefined): boolean;
|
|
@@ -15,23 +15,34 @@ const UNINSTALL_ONLY = '[ "${SYNOPKG_PKG_STATUS:-}" = UPGRADE ] || synopkgland_u
|
|
|
15
15
|
/** Package-account lifecycle hooks for CGI, daemon and background packages. */
|
|
16
16
|
export function renderStandaloneScript(name, manifest) {
|
|
17
17
|
const script = renderStandaloneScriptInternal(name, manifest);
|
|
18
|
-
const
|
|
19
|
-
const prefix = renderNativeGatewayDiagnostic(manifest, name) + renderNativeGatewayPortWait(manifest, name) + renderSettingsHook(name, manifest) + prepared;
|
|
18
|
+
const prefix = renderStandaloneScriptPrefix(name, manifest);
|
|
20
19
|
const sdk = prefix ? script.replace('#!/bin/sh\n', () => '#!/bin/sh\n' + prefix) : script;
|
|
21
20
|
return manifest.appScripts?.some((app) => app === name)
|
|
22
21
|
? composeAppScript(name, sdk)
|
|
23
22
|
: sdk;
|
|
24
23
|
}
|
|
24
|
+
function renderStandaloneScriptPrefix(name, manifest) {
|
|
25
|
+
const prepared = ['postinst', 'postupgrade'].includes(name) ? renderNativeGatewayPrepare(manifest) : '';
|
|
26
|
+
return renderNativeGatewayDiagnostic(manifest, name) + renderNativeGatewayPortWait(manifest, name) + renderSettingsHook(name, manifest) + prepared;
|
|
27
|
+
}
|
|
28
|
+
/** Settings inspection peels its hook only at the position build's renderer writes.
|
|
29
|
+
* The backend inspector then replays the entire remaining lifecycle script. */
|
|
30
|
+
export function matchesStandaloneScriptPrefix(name, manifest, actual) {
|
|
31
|
+
const blocks = [renderNativeGatewayDiagnostic(manifest, name), renderNativeGatewayPortWait(manifest, name)].filter(Boolean);
|
|
32
|
+
return standaloneScriptVariants(name, manifest, renderStandaloneScriptPrefix(name, manifest)).some(prefix => actual.startsWith('#!/bin/sh\n' + prefix) &&
|
|
33
|
+
// A released prefix omits these blocks entirely; moving one after the
|
|
34
|
+
// settings hook must not masquerade as that released variant.
|
|
35
|
+
blocks.every(block => prefix.includes(block) || !actual.includes(block)));
|
|
36
|
+
}
|
|
25
37
|
/** Accept the exact current, diagnostic-only, and released lifecycle variants. */
|
|
26
38
|
export function matchesStandaloneScript(name, manifest, actual) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
39
|
+
return actual !== undefined && standaloneScriptVariants(name, manifest, renderStandaloneScript(name, manifest)).includes(actual);
|
|
40
|
+
}
|
|
41
|
+
function standaloneScriptVariants(name, manifest, expected) {
|
|
30
42
|
const diagnostic = renderNativeGatewayDiagnostic(manifest, name);
|
|
31
43
|
const wait = renderNativeGatewayPortWait(manifest, name);
|
|
32
44
|
const diagnosticOnly = wait ? expected.replace(wait, '') : expected;
|
|
33
|
-
return (wait
|
|
34
|
-
(diagnostic !== '' && actual === diagnosticOnly.replace(diagnostic, ''));
|
|
45
|
+
return [expected, ...(wait ? [diagnosticOnly] : []), ...(diagnostic ? [diagnosticOnly.replace(diagnostic, '')] : [])];
|
|
35
46
|
}
|
|
36
47
|
function renderStandaloneScriptInternal(name, manifest) {
|
|
37
48
|
if (manifest.compose) {
|
|
@@ -100,6 +100,12 @@ export interface StandaloneManifest {
|
|
|
100
100
|
entry?: EntryMode;
|
|
101
101
|
/** `portal`: registers the DSM Application Portal entry; an administrator activates it. */
|
|
102
102
|
portal?: PortalDeclaration;
|
|
103
|
+
/**
|
|
104
|
+
* `console`: the main menu gets two icons, the gateway's native UI in a new
|
|
105
|
+
* tab and the SDK UI in an administrator-only DSM window titled `title`
|
|
106
|
+
* (default "<displayname> for DSM"). Absent keeps the single window icon.
|
|
107
|
+
*/
|
|
108
|
+
console?: ConsoleDeclaration;
|
|
103
109
|
/** The project's dsm-scripts/<DSM script name> files, each run inside the generated script of that name. */
|
|
104
110
|
appScripts?: AppScriptName[];
|
|
105
111
|
/** `icon`: a square PNG of at least 256 px, or a directory of icon_<size>.png; absent uses the SDK default. */
|
|
@@ -112,6 +118,9 @@ export interface StandaloneManifest {
|
|
|
112
118
|
* accounts DSM's Control Panel > Application Privileges allows for the app.
|
|
113
119
|
*/
|
|
114
120
|
export type EntryMode = "admin-only" | "app-privilege";
|
|
121
|
+
export interface ConsoleDeclaration {
|
|
122
|
+
title?: string;
|
|
123
|
+
}
|
|
115
124
|
export interface Capabilities {
|
|
116
125
|
composeMounts?: true;
|
|
117
126
|
webapi?: WebAPIProfiles;
|
|
@@ -56,6 +56,7 @@ export const KNOWN_FIELDS = new Set([
|
|
|
56
56
|
"uninstall",
|
|
57
57
|
"entry",
|
|
58
58
|
"portal",
|
|
59
|
+
"console",
|
|
59
60
|
"icon",
|
|
60
61
|
"reproducible",
|
|
61
62
|
"root",
|
|
@@ -65,6 +66,7 @@ const I18N_FIELDS = new Set(["messages", "glossary", "info"]);
|
|
|
65
66
|
const PACKAGE_ID = /^[A-Za-z][A-Za-z0-9]{1,63}$/;
|
|
66
67
|
const VERSION = /^\d+(\.\d+){0,3}$/;
|
|
67
68
|
const OS_MIN_VER = /^\d+\.\d+(\.\d+)?-\d+$/;
|
|
69
|
+
const CONSOLE_TITLE = /^[^"\\\x00-\x1f\x7f]{1,64}$/;
|
|
68
70
|
const GO_MAIN = /^\.\/[A-Za-z0-9_./-]*$/;
|
|
69
71
|
function isRecord(value) {
|
|
70
72
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -399,6 +401,7 @@ function readManifest(raw, c) {
|
|
|
399
401
|
: undefined;
|
|
400
402
|
if (entryProblem)
|
|
401
403
|
c.fail("STANDALONE_PORTAL_ENTRY_INVALID", entryProblem);
|
|
404
|
+
const dsmConsole = raw.console === undefined ? undefined : readConsole(raw, ui?.launch, c);
|
|
402
405
|
const loginStyle = raw.login_style === undefined
|
|
403
406
|
? undefined
|
|
404
407
|
: readLoginStyle(raw.login_style, raw.portal !== undefined, c);
|
|
@@ -454,6 +457,7 @@ function readManifest(raw, c) {
|
|
|
454
457
|
...(installSettings ? { installSettings } : {}),
|
|
455
458
|
...(entry ? { entry } : {}),
|
|
456
459
|
...(portal ? { portal } : {}),
|
|
460
|
+
...(dsmConsole ? { console: dsmConsole } : {}),
|
|
457
461
|
...(icon ? { icon } : {}),
|
|
458
462
|
...(loginStyle ? { loginStyle } : {}),
|
|
459
463
|
...(appScripts ? { appScripts } : {}),
|
|
@@ -503,6 +507,30 @@ const LOGIN_STYLE_FIELDS = [
|
|
|
503
507
|
"wallpaper_thumbnail",
|
|
504
508
|
"title",
|
|
505
509
|
];
|
|
510
|
+
/**
|
|
511
|
+
* `console`: {} or { title }. The native icon is the gateway's url launcher,
|
|
512
|
+
* so the App needs identity_gateway and its own SDK window; that window is the
|
|
513
|
+
* console, never a new page or a portal.
|
|
514
|
+
*/
|
|
515
|
+
function readConsole(raw, launch, c) {
|
|
516
|
+
const value = raw.console;
|
|
517
|
+
if (!isRecord(value) ||
|
|
518
|
+
Object.keys(value).some((k) => k !== "title") ||
|
|
519
|
+
(value.title !== undefined &&
|
|
520
|
+
(typeof value.title !== "string" || !CONSOLE_TITLE.test(value.title)))) {
|
|
521
|
+
c.fail("STANDALONE_CONSOLE_INVALID", 'console must be {} or { title: <1-64 characters, no quotes, backslashes or control characters> }.');
|
|
522
|
+
return undefined;
|
|
523
|
+
}
|
|
524
|
+
if (raw.identity_gateway === undefined || raw.compose !== undefined) {
|
|
525
|
+
c.fail("STANDALONE_CONSOLE_REQUIRES_GATEWAY", "console needs identity_gateway on an App with its own SDK UI: the native icon opens the gateway, the console opens the SDK UI. Compose and Apps without a gateway keep one icon.");
|
|
526
|
+
return undefined;
|
|
527
|
+
}
|
|
528
|
+
if (launch !== undefined)
|
|
529
|
+
c.fail("STANDALONE_CONSOLE_LAUNCH", "console always opens in a DSM window and the native icon in a new tab; remove ui.launch.");
|
|
530
|
+
if (raw.portal !== undefined)
|
|
531
|
+
c.fail("STANDALONE_CONSOLE_PORTAL", "console and portal both claim the App's SDK UI; declare one of them.");
|
|
532
|
+
return value.title === undefined ? {} : { title: value.title };
|
|
533
|
+
}
|
|
506
534
|
/** `login_style`: a portal's colour, wallpaper (with its thumbnail) and title image, each optional. */
|
|
507
535
|
function readLoginStyle(value, portal, c) {
|
|
508
536
|
const invalid = (message) => c.fail("STANDALONE_LOGIN_STYLE_INVALID", message);
|
package/dist/standalone/spk.d.ts
CHANGED
|
@@ -36,4 +36,9 @@ export interface AssembleOptions {
|
|
|
36
36
|
*/
|
|
37
37
|
export declare function assembleStandaloneSpk(options: AssembleOptions): Promise<void>;
|
|
38
38
|
/** Manifest validation already refused quotes, backslashes and newlines. */
|
|
39
|
+
/**
|
|
40
|
+
* INFO `dsmappname` lists the App's launchable classes. With a console,
|
|
41
|
+
* Package Center's Open (`dsmapplaunchname`) opens the console window.
|
|
42
|
+
*/
|
|
43
|
+
export declare function consoleInfo(manifest: Pick<StandaloneManifest, "package" | "console">): Array<[string, string]>;
|
|
39
44
|
export declare function renderInfo(manifest: StandaloneManifest, arch: NativeArchFamily, checksum: string, executorDigest?: string): string;
|
package/dist/standalone/spk.js
CHANGED
|
@@ -91,7 +91,7 @@ export async function assembleStandaloneSpk(options) {
|
|
|
91
91
|
writeFile(join(uiRoot, windowScriptName(manifest)), renderWindowScript(manifest, assetVersion, icons, options.portalText));
|
|
92
92
|
writeFile(join(uiRoot, hostScriptName(manifest.build)), renderHostAdapterScript(manifest.package));
|
|
93
93
|
writeDsmWindowIcons(uiRoot, icons);
|
|
94
|
-
writeUiTexts(uiRoot, appTitle(manifest));
|
|
94
|
+
writeUiTexts(uiRoot, appTitle(manifest), manifest.console);
|
|
95
95
|
if (options.apiDescription)
|
|
96
96
|
writeFile(join(staging, "package", "synopkgland-api.json"), JSON.stringify(options.apiDescription) + "\n");
|
|
97
97
|
const cgiTarget = join(staging, "package", CGI_PAYLOAD_PATH);
|
|
@@ -242,6 +242,7 @@ export async function assembleStandaloneSpk(options) {
|
|
|
242
242
|
}
|
|
243
243
|
: {}),
|
|
244
244
|
...(manifest.entry ? { entry: manifest.entry } : {}),
|
|
245
|
+
...(manifest.console ? { console: true } : {}),
|
|
245
246
|
...(manifest.uninstall
|
|
246
247
|
? {
|
|
247
248
|
uninstall: uninstallRecord(manifest.uninstall, manifest.i18n?.catalogs),
|
|
@@ -319,6 +320,16 @@ function quote(value) {
|
|
|
319
320
|
return `"${value.replace(/([\\"$`])/g, "\\$1")}"`;
|
|
320
321
|
}
|
|
321
322
|
/** Manifest validation already refused quotes, backslashes and newlines. */
|
|
323
|
+
/**
|
|
324
|
+
* INFO `dsmappname` lists the App's launchable classes. With a console,
|
|
325
|
+
* Package Center's Open (`dsmapplaunchname`) opens the console window.
|
|
326
|
+
*/
|
|
327
|
+
export function consoleInfo(manifest) {
|
|
328
|
+
const ns = `SYNO.SDS.${manifest.package}`;
|
|
329
|
+
return manifest.console
|
|
330
|
+
? [["dsmappname", `${ns}.Instance ${ns}.Console`], ["dsmapplaunchname", `${ns}.Console`]]
|
|
331
|
+
: [["dsmappname", `${ns}.Instance`]];
|
|
332
|
+
}
|
|
322
333
|
export function renderInfo(manifest, arch, checksum, executorDigest) {
|
|
323
334
|
const fields = [
|
|
324
335
|
["package", manifest.package],
|
|
@@ -329,7 +340,7 @@ export function renderInfo(manifest, arch, checksum, executorDigest) {
|
|
|
329
340
|
["arch", infoArchValue(arch)],
|
|
330
341
|
["os_min_ver", manifest.os_min_ver],
|
|
331
342
|
["dsmuidir", "ui"],
|
|
332
|
-
|
|
343
|
+
...consoleInfo(manifest),
|
|
333
344
|
["thirdparty", "yes"],
|
|
334
345
|
["startable", "yes"],
|
|
335
346
|
["checksum", checksum],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synopackageland/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Syno Package Land Developer Kit CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@napi-rs/keyring": "^2.1.1",
|
|
30
|
-
"@synopackageland/templates": "0.
|
|
30
|
+
"@synopackageland/templates": "0.25.0",
|
|
31
31
|
"ajv": "^8.17.1",
|
|
32
32
|
"commander": "^13.1.0",
|
|
33
33
|
"json-canonicalize": "^3.0.1",
|
package/skills/SKILL.md
CHANGED
|
@@ -27,6 +27,18 @@ Read [reference/ui-vue.md](reference/ui-vue.md) when editing Vue components in t
|
|
|
27
27
|
|
|
28
28
|
Read [reference/ui-json.md](reference/ui-json.md) when generating or editing JSON UI; discover the installed catalog, validate the spec, then render and fix layout issues.
|
|
29
29
|
|
|
30
|
+
## UI copywriting
|
|
31
|
+
|
|
32
|
+
For UI/locale strings we author in the SDK, UI package, templates and our own
|
|
33
|
+
Apps, remove the final sentence-ending period in every language, even from
|
|
34
|
+
multi-sentence text; keep periods between sentences. Preserve question marks,
|
|
35
|
+
exclamation marks, ellipses, abbreviations, version numbers, URLs and trailing
|
|
36
|
+
`{key}` placeholders. In upstream forks, apply this only to UI copy we add under
|
|
37
|
+
`synology/` (such as DSM install wizards and gateway pages); leave upstream
|
|
38
|
+
UI/locale strings and release notes unchanged. See
|
|
39
|
+
[Vue copy and i18n](reference/ui-vue.md#ui-copy-and-i18n) and
|
|
40
|
+
[JSON copy and i18n](reference/ui-json.md#ui-copy-and-i18n) for examples.
|
|
41
|
+
|
|
30
42
|
For UI discovery, run `npx --no syno-pkg-land-ui catalog --json` and
|
|
31
43
|
`npx --no syno-pkg-land-ui describe Button --json` from the installed frontend.
|
|
32
44
|
Read its prompt and exported Vue docs/types before choosing component APIs.
|
|
@@ -155,6 +167,15 @@ Application Portal 是獨立 opt-in:不想要別名就省略 `portal`,不生
|
|
|
155
167
|
`defaultAlias`、Portal 面板/capability 或 Web Station alias;沒有 Portal
|
|
156
168
|
不是 inspect/info/doctor 問題。明確啟用 `portal` 時才將其 alias 作為 `defaultAlias`。
|
|
157
169
|
|
|
170
|
+
宣告 `console` 的 App 在主選單有兩個圖示,適用於包裝開源專案、又有額外整合操作的 App。
|
|
171
|
+
原生圖示叫 `displayname`,是 `type: "url"`,經 `sso/open.cgi` 在新分頁開原生 UI。
|
|
172
|
+
主控台預設叫「<displayname> for DSM」,是只給管理員的 `type: "app"` DSM 視窗,掛載 `ui.dir` 的 SDK UI。
|
|
173
|
+
套件中心「開啟」(INFO `dsmapplaunchname`)開主控台;Application Privileges 只列出原生圖示。
|
|
174
|
+
`console: {}` 用預設名稱,`console: { title: "<名稱>" }` 覆寫所有語系。
|
|
175
|
+
省略 `console` 維持單一圖示。`console` 必須搭配 `identity_gateway` 與 SDK UI;
|
|
176
|
+
compose App、沒有 gateway 的 App、`ui.launch` 或 `portal` 都會被 check 拒絕。
|
|
177
|
+
冷登入由 url 圖示經 `open.cgi` 續接,`oidc.browser` 需要支援此續接的 SDK。
|
|
178
|
+
|
|
158
179
|
Schema 2 Compose 使用 `dsm-package.yaml` 的 `compose.files`、`compose.web` 與
|
|
159
180
|
`identity_gateway`;不使用 file/targets/address_env 或手寫 upstream。
|
|
160
181
|
省略 `ui.launch` 時預設 `new-page`,使用 SDK standalone launcher,不帶 Application Portal。
|
|
@@ -57,6 +57,15 @@ Application Portal 是獨立 opt-in:不想要別名就省略 `portal`,不生
|
|
|
57
57
|
`defaultAlias`、Portal 面板/capability 或 Web Station alias;沒有 Portal
|
|
58
58
|
不是 inspect/info/doctor 問題。明確啟用 `portal` 時才將其 alias 作為 `defaultAlias`。
|
|
59
59
|
|
|
60
|
+
宣告 `console` 的 App 在主選單有兩個圖示,適用於包裝開源專案、又有額外整合操作的 App。
|
|
61
|
+
原生圖示叫 `displayname`,是 `type: "url"`,經 `sso/open.cgi` 在新分頁開原生 UI。
|
|
62
|
+
主控台預設叫「<displayname> for DSM」,是只給管理員的 `type: "app"` DSM 視窗,掛載 `ui.dir` 的 SDK UI。
|
|
63
|
+
套件中心「開啟」(INFO `dsmapplaunchname`)開主控台;Application Privileges 只列出原生圖示。
|
|
64
|
+
`console: {}` 用預設名稱,`console: { title: "<名稱>" }` 覆寫所有語系。
|
|
65
|
+
省略 `console` 維持單一圖示。`console` 必須搭配 `identity_gateway` 與 SDK UI;
|
|
66
|
+
compose App、沒有 gateway 的 App、`ui.launch` 或 `portal` 都會被 check 拒絕。
|
|
67
|
+
冷登入由 url 圖示經 `open.cgi` 續接,`oidc.browser` 需要支援此續接的 SDK。
|
|
68
|
+
|
|
60
69
|
Docker/web service App 設定 `integration.web.launch`,內建預設是 `new-page`;
|
|
61
70
|
省略開啟方式時直接開新分頁,不宣告 Application Portal。SDK 原生 App
|
|
62
71
|
省略時使用 `dsm-window`;非 UI SDK 原生 App 必須明確寫 `ui.launch: new-page`。
|
|
@@ -25,6 +25,35 @@ every state path you bind. Use only catalog components and real app-owned action
|
|
|
25
25
|
computed functions and message keys. Keep NAS pickers, native DSM bridges,
|
|
26
26
|
authorization, polling and domain-specific views in Vue/TypeScript extensions.
|
|
27
27
|
|
|
28
|
+
## UI copy and i18n
|
|
29
|
+
|
|
30
|
+
Use the same [UI copy rule as Vue](ui-vue.md#ui-copy-and-i18n) for literal
|
|
31
|
+
display text in JSON specs and the locale catalogs behind message keys. It
|
|
32
|
+
applies to copy we author in the SDK, UI package, templates and our own Apps.
|
|
33
|
+
In upstream forks, only our added UI copy under `synology/` (for example DSM
|
|
34
|
+
install wizards and gateway pages) is in scope. Leave upstream UI/locale
|
|
35
|
+
strings and release notes unchanged; any future checker must exclude upstream
|
|
36
|
+
source and scan only our owned files.
|
|
37
|
+
|
|
38
|
+
In every language, remove the final sentence-ending period (`.`, `。`, `.`),
|
|
39
|
+
even when the string contains multiple sentences. Preserve periods between
|
|
40
|
+
sentences. Keep question marks, exclamation marks, ellipses (`…`, `...`),
|
|
41
|
+
abbreviations such as `etc.`, version numbers, URLs and trailing `{key}`
|
|
42
|
+
placeholders intact. Apply the rule to each translation and plural form, not
|
|
43
|
+
to message keys, action names, URLs or other non-display spec data.
|
|
44
|
+
|
|
45
|
+
| Avoid (message value) | Use (message value) |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| `"Saved."` | `"Saved"` |
|
|
48
|
+
| `"Try again. Contact support if it still fails."` | `"Try again. Contact support if it still fails"` |
|
|
49
|
+
| `"請稍後再開。若一直打不開,請聯絡送審的人。"` | `"請稍後再開。若一直打不開,請聯絡送審的人"` |
|
|
50
|
+
| `"保存しました。"` | `"保存しました"` |
|
|
51
|
+
|
|
52
|
+
Keep `"Continue?"`, `"Done!"`, `"Loading…"`, `"Loading..."`, `"Images, etc."`,
|
|
53
|
+
`"Version {version}"`, `"https://example.com"` and `"Saved to {folder}"` unchanged.
|
|
54
|
+
Use a script for bulk corrections rather than editing entries one by one.
|
|
55
|
+
This is authoring guidance; it does not add validation to the commands below.
|
|
56
|
+
|
|
28
57
|
## Validate, fix, render
|
|
29
58
|
|
|
30
59
|
After saving the generated spec as `ui.json`, run:
|
|
@@ -73,6 +73,38 @@ the first screen waits for the startup chain, not the bundle:
|
|
|
73
73
|
- Start polling on its schedule; do not repeat at mount a read startup just
|
|
74
74
|
made. Delay background sampling that the first screen does not need.
|
|
75
75
|
|
|
76
|
+
## UI copy and i18n
|
|
77
|
+
|
|
78
|
+
Apply this rule to UI strings we author in the SDK, UI package, templates and
|
|
79
|
+
our own Apps, whether in locale catalogs or inline Vue/TypeScript copy: buttons,
|
|
80
|
+
titles, labels, field descriptions, hints, toasts and error messages. In an
|
|
81
|
+
upstream fork, apply it only to our added UI copy under `synology/`, including
|
|
82
|
+
DSM install wizards and gateway pages. Never rewrite upstream UI/locale strings
|
|
83
|
+
to match this style; preserve them for future upstream merges. Release notes
|
|
84
|
+
are outside this UI-copy rule.
|
|
85
|
+
|
|
86
|
+
Remove only the final sentence-ending period, including `.` and fullwidth
|
|
87
|
+
periods such as `。` and `.`, in every language. This applies to both single-
|
|
88
|
+
and multi-sentence strings. Keep sentence-separating periods inside the string.
|
|
89
|
+
Preserve question marks (`?`, `?`), exclamation marks (`!`, `!`), ellipses
|
|
90
|
+
(`…`, `...`), abbreviations such as `etc.`, version numbers, URLs and trailing
|
|
91
|
+
`{key}` placeholders; do not trim dots indiscriminately or alter substituted
|
|
92
|
+
values.
|
|
93
|
+
|
|
94
|
+
| Avoid | Use |
|
|
95
|
+
| --- | --- |
|
|
96
|
+
| `Save.` | `Save` |
|
|
97
|
+
| `Try again later. If it still fails, contact support.` | `Try again later. If it still fails, contact support` |
|
|
98
|
+
| `請稍後再開。若一直打不開,請聯絡送審的人。` | `請稍後再開。若一直打不開,請聯絡送審的人` |
|
|
99
|
+
| `設定を保存しました。` | `設定を保存しました` |
|
|
100
|
+
|
|
101
|
+
Keep these unchanged: `Continue?`, `Done!`, `Loading…`, `Loading...`,
|
|
102
|
+
`Images, videos, etc.`, `Version {version}`, `https://example.com` and
|
|
103
|
+
`Saved to {folder}`. Preserve internal punctuation in every translation and
|
|
104
|
+
plural form. For bulk copy corrections, use a script rather than editing each
|
|
105
|
+
entry by hand. Any future punctuation checker must scan only our owned files,
|
|
106
|
+
excluding upstream source; this guidance does not add a CLI check or gate.
|
|
107
|
+
|
|
76
108
|
## Verify the actual Vue app
|
|
77
109
|
|
|
78
110
|
From `frontend`, run:
|
|
@@ -124,6 +156,15 @@ Application Portal 是獨立 opt-in:不想要別名就省略 `portal`,不生
|
|
|
124
156
|
`defaultAlias`、Portal 面板/capability 或 Web Station alias;沒有 Portal
|
|
125
157
|
不是 inspect/info/doctor 問題。明確啟用 `portal` 時才將其 alias 作為 `defaultAlias`。
|
|
126
158
|
|
|
159
|
+
宣告 `console` 的 App 在主選單有兩個圖示,適用於包裝開源專案、又有額外整合操作的 App。
|
|
160
|
+
原生圖示叫 `displayname`,是 `type: "url"`,經 `sso/open.cgi` 在新分頁開原生 UI。
|
|
161
|
+
主控台預設叫「<displayname> for DSM」,是只給管理員的 `type: "app"` DSM 視窗,掛載 `ui.dir` 的 SDK UI。
|
|
162
|
+
套件中心「開啟」(INFO `dsmapplaunchname`)開主控台;Application Privileges 只列出原生圖示。
|
|
163
|
+
`console: {}` 用預設名稱,`console: { title: "<名稱>" }` 覆寫所有語系。
|
|
164
|
+
省略 `console` 維持單一圖示。`console` 必須搭配 `identity_gateway` 與 SDK UI;
|
|
165
|
+
compose App、沒有 gateway 的 App、`ui.launch` 或 `portal` 都會被 check 拒絕。
|
|
166
|
+
冷登入由 url 圖示經 `open.cgi` 續接,`oidc.browser` 需要支援此續接的 SDK。
|
|
167
|
+
|
|
127
168
|
Docker/web service App 設定 `integration.web.launch`,內建預設是 `new-page`;
|
|
128
169
|
省略開啟方式時直接開新分頁,不宣告 Application Portal。SDK 原生 App
|
|
129
170
|
省略時使用 `dsm-window`;非 UI SDK 原生 App 必須明確寫 `ui.launch: new-page`。
|
|
@@ -6,6 +6,11 @@ Never edit upstream README, Dockerfile, source, config, workflows or ignore file
|
|
|
6
6
|
Do not rewrite the App or treat it as an SDK component. Use `.git/info/exclude`
|
|
7
7
|
for local output when needed.
|
|
8
8
|
|
|
9
|
+
Apply the [UI copy rule](ui-vue.md#ui-copy-and-i18n) only to UI strings we add
|
|
10
|
+
under `synology/`, such as DSM install wizards and gateway pages; leave upstream
|
|
11
|
+
UI/locale strings and release notes unchanged, and exclude upstream source from
|
|
12
|
+
any future punctuation checker.
|
|
13
|
+
|
|
9
14
|
## Decide whether to expose AI control
|
|
10
15
|
|
|
11
16
|
Ask whether the user needs AI control unless they have already explicitly requested it.
|