@synopackageland/cli 0.10.6 → 0.10.8

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,75 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.8 (2026-10-15)
4
+
5
+ Builds with SDK 0.10.7 and templates 0.10.8, as 0.10.7 did: `init` still
6
+ writes SDK v0.10.7, `@synopackageland/sdk` 0.10.7 and `@synopackageland/ui`
7
+ 0.9.8, and the executor digest table is unchanged, so an App rebuilt on it
8
+ keeps its approval unless its executor or policy changes. Only the DSM
9
+ window's `ctx.call` changes. The CLI still needs SDK 0.10.5 or later: the
10
+ SDK's Gate, `useLifecycleRetry`, `appLifecycle` and `ExecutorApproval` already
11
+ read an `APP_STOPPED` that arrives at once and check again; SDK 0.10.7 changed
12
+ only its protocol README's wording, and nothing in the CLI depends on it.
13
+
14
+ 繁中摘要:搭配 SDK 0.10.7、templates 0.10.8(UI 0.9.8),digest 不變;SDK 下限維持 0.10.5
15
+ (SDK 的 Gate/`useLifecycleRetry`/`appLifecycle`/`ExecutorApproval` 本來就處理立即回的
16
+ `APP_STOPPED`)。只改 DSM window 的 `ctx.call`。
17
+
18
+ - `ctx.call` rejects `APP_STOPPED` at once: a stopped package waits for the
19
+ user to start it in Package Center, so the SDK's stopped copy shows within a
20
+ page's next read instead of 30 s later (Download Hub 29.3 s, t-0527; Reclaim
21
+ showed its old content or loading, t-0528). `APP_STARTING` and
22
+ `EXECUTOR_STARTING` repeat for 4 s of waiting (0.5, 1, 2, 0.5 s), so a short
23
+ start still shows the page nothing and the SDK's starting copy follows a
24
+ longer one within its own 4 s recheck. `executor.control`, which the window's
25
+ approval starts the executor with, still repeats a start for 30 s; the SDK's
26
+ status reads are still not repeated.
27
+ 繁中:套件停止時 `ctx.call` 立即回 `APP_STOPPED`;啟動中最多重試 4 s(`executor.control` 維持 30 s)。
28
+
29
+ ### Behavior changes
30
+
31
+ - An App rebuilt with CLI 0.10.8 shows a stopped package's copy within its
32
+ page's next read: `ctx.call` rejects `APP_STOPPED` at once instead of after
33
+ about 30 s, and repeats `APP_STARTING` and `EXECUTOR_STARTING` for at most
34
+ about 4 s. The App's code needs no change; only its devDependency
35
+ `@synopackageland/cli` 0.10.8 and its lockfile, then a rebuild.
36
+ - An e2e test that waits only for the Gate's stopped title
37
+ (`authorization.stopped.title`) can miss it: `ExecutorApproval` now takes
38
+ over first and shows its own stopped copy. Accept either of the SDK's stopped
39
+ copies.
40
+ 繁中:App 升到 CLI 0.10.8 重新 build 後,套件停止時頁面立即收到 `APP_STOPPED`(不再等 30 s),
41
+ 啟動中最多重試約 4 s;App 不需改程式。E2E 若只等 Gate 的停止標題,應接受 SDK 的任一停止文案。
42
+
43
+ ## 0.10.7 (2026-10-15)
44
+
45
+ Builds with SDK 0.10.7 and templates 0.10.8: `init` writes SDK v0.10.7 (Go
46
+ module, Rust crate tag and `backend.sdk`), `@synopackageland/sdk` 0.10.7 and
47
+ `@synopackageland/ui` 0.9.8. The executor digest table adds SDK 0.10.7, whose
48
+ executor and lifecycle digests equal 0.10.6, so an App rebuilt on it keeps its
49
+ approval unless its executor or policy changes. The CLI still needs SDK
50
+ 0.10.5 or later: `ctx.portal.lastStatus()` is new and optional, SDK 0.10.7's
51
+ `PortalSettings` keeps its placeholder rows without it, and older SDKs do not
52
+ read it; nothing in the CLI depends on SDK 0.10.7. An App gets the settings
53
+ page without placeholder movement only with both SDK 0.10.7 and a DSM window
54
+ built by CLI 0.10.7.
55
+
56
+ 繁中摘要:搭配 SDK 0.10.7、templates 0.10.8(UI 0.9.8);digest 與 0.10.6 相同;SDK 下限維持
57
+ 0.10.5(`ctx.portal.lastStatus()` 是選用的新增,舊 SDK 不讀、SDK 0.10.7 沒有它就畫佔位列)。
58
+ e2e 新增 `openUntilLoaded`(`signIn` 遇 `net::ERR_ABORTED` 最多重開約 30 s)、遮蔽 socket.io `sid=`。
59
+
60
+ - `signIn` and `dsmLoginSetup` keep reopening DSM's login page while the
61
+ navigation fails with `net::ERR_ABORTED`, for up to about 30 s, after a saved
62
+ profile is refused (error 119); they then fail with a clear error. Any other
63
+ error still fails at once. The helper is exported as
64
+ `openUntilLoaded(app, path, {timeoutMs, pauseMs})`.
65
+ - e2e output and report masking also masks the `sid=` query parameter of DSM's
66
+ socket.io URLs, which carries the connection id.
67
+ - `ctx.portal` reads the portal status when the DSM window opens and keeps
68
+ the newest one it read (`lastStatus()`); a `status()` asked for while a read
69
+ runs shares it. The SDK's `PortalSettings` starts from it, so a settings
70
+ page no longer moves when its placeholder rows become the real section
71
+ (Reclaim: 100 px, t-0520).
72
+
3
73
  ## 0.10.6 (2026-10-15)
