@synopackageland/cli 0.17.3 → 0.17.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 CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.17.4 (2026-10-19)
6
+
7
+ - Accept `compose.startup_timeout_seconds` (default 120, integer 2–900), use the SDK plan's value for daemon start, and deliver the same budget to container bootstrap adapters through the SDK-generated environment. Reject a planner that cannot honor an explicitly declared budget. Keep ingress closed until readiness succeeds.
8
+
9
+ - Show actionable image-pull diagnosis in nine locales and direct users to Package Center Repair when a package starts with stale container assets.
10
+ - Pin templates 0.17.6 and SDK/App 0.17.4; keep UI at 0.12.3.
11
+
3
12
  ## 0.17.3 (2026-10-19)
4
13
 
5
14
  - Pin templates 0.17.5, SDK/App 0.17.3 and UI 0.12.3 for generated Apps.
package/README.md CHANGED
@@ -259,6 +259,16 @@ Compose uses schema-2 `dsm-package.yaml` and `ui.launch`; the default is new-pag
259
259
  Classless gateways use type url via the shared contract/app-launch resolver;
260
260
  AppWindow wrappers use standalone. Headless projects have no launcher.
261
261
 
262
+ Rebuilt container Apps include informational **Registry check** diagnostics in
263
+ preinst, postinst, postupgrade and prestart. Messages use the existing nine-language
264
+ catalog and end with the native recovery step: fix the cause, then use Package
265
+ Center **Repair**. Repair re-runs the install worker and downloads missing images;
266
+ it is not an SDK Retry action. Checks use only immutable image references from the
267
+ packaged lock, never the Docker socket or registry credentials. They do not block
268
+ cached/offline startup or establish daemon disk space, image existence or pull
269
+ success. Native wrapper codes alone never identify a cause. SDK and CLI changes
270
+ must ship together and Apps must be rebuilt to receive these hooks and catalogs.
271
+
262
272
  See [Compose](skills/reference/compose.md) for multiple upstream files,
263
273
  generated override/worker artifacts, image proofs and the support matrix.
264
274
  Containers use a fixed package identity and do not inherit a signed-in user's
