@synopackageland/cli 0.17.5 → 0.17.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.17.7 (2026-10-20)
4
+
5
+ - Inspect Compose Apps with custom icons and localized titles using their archived build inputs, preserving every icon size and strict launcher validation. Preserve declared daemon uninstall hooks when checking fixed lifecycle scripts.
6
+
7
+ ## 0.17.6 (2026-10-20)
8
+
9
+ - Inspect sharedFolder, Portal and install-selected Apps by reconstructing fixed lifecycle scripts from their complete inputs, including native HTTPS ingress and App hooks.
10
+
3
11
  ## 0.17.5 (2026-10-20)
4
12
 
5
13
  - Run native HTTPS preparation inside the SDK lifecycle steps before App hooks, keeping generated hook wrappers inspectable.
@@ -34,5 +34,7 @@ export declare function iconReport(root: string, icon: string | undefined): Icon
34
34
  */
35
35
  export declare function windowIconTemplate(icons: PackageIcons): string;
36
36
  export declare function windowIconFile(icons: PackageIcons, size: number): string;
37
+ /** Recover the build's exact icon inputs from an SPK, including hand-drawn sizes. */
38
+ export declare function readDsmWindowIcons(uiDir: string): PackageIcons;
37
39
  export declare function writeDsmWindowIcons(uiDir: string, icons: PackageIcons): void;
38
40
  export declare function writePackageCenterIcons(spkRoot: string, icons: PackageIcons): void;
