@synopackageland/cli 0.17.6 → 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,9 @@
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
+
3
7
  ## 0.17.6 (2026-10-20)
4
8
 
5
9
  - Inspect sharedFolder, Portal and install-selected Apps by reconstructing fixed lifecycle scripts from their complete inputs, including native HTTPS ingress and App hooks.
@@ -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,6 +24,7 @@ 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";
@@ -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. */
@@ -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);
@@ -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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synopackageland/cli",
3
- "version": "0.17.6",
3
+ "version": "0.17.7",
4
4
  "description": "Syno Package Land Developer Kit CLI",
5
5
  "type": "module",
6
6
  "bin": {