@@ -0,0 +1,4 @@
1
+ /** Packaged data for compose/control; one source of native text in all locales. */
2
+ export declare function composeDiagnosisMessages(): Record<string, Record<string, string>>;
3
+ /** Diagnostics are informational and cannot mask or replace a native result. */
4
+ export declare function renderComposeDiagnosis(project: string, phase: "preinst" | "postinst" | "postupgrade" | "prestart"): string;
@@ -0,0 +1,12 @@
1
+ import { LOCALES } from "../standalone/i18n.js";
2
+ import { WIZARD_MESSAGES } from "./wizard-i18n.js";
3
+ /** Packaged data for compose/control; one source of native text in all locales. */
4
+ export function composeDiagnosisMessages() {
5
+ return Object.fromEntries(LOCALES.map((locale) => [locale, Object.fromEntries(Object.entries(WIZARD_MESSAGES)
6
+ .filter(([key]) => key.startsWith("compose.diagnosis."))
7
+ .map(([key, texts]) => [key.slice("compose.diagnosis.".length), texts[locale]]))]));
8
+ }
9
+ /** Diagnostics are informational and cannot mask or replace a native result. */
10
+ export function renderComposeDiagnosis(project, phase) {
11
+ return `"${project}/control" diagnose --project "${project}" --phase ${phase} || :\n`;
12
+ }
@@ -1,4 +1,136 @@
1
1
  export declare const WIZARD_MESSAGES: {
2
+ "compose.diagnosis.registry_check": {
3
+ en: string;
4
+ "zh-TW": string;
5
+ "zh-CN": string;
6
+ ja: string;
7
+ ko: string;
8
+ de: string;
9
+ fr: string;
10
+ es: string;
11
+ it: string;
12
+ };
13
+ "compose.diagnosis.container_manager": {
14
+ en: string;
15
+ "zh-TW": string;
16
+ "zh-CN": string;
17
+ ja: string;
18
+ ko: string;
19
+ de: string;
20
+ fr: string;
21
+ es: string;
22
+ it: string;
23
+ };
24
+ "compose.diagnosis.tls_trust": {
25
+ en: string;
26
+ "zh-TW": string;
27
+ "zh-CN": string;
28
+ ja: string;
29
+ ko: string;
30
+ de: string;
31
+ fr: string;
32
+ es: string;
33
+ it: string;
34
+ };
35
+ "compose.diagnosis.unreachable": {
36
+ en: string;
37
+ "zh-TW": string;
38
+ "zh-CN": string;
39
+ ja: string;
40
+ ko: string;
41
+ de: string;
42
+ fr: string;
43
+ es: string;
44
+ it: string;
45
+ };
46
+ "compose.diagnosis.dns": {
47
+ en: string;
48
+ "zh-TW": string;
49
+ "zh-CN": string;
50
+ ja: string;
51
+ ko: string;
52
+ de: string;
53
+ fr: string;
54
+ es: string;
55
+ it: string;
56
+ };
57
+ "compose.diagnosis.rate_limit": {
58
+ en: string;
59
+ "zh-TW": string;
60
+ "zh-CN": string;
61
+ ja: string;
62
+ ko: string;
63
+ de: string;
64
+ fr: string;
65
+ es: string;
66
+ it: string;
67
+ };
68
+ "compose.diagnosis.disk_full": {
69
+ en: string;
70
+ "zh-TW": string;
71
+ "zh-CN": string;
72
+ ja: string;
73
+ ko: string;
74
+ de: string;
75
+ fr: string;
76
+ es: string;
77
+ it: string;
78
+ };
79
+ "compose.diagnosis.not_found": {
80
+ en: string;
81
+ "zh-TW": string;
82
+ "zh-CN": string;
83
+ ja: string;
84
+ ko: string;
85
+ de: string;
86
+ fr: string;
87
+ es: string;
88
+ it: string;
89
+ };
90
+ "compose.diagnosis.denied": {
91
+ en: string;
92
+ "zh-TW": string;
93
+ "zh-CN": string;
94
+ ja: string;
95
+ ko: string;
96
+ de: string;
97
+ fr: string;
98
+ es: string;
99
+ it: string;
100
+ };
101
+ "compose.diagnosis.unknown": {
102
+ en: string;
103
+ "zh-TW": string;
104
+ "zh-CN": string;
105
+ ja: string;
106
+ ko: string;
107
+ de: string;
108
+ fr: string;
109
+ es: string;
110
+ it: string;
111
+ };
112
+ "compose.diagnosis.repair": {
113
+ en: string;
114
+ "zh-TW": string;
115
+ "zh-CN": string;
116
+ ja: string;
117
+ ko: string;
118
+ de: string;
119
+ fr: string;
120
+ es: string;
121
+ it: string;
122
+ };
123
+ "compose.diagnosis.error_code": {
124
+ en: string;
125
+ "zh-TW": string;
126
+ "zh-CN": string;
127
+ ja: string;
128
+ ko: string;
129
+ de: string;
130
+ fr: string;
131
+ es: string;
132
+ it: string;
133
+ };
2
134
  "delegate.preselection": {
3
135
  en: string;
4
136
  "zh-TW": string;
@@ -1,4 +1,136 @@
1
1
  export const WIZARD_MESSAGES = {
2
+ "compose.diagnosis.registry_check": {
3
+ "en": "Registry check",
4
+ "zh-TW": "登錄站檢查",
5
+ "zh-CN": "镜像仓库检查",
6
+ "ja": "レジストリ確認",
7
+ "ko": "레지스트리 확인",
8
+ "de": "Registry-Prüfung",
9
+ "fr": "Vérification du registre",
10
+ "es": "Comprobación del registro",
11
+ "it": "Verifica del registro"
12
+ },
13
+ "compose.diagnosis.container_manager": {
14
+ "en": "Container Manager",
15
+ "zh-TW": "Container Manager 錯誤",
16
+ "zh-CN": "Container Manager 错误",
17
+ "ja": "Container Manager のエラー",
18
+ "ko": "Container Manager 오류",
19
+ "de": "Container Manager-Fehler",
20
+ "fr": "Erreur de Container Manager",
21
+ "es": "Error de Container Manager",
22
+ "it": "Errore di Container Manager"
23
+ },
24
+ "compose.diagnosis.tls_trust": {
25
+ "en": "The registry certificate could not be verified. Check the NAS date and time. Ask your network administrator to configure trusted certificates for Container Manager. Alternatively, ask them to let this registry bypass network certificate checks.",
26
+ "zh-TW": "無法驗證登錄站憑證。請檢查 NAS 日期與時間。若公司網路會攔查加密連線,請網管協助。可為 Container Manager 設定信任憑證,或讓登錄站略過攔查。",
27
+ "zh-CN": "无法验证镜像仓库证书。请检查 NAS 日期和时间。如果公司网络会检查加密连接,请联系网络管理员。可为 Container Manager 配置信任证书,或让镜像仓库绕过此类检查。",
28
+ "ja": "レジストリの証明書を検証できません。NAS の日付と時刻を確認してください。Container Manager で信頼する証明書の設定をネットワーク管理者に依頼してください。または、このレジストリをネットワークの証明書検査から除外してもらってください。",
29
+ "ko": "레지스트리 인증서를 확인할 수 없습니다. NAS 날짜와 시간을 확인하십시오. 네트워크 관리자에게 Container Manager의 신뢰할 수 있는 인증서 설정을 요청하십시오. 또는 이 레지스트리를 네트워크 인증서 검사에서 제외하도록 요청하십시오.",
30
+ "de": "Das Zertifikat der Registry konnte nicht geprüft werden. Prüfen Sie Datum und Uhrzeit des NAS. Bitten Sie Ihre Netzwerkadministration, vertrauenswürdige Zertifikate für Container Manager einzurichten. Alternativ kann sie diese Registry von der Zertifikatsprüfung im Netzwerk ausnehmen.",
31
+ "fr": "Le certificat du registre ne peut pas être vérifié. Vérifiez la date et l’heure du NAS. Demandez à votre administrateur réseau de configurer les certificats de confiance pour Container Manager. Il peut aussi exclure ce registre du contrôle des certificats sur le réseau.",
32
+ "es": "No se pudo verificar el certificado del registro. Compruebe la fecha y la hora del NAS. Solicite a su administrador de red que configure certificados de confianza para Container Manager. Como alternativa, puede excluir este registro de la comprobación de certificados de la red.",
33
+ "it": "Impossibile verificare il certificato del registro. Controllare la data e l’ora del NAS. Chiedere all’amministratore di rete di configurare i certificati attendibili per Container Manager. In alternativa, chiedere di escludere questo registro dai controlli dei certificati di rete."
34
+ },
35
+ "compose.diagnosis.unreachable": {
36
+ "en": "Cannot reach the registry. Check the NAS network, firewall and Container Manager proxy settings.",
37
+ "zh-TW": "無法連線至登錄站。檢查 NAS 網路、防火牆及 Container Manager 的代理伺服器設定。",
38
+ "zh-CN": "无法连接镜像仓库。检查 NAS 网络、防火墙及 Container Manager 的代理服务器设置。",
39
+ "ja": "レジストリに接続できません。NAS のネットワーク、ファイアウォール、Container Manager のプロキシ設定を確認してください。",
40
+ "ko": "레지스트리에 연결할 수 없습니다. NAS 네트워크, 방화벽 및 Container Manager 프록시 설정을 확인하십시오.",
41
+ "de": "Die Registry ist nicht erreichbar. Prüfen Sie Netzwerk, Firewall und die Proxy-Einstellungen von Container Manager.",
42
+ "fr": "Le registre est inaccessible. Vérifiez le réseau du NAS, le pare-feu et les paramètres de proxy de Container Manager.",
43
+ "es": "No se puede acceder al registro. Compruebe la red del NAS, el cortafuegos y la configuración de proxy de Container Manager.",
44
+ "it": "Impossibile raggiungere il registro. Controllare la rete del NAS, il firewall e le impostazioni proxy di Container Manager."
45
+ },
46
+ "compose.diagnosis.dns": {
47
+ "en": "The registry name could not be resolved. Check the NAS DNS settings.",
48
+ "zh-TW": "無法解析登錄站名稱。檢查 NAS 的 DNS 設定。",
49
+ "zh-CN": "无法解析镜像仓库名称。检查 NAS 的 DNS 设置。",
50
+ "ja": "レジストリ名を解決できません。NAS の DNS 設定を確認してください。",
51
+ "ko": "레지스트리 이름을 확인할 수 없습니다. NAS DNS 설정을 확인하십시오.",
52
+ "de": "Der Name der Registry konnte nicht aufgelöst werden. Prüfen Sie die DNS-Einstellungen des NAS.",
53
+ "fr": "Le nom du registre ne peut pas être résolu. Vérifiez les paramètres DNS du NAS.",
54
+ "es": "No se pudo resolver el nombre del registro. Compruebe la configuración DNS del NAS.",
55
+ "it": "Impossibile risolvere il nome del registro. Controllare le impostazioni DNS del NAS."
56
+ },
57
+ "compose.diagnosis.rate_limit": {
58
+ "en": "The registry download limit was reached. Wait before trying again. You can also ask an administrator to check registry sign-in.",
59
+ "zh-TW": "已達登錄站下載上限。請稍後再試,也可請管理員檢查登錄站登入設定。",
60
+ "zh-CN": "已达到镜像仓库下载上限。请稍后再试,也可请管理员检查镜像仓库登录设置。",
61
+ "ja": "レジストリのダウンロード制限に達しました。しばらく待つか、管理者にレジストリのサインイン設定の確認を依頼してください。",
62
+ "ko": "레지스트리 다운로드 한도에 도달했습니다. 기다리거나 관리자에게 레지스트리 로그인 설정 확인을 요청하십시오.",
63
+ "de": "Das Downloadlimit der Registry wurde erreicht. Warten Sie oder bitten Sie einen Administrator, die Anmeldung bei der Registry zu prüfen.",
64
+ "fr": "La limite de téléchargement du registre est atteinte. Patientez ou demandez à un administrateur de vérifier la connexion au registre.",
65
+ "es": "Se alcanzó el límite de descargas del registro. Espere o solicite a un administrador que compruebe el inicio de sesión en el registro.",
66
+ "it": "È stato raggiunto il limite di download del registro. Attendere o chiedere a un amministratore di controllare l’accesso al registro."
67
+ },
68
+ "compose.diagnosis.disk_full": {
69
+ "en": "There is not enough storage for the image. Free space on Container Manager's storage volume.",
70
+ "zh-TW": "儲存空間不足,無法下載映像。請釋出 Container Manager 儲存磁碟區的空間。",
71
+ "zh-CN": "存储空间不足,无法下载映像。请释放 Container Manager 存储卷的空间。",
72
+ "ja": "イメージ用の空き容量が不足しています。Container Manager のストレージボリュームの空き容量を増やしてください。",
73
+ "ko": "이미지를 위한 저장 공간이 부족합니다. Container Manager 저장소 볼륨의 공간을 확보하십시오.",
74
+ "de": "Für das Image ist nicht genug Speicherplatz verfügbar. Geben Sie Speicherplatz auf dem Speichervolume von Container Manager frei.",
75
+ "fr": "L’espace de stockage est insuffisant pour l’image. Libérez de l’espace sur le volume de stockage de Container Manager.",
76
+ "es": "No hay espacio de almacenamiento suficiente para la imagen. Libere espacio en el volumen de almacenamiento de Container Manager.",
77
+ "it": "Lo spazio di archiviazione per l’immagine non è sufficiente. Liberare spazio sul volume di archiviazione di Container Manager."
78
+ },
79
+ "compose.diagnosis.not_found": {
80
+ "en": "The required image version is unavailable. Check for an App update or contact its publisher.",
81
+ "zh-TW": "所需映像版本無法取得。檢查 App 更新或聯絡發佈者。",
82
+ "zh-CN": "无法获取所需映像版本。检查应用更新或联系发布者。",
83
+ "ja": "必要なイメージバージョンを取得できません。アプリの更新を確認するか、公開元に問い合わせてください。",
84
+ "ko": "필요한 이미지 버전을 사용할 수 없습니다. 앱 업데이트를 확인하거나 게시자에게 문의하십시오.",
85
+ "de": "Die erforderliche Image-Version ist nicht verfügbar. Prüfen Sie auf App-Updates oder wenden Sie sich an den Herausgeber.",
86
+ "fr": "La version d’image requise est indisponible. Recherchez une mise à jour de l’application ou contactez son éditeur.",
87
+ "es": "La versión de imagen requerida no está disponible. Busque una actualización de la aplicación o contacte con su editor.",
88
+ "it": "La versione dell’immagine richiesta non è disponibile. Verificare gli aggiornamenti dell’app o contattare l’editore."
89
+ },
90
+ "compose.diagnosis.denied": {
91
+ "en": "The registry denied access. Ask an administrator to check registry sign-in and image permissions.",
92
+ "zh-TW": "登錄站拒絕存取。請管理員檢查登錄站登入設定及映像權限。",
93
+ "zh-CN": "镜像仓库拒绝访问。请管理员检查镜像仓库登录设置及映像权限。",
94
+ "ja": "レジストリにアクセスを拒否されました。管理者にレジストリのサインイン設定とイメージの権限の確認を依頼してください。",
95
+ "ko": "레지스트리가 접근을 거부했습니다. 관리자에게 레지스트리 로그인 설정과 이미지 권한 확인을 요청하십시오.",
96
+ "de": "Die Registry hat den Zugriff verweigert. Bitten Sie einen Administrator, die Anmeldung und die Image-Berechtigungen zu prüfen.",
97
+ "fr": "Le registre a refusé l’accès. Demandez à un administrateur de vérifier la connexion au registre et les autorisations de l’image.",
98
+ "es": "El registro denegó el acceso. Solicite a un administrador que compruebe el inicio de sesión y los permisos de la imagen.",
99
+ "it": "Il registro ha negato l’accesso. Chiedere a un amministratore di controllare l’accesso al registro e le autorizzazioni dell’immagine."
100
+ },
101
+ "compose.diagnosis.unknown": {
102
+ "en": "The check could not determine the cause. Open Container Manager for details or contact the App publisher.",
103
+ "zh-TW": "檢查無法判斷原因。開啟 Container Manager 查看詳細資訊,或聯絡 App 發佈者。",
104
+ "zh-CN": "检查无法确定原因。打开 Container Manager 查看详细信息,或联系应用发布者。",
105
+ "ja": "確認では原因を特定できませんでした。Container Manager で詳細を確認するか、アプリの公開元に問い合わせてください。",
106
+ "ko": "확인으로 원인을 파악할 수 없습니다. Container Manager에서 세부 정보를 확인하거나 앱 게시자에게 문의하십시오.",
107
+ "de": "Die Prüfung konnte die Ursache nicht ermitteln. Öffnen Sie Container Manager für Details oder wenden Sie sich an den App-Herausgeber.",
108
+ "fr": "La vérification ne permet pas de déterminer la cause. Ouvrez Container Manager pour plus de détails ou contactez l’éditeur de l’application.",
109
+ "es": "La comprobación no pudo determinar la causa. Abra Container Manager para ver los detalles o contacte con el editor de la aplicación.",
110
+ "it": "La verifica non ha potuto determinare la causa. Aprire Container Manager per i dettagli o contattare l’editore dell’app."
111
+ },
112
+ "compose.diagnosis.repair": {
113
+ "en": "After fixing the cause, use Package Center \"Repair\". The system will reinstall the package and download missing images.",
114
+ "zh-TW": "修正後,請在套件中心按「修復」,系統會重新安裝並下載缺少的映像。",
115
+ "zh-CN": "修正后,请在套件中心点击“修复”,系统会重新安装并下载缺少的映像。",
116
+ "ja": "原因を解消してから、パッケージセンターで「修復」を実行してください。システムがパッケージを再インストールし、不足しているイメージをダウンロードします。",
117
+ "ko": "원인을 해결한 후 패키지 센터에서 \"복구\"를 선택하십시오. 시스템이 패키지를 다시 설치하고 누락된 이미지를 다운로드합니다.",
118
+ "de": "Beheben Sie die Ursache und wählen Sie im Paket-Zentrum „Reparieren“. Das System installiert das Paket erneut und lädt fehlende Images herunter.",
119
+ "fr": "Corrigez la cause, puis sélectionnez « Réparer » dans le Centre de paquets. Le système réinstallera le paquet et téléchargera les images manquantes.",
120
+ "es": "Corrija la causa y seleccione \"Reparar\" en el Centro de paquetes. El sistema reinstalará el paquete y descargará las imágenes que faltan.",
121
+ "it": "Correggere la causa e selezionare «Ripara» nel Centro pacchetti. Il sistema reinstallerà il pacchetto e scaricherà le immagini mancanti."
122
+ },
123
+ "compose.diagnosis.error_code": {
124
+ "en": "(Error code: {code})",
125
+ "zh-TW": "(錯誤碼:{code})",
126
+ "zh-CN": "(错误代码:{code})",
127
+ "ja": "(エラーコード:{code})",
128
+ "ko": "(오류 코드: {code})",
129
+ "de": "(Fehlercode: {code})",
130
+ "fr": "(Code d’erreur : {code})",
131
+ "es": "(Código de error: {code})",
132
+ "it": "(Codice errore: {code})"
133
+ },
2
134
  "delegate.preselection": {
3
135
  en: "Pre-select the shared folder for background saves. Confirm this selection with your administrator password when first enabling background access in the app. This does not grant the package account access to the share.",
4
136
  "zh-TW": "預選背景存檔使用的共用資料夾。首次在 App 啟用背景存取時,以管理員密碼確認這個選項。這不會授予套件帳號共用資料夾權限。",
@@ -31,5 +31,16 @@
31
31
  "armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
32
32
  },
33
33
  "goToolchain": "go1.26.9"
34
+ },
35
+ "0.17.4": {
36
+ "x86_64": "68f372684997258adc61312f2c353e1a286a6b5aaa556f716b4e6ba6f33893d0",
37
+ "aarch64": "f6950301e201a4ebc14577a4554fa1b671f90d0f354d1e8809f4d1dafb57c723",
38
+ "armv7": "e77c9b6ae22ecfa85d9834af3350192e445987265a7ca6c02ca9b01d88a1b4f7",
39
+ "lifecycle": {
40
+ "x86_64": "d65500c4f04713a32b45d360059281391acfa5e00ee24895fdd2de7b82735372",
41
+ "aarch64": "94f6cd119186ff482b8aea56541dd1f9d53c147566bc1efdca6f5a1c86198331",
42
+ "armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
43
+ },
44
+ "goToolchain": "go1.26.9"
34
45
  }
35
46
  }
@@ -1,5 +1,15 @@
1
1
  /** SDK planner errors accepted at this versioned tool boundary. */
2
2
  export const COMPOSE_ERRORS = {
3
+ COMPOSE_STARTUP_INVALID: {
4
+ code: "COMPOSE_STARTUP_INVALID",
5
+ description: "The container startup budget is invalid.",
6
+ suggestion: "Set compose.startup_timeout_seconds to an integer from 2 to 900 seconds; the default is 120. Read the same SYNOPKGLAND_STARTUP_BUDGET_SECONDS in the App bootstrap adapter.",
7
+ },
8
+ COMPOSE_STARTUP_TIMEOUT: {
9
+ code: "COMPOSE_STARTUP_TIMEOUT",
10
+ description: "Container readiness exhausted its declared startup budget.",
11
+ suggestion: "Inspect the safe phase code in daemon/daemon.log and upstream migration/bootstrap health. Declare a sufficient bounded startup_timeout_seconds and rebuild; keep ingress closed until ready.",
12
+ },
3
13
  COMPOSE_STORAGE_INIT_INVALID: {
4
14
  code: "COMPOSE_STORAGE_INIT_INVALID",
5
15
  description: "The container storage initialization declaration is invalid.",
@@ -2,6 +2,7 @@ import type { NativeArchFamily } from "../build-spk/dsm-arch.js";
2
2
  import { type Daemon } from "./daemon.js";
3
3
  import type { StandaloneError, StandaloneManifest } from "./manifest.js";
4
4
  export interface ComposePlan {
5
+ startup_timeout_seconds?: number;
5
6
  ingress?: {
6
7
  address: string;
7
8
  subnet: string;
@@ -68,7 +69,7 @@ export interface ComposeApp {
68
69
  document: Record<string, any>;
69
70
  plan: ComposePlan;
70
71
  }
71
- export declare const COMPOSE_FIELDS: readonly ["resource_prefix", "files", "env_files", "profiles", "web", "data", "shares", "network", "services", "helpers", "sdk"];
72
+ export declare const COMPOSE_FIELDS: readonly ["startup_timeout_seconds", "resource_prefix", "files", "env_files", "profiles", "web", "data", "shares", "network", "services", "helpers", "sdk"];
72
73
  export declare function readComposeApp(value: unknown, root: string, arches: NativeArchFamily[], fail: (e: StandaloneError) => void, context?: {
73
74
  package?: string;
74
75
  sdk?: string;
@@ -1,3 +1,4 @@
1
+ import { composeDiagnosisMessages, renderComposeDiagnosis } from "../build-spk/compose-diagnosis.js";
1
2
  import { COMPOSE_ERRORS } from "./compose-errors.js";
2
3
  import { spawnSync } from "node:child_process";
3
4
  import { createHash } from "node:crypto";
@@ -10,6 +11,7 @@ import { goChildEnv } from "../runtime/go-env.js";
10
11
  import { sdkSource } from "../runtime/sdk-source.js";
11
12
  import { readDaemon } from "./daemon.js";
12
13
  export const COMPOSE_FIELDS = [
14
+ "startup_timeout_seconds",
13
15
  "resource_prefix",
14
16
  "files",
15
17
  "env_files",
@@ -69,6 +71,10 @@ export function readComposeApp(value, root, arches, fail, context = {}) {
69
71
  if (value.sdk !== undefined &&
70
72
  (typeof value.sdk !== "string" || !/^\d+\.\d+\.\d+$/.test(value.sdk)))
71
73
  return invalid("compose.sdk must name the matching SDK release.", "SDK_COMPOSE_UNSUPPORTED");
74
+ if (value.startup_timeout_seconds !== undefined &&
75
+ (!Number.isInteger(value.startup_timeout_seconds) ||
76
+ value.startup_timeout_seconds < 2 || value.startup_timeout_seconds > 900))
77
+ return invalid("compose.startup_timeout_seconds must be an integer from 2 to 900 seconds (default 120).", "COMPOSE_STARTUP_INVALID");
72
78
  const sdk = typeof value.sdk === "string" ? value.sdk : (context.sdk ?? templateSdkVersion());
73
79
  let targets = {};
74
80
  if (value.helpers !== undefined) {
@@ -107,6 +113,9 @@ export function readComposeApp(value, root, arches, fail, context = {}) {
107
113
  }
108
114
  if (result.status !== 0 || plan.error)
109
115
  throw new Error(plan.error ?? "[SDK_COMPOSE_FAILED] SDK planner failed.");
116
+ if (value.startup_timeout_seconds !== undefined &&
117
+ plan.startup_timeout_seconds !== value.startup_timeout_seconds)
118
+ throw new Error("[SDK_COMPOSE_UNSUPPORTED] This SDK does not implement the declared startup budget; use the matching SDK/CLI release.");
110
119
  const document = parse(plan.effective);
111
120
  const web = value.web;
112
121
  return {
@@ -141,7 +150,7 @@ export function composeDaemon(m) {
141
150
  args: ["run"],
142
151
  targets: m.compose.targets,
143
152
  env: {},
144
- timeouts: { start: 120, stop: 30, status: 5, uninstall: 60 },
153
+ timeouts: { start: m.compose.plan.startup_timeout_seconds ?? 120, stop: 30, status: 5, uninstall: 60 },
145
154
  };
146
155
  }
147
156
  export function composeNetwork(m) {
@@ -187,6 +196,7 @@ export function writeComposePayload(c, root, target, gateway) {
187
196
  "compose.lock.json": JSON.stringify(c.plan.lock, null, 2) + "\n",
188
197
  "request.json": JSON.stringify({ declaration: c.declaration, sdk: c.sdk, gateway }, null, 2) + "\n",
189
198
  "images.json": JSON.stringify(c.plan.image_proofs ?? {}, null, 2) + "\n",
199
+ "diagnosis-messages.json": JSON.stringify(composeDiagnosisMessages(), null, 2) + "\n",
190
200
  }))
191
201
  writeFileSync(join(target, name), content, { mode: 0o600 });
192
202
  }
@@ -195,7 +205,7 @@ export function renderComposeInstall(pkg) {
195
205
  return `#!/bin/sh
196
206
  set -eu
197
207
  "\${SYNOPKG_PKGINST_TEMP_DIR:?}/compose/control" preflight || exit $?
198
- exec "\${SYNOPKG_PKGINST_TEMP_DIR:?}/compose/control" prepare --project "\${SYNOPKG_PKGINST_TEMP_DIR:?}/compose" --package '${pkg}'
208
+ ${renderComposeDiagnosis("\${SYNOPKG_PKGINST_TEMP_DIR:?}/compose", "preinst")}exec "\${SYNOPKG_PKGINST_TEMP_DIR:?}/compose/control" prepare --project "\${SYNOPKG_PKGINST_TEMP_DIR:?}/compose" --package '${pkg}'
199
209
  `;
200
210
  }
201
211
  /** The same SDK entry point supplies the package-account install materializer. */
@@ -1,3 +1,4 @@
1
+ import { renderComposeDiagnosis } from "../build-spk/compose-diagnosis.js";
1
2
  import { composeDaemon, renderComposeInstall } from "./compose.js";
2
3
  import { renderBackgroundControl, renderBackgroundWorkerStart, renderBackgroundWorkerStop, renderUserManagerRecovery, backgroundUnitName, } from "./background.js";
3
4
  import { renderPackagePostuninst } from "../build-spk/native-lifecycle.js";
@@ -29,7 +30,13 @@ export function renderStandaloneScript(name, manifest) {
29
30
  !manifest.compose.plan.mounts_blocked
30
31
  ? `"/var/packages/${manifest.package}/target/compose/control" remember-ingress --project "/var/packages/${manifest.package}/target/compose" --package '${manifest.package}' || exit $?\n`
31
32
  : "";
32
- const ledger = preflight + remember +
33
+ const project = `\${SYNOPKG_PKGDEST:-/var/packages/${manifest.package}/target}/compose`;
34
+ const diagnosis = ["postinst", "postupgrade"].includes(name)
35
+ ? renderComposeDiagnosis(project, name)
36
+ : name === "start-stop-status"
37
+ ? `if [ "$1" = prestart ]; then\n ${renderComposeDiagnosis(project, "prestart")}fi\n`
38
+ : "";
39
+ const ledger = diagnosis + preflight + remember +
33
40
  (manifest.identityGateway && !manifest.compose.plan.mounts_blocked
34
41
  ? name === "start-stop-status"
35
42
  ? `if [ "$1" = restart ]; then\n "$0" stop || exit $?\n exec "$0" start\nfi\nif [ "$1" = start ]; then\n ${control} acquire ${paths} || exit $?\nelif [ "$1" = stop ]; then\n ${control} release ${paths} || exit $?\nfi\n`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synopackageland/cli",
3
- "version": "0.17.3",
3
+ "version": "0.17.4",
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.17.5",
30
+ "@synopackageland/templates": "0.17.6",
31
31
  "ajv": "^8.17.1",
32
32
  "commander": "^13.1.0",
33
33
  "json-canonicalize": "^3.0.1",
@@ -35,6 +35,28 @@ and `identity_gateway` for headless projects. Optional `compose.helpers` names
35
35
  architecture-specific native payloads for token/account adapters. All lifecycle
36
36
  scripts run as the package account; containers are managed by Container Manager.
37
37
 
38
+ ## Startup budget
39
+
40
+ Set `compose.startup_timeout_seconds: 600` for a slow cold start or migration.
41
+ Omitting it uses 120 seconds; only integers from 2 through 900 are accepted.
42
+ Use a matching SDK that implements the field and rebuild the App; older planners
43
+ are rejected for explicit budgets. The SDK plan drives generated daemon start
44
+ and supplies every container with `SYNOPKGLAND_STARTUP_BUDGET_SECONDS`, overriding
45
+ upstream and service-policy values for that SDK-owned key.
46
+
47
+ Custom bootstrap adapters must read that environment value once and use one
48
+ deadline across all bootstrap phases; an existing hardcoded adapter timer does
49
+ not change automatically. Keep the health endpoint unready until setup/migration
50
+ and any serving restart finish. Do not log credentials, raw API errors or bodies.
51
+
52
+ The gateway starts after readiness, so ingress remains closed while waiting.
53
+ The supervisor reserves five extra seconds for handoff/probe completion/process
54
+ stability. Timeout returns `COMPOSE_STARTUP_TIMEOUT` plus a safe last `phase` code
55
+ in the start result and `daemon/daemon.log`. Stop/uninstall limits are independent.
56
+ The budget begins after the native Container Manager worker returns; it does not
57
+ extend image pulls, native worker acquisition or Package Center deadlines. Verify
58
+ the installed DSM unit and full fresh-install lifecycle on the target NAS.
59
+
38
60
  ## Planning and artifacts
39
61
 
40
62
  The pinned compose-go reference loader merges ordered files, env files and