4
74
 
5
75
  Builds with SDK 0.10.6 and templates 0.10.7: `init` writes SDK v0.10.6 (Go
package/dist/e2e/dsm.d.ts CHANGED
@@ -32,7 +32,7 @@ export declare function freePort(): Promise<number>;
32
32
  * command's exit code.
33
33
  */
34
34
  export declare function runWithTunnel(command: readonly string[], outer?: Env): Promise<number>;
35
- /** `text` with DSM's session cookies, X-SYNO-TOKEN, SynoToken and _sid values replaced by ***. */
35
+ /** `text` with DSM's session cookies, X-SYNO-TOKEN, SynoToken, _sid and sid values replaced by ***. */
36
36
  export declare function redactDsmSecrets(text: string): string;
37
37
  /**
38
38
  * Makes an e2e output directory private (directories 700, files 600) and masks
@@ -140,6 +140,18 @@ export declare function dsmSessionLive(page: Pick<SessionPage, "evaluate">): Pro
140
140
  * thrown.
141
141
  */
142
142
  export declare function openPath(app: Pick<App, "open">, path: string): Promise<void>;
143
+ /**
144
+ * `app.open(path)`, repeated while it fails with net::ERR_ABORTED: a refused
145
+ * session's old desktop JS keeps navigating and aborted several opens in a row.
146
+ * Gives up with a clear error after `timeoutMs` (30 s); any other failure is
147
+ * thrown at once.
148
+ */
149
+ export declare function openUntilLoaded(app: Pick<App, "open">, path: string, { timeoutMs, pauseMs, now, sleep }?: {
150
+ timeoutMs?: number | undefined;
151
+ pauseMs?: number | undefined;
152
+ now?: (() => number) | undefined;
153
+ sleep?: ((ms: number) => Promise<void>) | undefined;
154
+ }): Promise<void>;
143
155
  /**
144
156
  * Restores the saved profile of `user`; signs in with the account file only
145
157
  * when that profile is missing, no longer reaches the desktop, or gives a
package/dist/e2e/dsm.js CHANGED
@@ -138,10 +138,10 @@ const mask = "***";
138
138
  const cookieHeader = /(\bcookie"?\s*[:=]\s*"?)([^"\\\r\n]*)/gi;
139
139
  const sessionCookieNames = ["id", "did", "_SSID"];
140
140
  const sessionCookie = new RegExp(`(^|;\\s*)(${sessionCookieNames.join("|")})=[^;\\s]*`, "gi");
141
- // The X-SYNO-TOKEN header, a SynoToken parameter or field, and a web API's _sid parameter.
141
+ // The X-SYNO-TOKEN header, a SynoToken parameter or field, a web API's _sid parameter and a socket.io connection's sid parameter.
142
142
  const tokenValue = /(\b(?:x-syno-token|synotoken)"?\s*[:=]\s*"?)[^"\\\s,;&}]+/gi;
143
- const sidParameter = /([?&]_sid=)[^&"\\\s]+/gi;
144
- /** `text` with DSM's session cookies, X-SYNO-TOKEN, SynoToken and _sid values replaced by ***. */
143
+ const sidParameter = /([?&]_?sid=)[^&"\\\s]+/gi;
144
+ /** `text` with DSM's session cookies, X-SYNO-TOKEN, SynoToken, _sid and sid values replaced by ***. */
145
145
  export function redactDsmSecrets(text) {
146
146
  return text
147
147
  .replace(cookieHeader, (_match, header, cookies) => header + cookies.replace(sessionCookie, `$1$2=${mask}`))
@@ -419,6 +419,30 @@ export async function openPath(app, path) {
419
419
  await app.open(path);
420
420
  }
421
421
  }
422
+ /**
423
+ * `app.open(path)`, repeated while it fails with net::ERR_ABORTED: a refused
424
+ * session's old desktop JS keeps navigating and aborted several opens in a row.
425
+ * Gives up with a clear error after `timeoutMs` (30 s); any other failure is
426
+ * thrown at once.
427
+ */
428
+ export async function openUntilLoaded(app, path, { timeoutMs = 30_000, pauseMs = 1_000, now = Date.now, sleep = (ms) => new Promise(done => setTimeout(done, ms)) } = {}) {
429
+ const deadline = now() + timeoutMs;
430
+ for (;;) {
431
+ try {
432
+ await app.open(path);
433
+ return;
434
+ }
435
+ catch (error) {
436
+ const message = String(error?.message ?? error);
437
+ if (!/net::ERR_ABORTED/.test(message))
438
+ throw error;
439
+ if (now() + pauseMs >= deadline) {
440
+ throw new Error(`Opening ${path} still aborted after ${Math.round(timeoutMs / 1000)} s of retries: ${message}`);
441
+ }
442
+ await sleep(pauseMs);
443
+ }
444
+ }
445
+ }
422
446
  async function restoredSessionLive(page) {
423
447
  return await desktopOrLogin(page) === "desktop" && await dsmSessionLive(page);
424
448
  }
@@ -442,7 +466,7 @@ export async function signIn(app, user = "admin", env = process.env) {
442
466
  // A refused session still shows the desktop; dropping its cookies shows the login form.
443
467
  if (profile) {
444
468
  await context.clearCookies();
445
- await openPath(app, "/");
469
+ await openUntilLoaded(app, "/");
446
470
  }
447
471
  const account = readAccount(accountFile(user, env), user);
448
472
  await page.locator(loginSelector).fill(account.username);
@@ -75,5 +75,16 @@
75
75
  "armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
76
76
  },
77
77
  "goToolchain": "go1.26.9"
78
+ },
79
+ "0.10.7": {
80
+ "x86_64": "4ba7040761a719e74d6b047ef68b9415e658ebb8b373b69555b8c307524e26ba",
81
+ "aarch64": "ba80b389e3291419389b66be7b0a0e1085d3dfec55c9c651fecae427279bb3d5",
82
+ "armv7": "49474467b9c9a7f31b30b5053b70eb4f71a95f2c06fbf3566e8de8f16972a299",
83
+ "lifecycle": {
84
+ "x86_64": "d65500c4f04713a32b45d360059281391acfa5e00ee24895fdd2de7b82735372",
85
+ "aarch64": "94f6cd119186ff482b8aea56541dd1f9d53c147566bc1efdca6f5a1c86198331",
86
+ "armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
87
+ },
88
+ "goToolchain": "go1.26.9"
78
89
  }
79
90
  }