@@ -149,6 +149,36 @@ export function windowIconTemplate(icons) {
149
149
  export function windowIconFile(icons, size) {
150
150
  return windowIconTemplate(icons).replace("{0}", String(size));
151
151
  }
152
+ /** Recover the build's exact icon inputs from an SPK, including hand-drawn sizes. */
153
+ export function readDsmWindowIcons(uiDir) {
154
+ const dir = join(uiDir, "images");
155
+ const candidates = readdirSync(dir).filter(name => /^app_[a-f0-9]{12}_256\.png$/.test(name));
156
+ if (candidates.length !== 1)
157
+ throw new Error("expected one complete DSM window icon set");
158
+ const template = candidates[0].replace(/_256\.png$/, "_{0}.png");
159
+ const window = new Map();
160
+ let solid = false;
161
+ for (const size of WINDOW_ICON_SIZES) {
162
+ const name = template.replace("{0}", String(size));
163
+ const bytes = readFileSync(join(dir, name));
164
+ if (bytes.length > MAX_ICON_BYTES)
165
+ throw new Error(`${name}: invalid DSM window icon`);
166
+ const image = decodePng(bytes);
167
+ if (image.width !== size || image.height !== size)
168
+ throw new Error(`${name}: invalid DSM window icon`);
169
+ if (size === 256)
170
+ solid = isSolid(image);
171
+ window.set(size, bytes);
172
+ }
173
+ const icons = {
174
+ source: window.get(256).equals(defaultPackageIcons().packageIcon256) ? "default" : "custom",
175
+ window, solid,
176
+ packageIcon: window.get(64), packageIcon256: window.get(256),
177
+ };
178
+ if (windowIconTemplate(icons) !== `images/${template}`)
179
+ throw new Error("DSM window icon filenames differ from their content hash");
180
+ return icons;
181
+ }
152
182
  export function writeDsmWindowIcons(uiDir, icons) {
153
183
  mkdirSync(join(uiDir, "images"), { recursive: true });
154
184
  for (const [size, png] of icons.window)
@@ -24,11 +24,12 @@ import { daemonConfig, readDaemon, readLifecycleTimeouts } from "./daemon.js";
24
24
  import { ROOT_SCRIPT_HINTS } from "./root-ops.js";
25
25
  import { DELETE_STATE_KEY } from "../build-spk/uninstall-wizard.js";
26
26
  import { readUninstallRecord, stateItemTexts } from "./uninstall.js";
27
+ import { DSM_LANGS } from "./i18n.js";
27
28
  import { BACKGROUND_UNIT_DIR, BACKGROUND_WORKER_PAYLOAD, BRIDGE_WORKER_ARGS, backgroundUnitName, renderBackgroundUnit, } from "./background.js";
28
29
  import { renderStandaloneResource } from "./resource.js";
29
30
  import { recoverSharedFolder, renderSharedFolderUpgradeWizardFiles, sharedFolderInstallSteps, SHARED_FOLDER_MARKER, } from "./shared-folder.js";
30
31
  import { renderInstallWizardFiles, renderNativeInstallWizard, } from "../build-spk/install-wizard.js";
31
- import { renderDelegateScopeSteps, renderDelegateInstallDraft, } from "../build-spk/delegate-scope.js";
32
+ import { renderDelegateScopeSteps, } from "../build-spk/delegate-scope.js";
32
33
  import { renderUninstallWizardFiles } from "../build-spk/uninstall-wizard.js";
33
34
  import { renderNativePrivilege } from "../build-spk/native-lifecycle.js";
34
35
  import { renderUiConfig } from "./dsm-window.js";
@@ -39,7 +40,7 @@ import { listPortExposures, loadCompose } from "../compose/analyze.js";
39
40
  import { uninstallCleanup, } from "../runtime/uninstall-cleanup.js";
40
41
  import { PORTAL_JSON_RELPATH, PORTAL_MUSTACHE_RELPATH, renderPortalJson, renderPortalLifecycle, renderPortalMustache, renderPortalWizardStep, } from "../portal/package.js";
41
42
  import { isPortalAlias } from "../portal/alias.js";
42
- import { defaultPackageIcons, MAX_ICON_BYTES } from "../build-spk/dsm-icons.js";
43
+ import { defaultPackageIcons, MAX_ICON_BYTES, readDsmWindowIcons } from "../build-spk/dsm-icons.js";
43
44
  import { isDefaultLoginAsset } from "../build-spk/login-style.js";
44
45
  import { decodePng } from "../build-spk/png.js";
45
46
  /** The SPK kind of each sdk-package.json backend a CGI is built for: Go itself, or the runtime of an App in another language. */
@@ -162,7 +163,7 @@ export async function inspectSpk(spkPath, expectedKind) {
162
163
  : undefined;
163
164
  // Resource-worker portals use DSM-owned INFO reload, with no executor requirement.
164
165
  const portal = standalone
165
- ? await recoverPortal(dir, info, `SYNO.SDS.${info.package}.Instance`, paths, payload, findings)
166
+ ? await recoverPortal(dir, info, `SYNO.SDS.${info.package}.Instance`, paths, payload, findings, nativeGateway)
166
167
  : undefined;
167
168
  if (portal &&
168
169
  info.os_min_ver &&
@@ -198,7 +199,7 @@ export async function inspectSpk(spkPath, expectedKind) {
198
199
  if (standalone && portal)
199
200
  findings.push(...inspectPortalInfo(info, hasExecutor));
200
201
  const daemon = standalone
201
- ? await checkDaemon(dir, info, paths, payload, findings, hasExecutor, background, combined, nativeGateway)
202
+ ? await checkDaemon(dir, info, paths, payload, findings, hasExecutor, background, combined, nativeGateway, uninstallHook?.uninstall)
202
203
  : undefined;
203
204
  if (background)
204
205
  checkBackground(dir, info, payload, findings, hasExecutor, daemon, kind !== "native-go", metadata.lifecycleTimeouts);
@@ -269,7 +270,7 @@ export async function inspectSpk(spkPath, expectedKind) {
269
270
  ? await inspectLoginStyle(dir, info.package, Boolean(portal || executor?.policy.portal), findings)
270
271
  : undefined;
271
272
  if (standalone)
272
- checkSharedFolder(dir, findings, info.package, sharedFolder, executor?.policy.scope === "install-selected", portal, hasExecutor);
273
+ checkSharedFolder(dir, findings, info.package, sharedFolder, executor?.policy.scope === "install-selected", portal, hasExecutor, nativeGateway, info.os_min_ver);
273
274
  const icon = inspectPackageIcons(dir, findings);
274
275
  const cgiEntry = payload.find((entry) => entry.path === CGI_PAYLOAD_PATH);
275
276
  if (standalone && !cgiEntry) {
@@ -627,6 +628,7 @@ async function inspectUnifiedCompose(dir, payload, metadata, findings) {
627
628
  displayname: info.displayname,
628
629
  description: info.description,
629
630
  maintainer: info.maintainer,
631
+ i18n: { info: archivedInfoTranslations(info) },
630
632
  os_min_ver: info.os_min_ver,
631
633
  build: Number(info.version?.split("-").pop()),
632
634
  arches: [arch],
@@ -653,7 +655,10 @@ async function inspectUnifiedCompose(dir, payload, metadata, findings) {
653
655
  throw new Error("unexpected worker resource; unapproved Shares must never auto-acquire");
654
656
  if (readdirSync(join(dir, "conf")).some((n) => !["privilege", ...(resource ? ["resource"] : [])].includes(n)))
655
657
  throw new Error("unexpected Compose resource");
656
- if (readOptional(join(root, "ui/config")) !== renderUiConfig(manifest))
658
+ // Replay build's shared renderer with the archived bytes, never a project
659
+ // path or the SDK default. Each hand-drawn icon size affects the URL hash.
660
+ const icons = readDsmWindowIcons(join(root, "ui"));
661
+ if (readOptional(join(root, "ui/config")) !== renderUiConfig(manifest, icons))
657
662
  throw new Error("LAUNCHER_TYPE_MISMATCH classless Compose must use the shared url launcher");
658
663
  if (g &&
659
664
  (cfg.package !== info.package ||
@@ -787,7 +792,7 @@ async function recoverCompose(dir, value, findings) {
787
792
  return undefined;
788
793
  }
789
794
  }
790
- async function checkDaemon(dir, info, paths, payload, findings, executor, background, combined, nativeGateway) {
795
+ async function checkDaemon(dir, info, paths, payload, findings, executor, background, combined, nativeGateway, uninstall) {
791
796
  if (![...paths].some((path) => path.startsWith("daemon/")))
792
797
  return;
793
798
  if (info.precheckstartstop !== "yes")
@@ -843,6 +848,7 @@ async function checkDaemon(dir, info, paths, payload, findings, executor, backgr
843
848
  root: dir,
844
849
  ...(combined ? { lifecycleTimeouts: daemon.timeouts } : {}),
845
850
  daemon,
851
+ uninstall,
846
852
  ...combined,
847
853
  capabilities: {
848
854
  ...(executor ? { rootOps: true } : {}),
@@ -879,6 +885,22 @@ async function checkDaemon(dir, info, paths, payload, findings, executor, backgr
879
885
  findings.push("daemon/config.json or the payload does not match the fixed daemon contract");
880
886
  }
881
887
  }
888
+ /** INFO retains the localized title inputs used by build's ui/config renderer. */
889
+ function archivedInfoTranslations(info) {
890
+ const translations = {};
891
+ for (const [locale, lang] of Object.entries(DSM_LANGS)) {
892
+ if (locale === "en")
893
+ continue;
894
+ const displayname = info[`displayname_${lang}`];
895
+ const description = info[`description_${lang}`];
896
+ if (displayname || description)
897
+ translations[locale] = {
898
+ ...(displayname ? { displayname } : {}),
899
+ ...(description ? { description } : {}),
900
+ };
901
+ }
902
+ return translations;
903
+ }
882
904
  function readInfo(path, findings, kind, compose = false) {
883
905
  if (!existsSync(path)) {
884
906
  findings.push("INFO is missing");
@@ -1081,7 +1103,7 @@ function checkBackground(dir, info, payload, findings, executor, daemon, bridge
1081
1103
  * declaration (without one, any present file must be the default native
1082
1104
  * install wizard), and no other install/upgrade wizard file may exist.
1083
1105
  */
1084
- function checkSharedFolder(dir, findings, pkg, folder, selected = false, portal, executor = false) {
1106
+ function checkSharedFolder(dir, findings, pkg, folder, selected = false, portal, executor = false, nativeGateway, osMinVer) {
1085
1107
  const wizardDir = join(dir, "WIZARD_UIFILES");
1086
1108
  const present = (existsSync(wizardDir) ? readdirSync(wizardDir) : [])
1087
1109
  .filter((name) => /^(install|upgrade)_uifile(_|\.|$)/.test(name))
@@ -1091,6 +1113,22 @@ function checkSharedFolder(dir, findings, pkg, folder, selected = false, portal,
1091
1113
  findings.push("the install.sharedFolder declaration in scripts/postinst is invalid");
1092
1114
  if (!pkg)
1093
1115
  return;
1116
+ // App wrappers have already been validated and unwrapped. Reconstruct the
1117
+ // remaining install-hook inputs from INFO, the payload and its build record.
1118
+ const lifecycleManifest = {
1119
+ package: pkg,
1120
+ os_min_ver: osMinVer,
1121
+ sharedFolder: folder,
1122
+ capabilities: selected
1123
+ ? { delegate: { scope: "install-selected" } }
1124
+ : executor
1125
+ ? { rootOps: true }
1126
+ : {},
1127
+ ...(nativeGateway
1128
+ ? { identityGateway: nativeGateway }
1129
+ : {}),
1130
+ ...portalManifest(portal),
1131
+ };
1094
1132
  const wizards = expectedWizardFiles(pkg, folder, selected, portal);
1095
1133
  const source = selected
1096
1134
  ? "the install-selected policy"
@@ -1110,25 +1148,17 @@ function checkSharedFolder(dir, findings, pkg, folder, selected = false, portal,
1110
1148
  findings.push(`WIZARD_UIFILES/${name} is not a wizard file produced by ${source}`);
1111
1149
  if (selected) {
1112
1150
  for (const hook of ["preinst", "postinst"]) {
1113
- if (!readOptional(join(dir, "scripts", hook))?.includes(renderDelegateInstallDraft(pkg, hook === "preinst")))
1114
- findings.push(`scripts/${hook} missing install-selected draft handling`);
1151
+ if (readOptional(join(dir, "scripts", hook)) !==
1152
+ renderStandaloneScript(hook, lifecycleManifest))
1153
+ findings.push(`scripts/${hook} does not match the fixed install-selected draft handling`);
1115
1154
  }
1116
1155
  }
1117
- if (!folder)
1156
+ if (!folder && !portal)
1118
1157
  return;
1119
1158
  for (const hook of ["postinst", "postupgrade"]) {
1120
1159
  if (readOptional(join(dir, "scripts", hook)) !==
1121
- renderStandaloneScript(hook, {
1122
- package: pkg,
1123
- sharedFolder: folder,
1124
- ...(selected
1125
- ? { capabilities: { delegate: { scope: "install-selected" } } }
1126
- : executor
1127
- ? { capabilities: { rootOps: true } }
1128
- : {}),
1129
- ...portalManifest(portal),
1130
- })) {
1131
- findings.push(`scripts/${hook} does not match the fixed script declared by install.sharedFolder`);
1160
+ renderStandaloneScript(hook, lifecycleManifest)) {
1161
+ findings.push(`scripts/${hook} does not match the fixed script declared by ${folder ? "install.sharedFolder" : "portal"}`);
1132
1162
  }
1133
1163
  }
1134
1164
  }
@@ -1160,7 +1190,7 @@ function portalManifest(portal) {
1160
1190
  * files and alias lifecycle must be exactly what the generator writes for the
1161
1191
  * package's own DSM window (native AppInstance or Compose `.url` entry); activation is never part of the package.
1162
1192
  */
1163
- async function recoverPortal(dir, info, appId, paths, payload, findings) {
1193
+ async function recoverPortal(dir, info, appId, paths, payload, findings, nativeGateway) {
1164
1194
  const files = [PORTAL_JSON_RELPATH, PORTAL_MUSTACHE_RELPATH];
1165
1195
  if (!files.some((path) => paths.has(path)))
1166
1196
  return;
@@ -1184,7 +1214,14 @@ async function recoverPortal(dir, info, appId, paths, payload, findings) {
1184
1214
  renderPortalMustache())
1185
1215
  throw new Error("content");
1186
1216
  for (const hook of ["preinst", "postinst", "postupgrade"]) {
1187
- if (!readOptional(join(dir, "scripts", hook))?.startsWith(`#!/bin/sh\n${renderPortalLifecycle(hook, portal)}`))
1217
+ const prepare = ["postinst", "postupgrade"].includes(hook)
1218
+ ? renderNativeGatewayPrepare({
1219
+ package: info.package,
1220
+ os_min_ver: info.os_min_ver,
1221
+ identityGateway: nativeGateway,
1222
+ })
1223
+ : "";
1224
+ if (!readOptional(join(dir, "scripts", hook))?.startsWith(`#!/bin/sh\n${prepare}${renderPortalLifecycle(hook, portal)}`))
1188
1225
  throw new Error(hook);
1189
1226
  }
1190
1227
  return portal;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synopackageland/cli",
3
- "version": "0.17.5",
3
+ "version": "0.17.7",
4
4
  "description": "Syno Package Land Developer Kit CLI",
5
5
  "type": "module",
6
6
  "bin": {