@@ -192,17 +192,21 @@ ${ns}.callOnce = function (method, params, options, requestTimeout) {
192
192
  };
193
193
 
194
194
  // APP_STOPPED, APP_STARTING and EXECUTOR_STARTING mean the executor op was not
195
- // sent: the package is stopped, or starting. The call repeats on its own, half a
196
- // second, then doubling up to 8 s, for 30 s of waiting in all; each attempt keeps
197
- // its own deadline. Past that it rejects with the last refusal, and leaving the
195
+ // sent. A stopped package waits for the user to start it in Package Center, so
196
+ // APP_STOPPED rejects at once (t-0529). A start ends by itself: a call repeats
197
+ // half a second, then doubling, for 4 s of waiting in all, so a short start
198
+ // shows the page nothing and the SDK's starting copy follows a long one within
199
+ // its own 4 s recheck; executor.control, which the window's approval starts the
200
+ // executor with, repeats up to 8 s apart for 30 s. Each attempt keeps its own
201
+ // deadline; past the wait it rejects with the last refusal, and leaving the
198
202
  // page during a wait rejects request_canceled at once. The SDK's status reads
199
203
  // are not repeated: its panels show the stopped or starting package and check
200
- // again on their own, and a page waiting here shows nothing (t-0503 R1).
201
- ${ns}.retryCodes = ["APP_STOPPED", "APP_STARTING", "EXECUTOR_STARTING"];
204
+ // again on their own (t-0503 R1).
205
+ ${ns}.retryCodes = ["APP_STARTING", "EXECUTOR_STARTING"];
202
206
  ${ns}.statusReads = ["delegate.status", "authorization.status", "executor.status"];
203
207
  ${ns}.call = function (method, params, options, requestTimeout) {
204
208
  var signal = options && options.signal;
205
- var left = ${ns}.statusReads.indexOf(method) < 0 ? 30000 : 0;
209
+ var left = ${ns}.statusReads.indexOf(method) >= 0 ? 0 : method === "executor.control" ? 30000 : 4000;
206
210
  var delay = 500;
207
211
  var attempt = function () {
208
212
  return ${ns}.callOnce(method, params, options, requestTimeout).catch(function (error) {
@@ -17,6 +17,9 @@ export declare function portalCatalogs(sdkDir: string): PortalCatalogs;
17
17
  * status or reject `{code}`. With `options.entry` (below DSM 7.2) the
18
18
  * backend's `portal.status` also reports the executor's entry, and
19
19
  * administrators get `register()` and `unregister()`; `openControlPanel()` opens Login Portal > Applications.
20
+ * `lastStatus()` is the newest status read; the access reads one when it is
21
+ * created, as the window opens, and a `status()` asked for while a read runs
22
+ * shares it.
20
23
  * `messages` is the SDK's panel text in the window's locale; `mountPanel(element)`
21
24
  * renders a plain panel for UIs without their own.
22
25
  */
@@ -24,6 +24,9 @@ export function portalCatalogs(sdkDir) {
24
24
  * status or reject `{code}`. With `options.entry` (below DSM 7.2) the
25
25
  * backend's `portal.status` also reports the executor's entry, and
26
26
  * administrators get `register()` and `unregister()`; `openControlPanel()` opens Login Portal > Applications.
27
+ * `lastStatus()` is the newest status read; the access reads one when it is
28
+ * created, as the window opens, and a `status()` asked for while a read runs
29
+ * shares it.
27
30
  * `messages` is the SDK's panel text in the window's locale; `mountPanel(element)`
28
31
  * renders a plain panel for UIs without their own.
29
32
  */
@@ -88,29 +91,49 @@ export const portalRuntime = (catalogs) => `function createPortalAccess(options)
88
91
  // The backend reports cleanup-required while a register or unregister runs,
89
92
  // so a status read waits for the change, whatever its result.
90
93
  var changing = Promise.resolve();
91
- function status() {
94
+ // last: the newest status read, which the SDK's PortalSettings shows before it
95
+ // reads again. A read that runs serves every status() asked for meanwhile,
96
+ // until a change starts; the change reads afresh.
97
+ var last, pending, reads = 0;
98
+ function read() {
99
+ var n = ++reads;
92
100
  return changing.then(function () {
93
101
  if (options.entry) return fromEntry();
94
102
  return options.isAdmin() ? fromDsm() : fromBackend();
103
+ }).then(function (out) {
104
+ if (n === reads) last = out;
105
+ return out;
95
106
  });
96
107
  }
108
+ function status() {
109
+ if (!pending) {
110
+ pending = read();
111
+ var done = function () { pending = undefined; };
112
+ pending.then(done, done);
113
+ }
114
+ return pending;
115
+ }
97
116
  function change(method) {
98
117
  if (!options.isAdmin()) return Promise.reject({ code: "admin_required" });
99
118
  var call = options.call(method, {});
100
119
  changing = call.then(function () {}, function () {});
101
- return call.then(status, function (error) {
120
+ pending = undefined;
121
+ return call.then(read, function (error) {
102
122
  throw { code: error && error.code === "ADMIN_REQUIRED" ? "admin_required" : "PORTAL_FAILED" };
103
123
  });
104
124
  }
105
125
  function set(alias) {
106
126
  if (!options.isAdmin()) return Promise.reject({ code: "admin_required" });
107
- return options.send("SYNO.Core.AppPortal", "set", 2, { id: options.appId, alias: alias }).then(status, function (error) {
127
+ return options.send("SYNO.Core.AppPortal", "set", 2, { id: options.appId, alias: alias }).then(read, function (error) {
108
128
  throw { code: ERRORS[error && error.code] || "portal_unavailable" };
109
129
  });
110
130
  }
111
131
  var access = {
112
132
  messages: messages,
113
133
  status: status,
134
+ lastStatus: function () {
135
+ return last;
136
+ },
114
137
  apply: function (alias) {
115
138
  if (typeof alias !== "string" || !ALIAS.test(alias)) return Promise.reject({ code: "portal_alias_invalid" });
116
139
  if (RESERVED.indexOf(alias) >= 0) return Promise.reject({ code: "portal_alias_reserved" });
@@ -180,5 +203,8 @@ export const portalRuntime = (catalogs) => `function createPortalAccess(options)
180
203
  dispose: function () { disposed = true; element.replaceChildren(); }
181
204
  };
182
205
  }
206
+ // The window reads the status when it opens, so a settings page shows the
207
+ // portal section as it is (t-0520).
208
+ status();
183
209
  return access;
184
210
  }`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synopackageland/cli",
3
- "version": "0.10.6",
3
+ "version": "0.10.8",
4
4
  "description": "Syno Package Land Developer Kit CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,7 +25,7 @@
25
25
  "prepublishOnly": "npm run build"
26
26
  },
27
27
  "dependencies": {
28
- "@synopackageland/templates": "0.10.7",
28
+ "@synopackageland/templates": "0.10.8",
29
29
  "ajv": "^8.17.1",
30
30
  "commander": "^13.1.0",
31
31
  "tar": "^7.4.3",