@synopackageland/cli 0.10.3 → 0.10.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,49 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.4 (2026-10-14)
4
+
5
+ Builds with SDK 0.10.4 and templates 0.10.4: `init` writes SDK v0.10.4 (Go
6
+ module, Rust crate tag and `backend.sdk`), `@synopackageland/sdk` 0.10.4 and
7
+ `@synopackageland/ui` 0.9.6. `check` and `build` still accept SDK 0.10.3 or
8
+ later: nothing in this CLI needs 0.10.4, and an App on 0.10.3 never sees the
9
+ stop and start codes the window now repeats. The executor digest table adds
10
+ SDK 0.10.4, whose executor and lifecycle digests equal 0.10.3, so an App
11
+ rebuilt on it keeps its approval unless its own root programs change. The
12
+ gate refuses a stale SDK `ts/dist` and builds the executor flow's race binary
13
+ before its timeout.
14
+
15
+ 繁中摘要:使用 SDK 0.10.4 與 templates 0.10.4(UI 0.9.6);`check`/`build` 仍接受 SDK
16
+ 0.10.3 以上。`ctx.call` 自動重試套件停止/啟動中的碼;非管理員的
17
+ `ctx.executor.status()` 可讀到已核准的 executor。digest 表新增 SDK 0.10.4(與 0.10.3
18
+ 相同),重建的 App 不需重新核准(除非自己的 root 程式改變)。
19
+
20
+ - `ctx.call` repeats a call the package refused with `APP_STOPPED`,
21
+ `APP_STARTING` or `EXECUTOR_STARTING` (the executor op was not sent): after
22
+ 0.5 s, doubling to at most 8 s, for 30 s of waiting in all, so an App comes
23
+ back by itself once the package has started. Other codes reject at once; past
24
+ the budget the last refusal rejects; leaving the page cancels the wait.
25
+ 繁中:套件停止/啟動中時 `ctx.call` 自動有界重試(最多等 30 s)。
26
+ - The window approval of an upgrade (outdated tasks) makes 11 scheduler calls
27
+ instead of 12: the outdated boot task is deleted by the approval's own first
28
+ EventScheduler get, made moments before the password, instead of a second
29
+ get (2.9 s on vt-nas in t-0484). The control task is still read again by id
30
+ before its delete (spec/scheduler/v1). A fresh approval stays at 8.
31
+ 繁中:升級核准的排程呼叫由 12 降為 11(boot 任務不再於刪除前重讀)。
32
+ - A scope approval reads the executor's selection back every second at most
33
+ (it backed off to 2 s), so the window sees the landed scope within a second
34
+ of `scope.confirm` (the Dialog closed 4.8 s after it in t-0482). Removing
35
+ the temporary selection task still lists Task Scheduler first, as
36
+ spec/scheduler/v1 requires of a delete.
37
+ 繁中:範圍核准讀回間隔上限 2 s → 1 s,confirmed 後 1 s 內看到結果。
38
+ - `ctx.executor.status()` for a user who is not an administrator asks the
39
+ App's `executor.status` (open to every signed-in user) instead of refusing
40
+ before any request: the pinned, running executor reads `ready`; anything
41
+ else stays `blocked` with `admin_required`. Task Scheduler is still read only
42
+ for administrators. A user no longer sees an approved executor as waiting
43
+ for approval (t-0482 F7).
44
+ 繁中:非管理員的 `ctx.executor.status()` 會向 App 的 `executor.status` 詢問,已核准且
45
+ 執行中即為 `ready`,不再一律回 `admin_required`。
46
+
3
47
  ## 0.10.3 (2026-10-14)
4
48
 
5
49
  Builds with SDK 0.10.3 and templates 0.10.3: `init` writes SDK v0.10.3 (Go
@@ -42,5 +42,16 @@
42
42
  "armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
43
43
  },
44
44
  "goToolchain": "go1.26.9"
45
+ },
46
+ "0.10.4": {
47
+ "x86_64": "4ba7040761a719e74d6b047ef68b9415e658ebb8b373b69555b8c307524e26ba",
48
+ "aarch64": "ba80b389e3291419389b66be7b0a0e1085d3dfec55c9c651fecae427279bb3d5",
49
+ "armv7": "49474467b9c9a7f31b30b5053b70eb4f71a95f2c06fbf3566e8de8f16972a299",
50
+ "lifecycle": {
51
+ "x86_64": "d65500c4f04713a32b45d360059281391acfa5e00ee24895fdd2de7b82735372",
52
+ "aarch64": "94f6cd119186ff482b8aea56541dd1f9d53c147566bc1efdca6f5a1c86198331",
53
+ "armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
54
+ },
55
+ "goToolchain": "go1.26.9"
45
56
  }
46
57
  }
@@ -144,7 +144,7 @@ Ext.ns(${JSON.stringify(ns)});
144
144
  // The SDK's clientTimeout: past the backend's request deadline
145
145
  // (ctx.requestTimeout, from backend.timeout_seconds), so its request_timeout
146
146
  // reply arrives first. options.signal aborts the fetch, for a page the user leaves.
147
- ${ns}.call = function (method, params, options, requestTimeout) {
147
+ ${ns}.callOnce = function (method, params, options, requestTimeout) {
148
148
  var signal = options && options.signal;
149
149
  var canceled = { code: "request_canceled", message: "The request was canceled" };
150
150
  if (signal && signal.aborted) {
@@ -191,6 +191,39 @@ ${ns}.call = function (method, params, options, requestTimeout) {
191
191
  });
192
192
  };
193
193
 
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
198
+ // page during a wait rejects request_canceled at once.
199
+ ${ns}.retryCodes = ["APP_STOPPED", "APP_STARTING", "EXECUTOR_STARTING"];
200
+ ${ns}.call = function (method, params, options, requestTimeout) {
201
+ var signal = options && options.signal;
202
+ var left = 30000;
203
+ var delay = 500;
204
+ var attempt = function () {
205
+ return ${ns}.callOnce(method, params, options, requestTimeout).catch(function (error) {
206
+ if (left <= 0 || !error || ${ns}.retryCodes.indexOf(error.code) < 0) throw error;
207
+ var wait = Math.min(delay, left);
208
+ left -= wait;
209
+ delay = Math.min(delay * 2, 8000);
210
+ return new Promise(function (resolve, reject) {
211
+ var leave = function () {
212
+ clearTimeout(timer);
213
+ reject({ code: "request_canceled", message: "The request was canceled" });
214
+ };
215
+ var timer = setTimeout(function () {
216
+ if (signal) signal.removeEventListener("abort", leave);
217
+ resolve(attempt());
218
+ }, wait);
219
+ if (signal && signal.aborted) leave();
220
+ else if (signal) signal.addEventListener("abort", leave);
221
+ });
222
+ });
223
+ };
224
+ return attempt();
225
+ };
226
+
194
227
  ${ns}.messages = ${JSON.stringify(manifest.i18n?.catalogs ?? { en: {} })};
195
228
  ${ns}.createI18n = (function () {
196
229
  ${I18N_RUNTIME}
@@ -58,7 +58,8 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
58
58
  var boot = task.kind === "boot";
59
59
  return send(boot ? eventAPI : taskAPI, "delete", boot ? 1 : 2, boot ? ref : {tasks: [ref]});
60
60
  }
61
- function admin() { if (!options.user() || options.user().admin !== true) throw fail("admin_required"); }
61
+ function isAdmin() { return !!options.user() && options.user().admin === true; }
62
+ function admin() { if (!isAdmin()) throw fail("admin_required"); }
62
63
  function missing() { return {state: "needs-admin-approval", reason: "Task is missing.", code: "task_missing"}; }
63
64
  // EventScheduler keys a task by its name (DSM refuses a second one with 117
64
65
  // DUPLICATE_PRIMARY_KEY) and get answers no data for a missing name, so get
@@ -155,16 +156,19 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
155
156
  var errors = {}, created = [], removed = [], withdrawFailed;
156
157
  // Delete only a verified, same-header outdated task, then create the pinned
157
158
  // replacement. set has no measured wire contract, and a same-named task
158
- // cannot be created first. A task to delete is read again by get just
159
- // before; a missing one keeps the first read: DSM refuses a second boot
160
- // task name, and the readback after the writes lists control tasks for
161
- // duplicates. Stop at the first error.
159
+ // cannot be created first. The boot task keeps this approval's first
160
+ // read: EventScheduler get by name is the whole lookup, it was made in
161
+ // this step moments ago, and delete and create address the name. A
162
+ // control task to delete is read again by the id that read listed
163
+ // (spec/scheduler/v1); a missing one keeps the first read: DSM refuses a
164
+ // second boot task name, and the readback after the writes lists control
165
+ // tasks for duplicates. Stop at the first error.
162
166
  var chain = Promise.resolve();
163
167
  options.tasks.forEach(function (task) {
164
168
  chain = chain.then(function () {
165
169
  if (Object.keys(errors).length) return;
166
170
  var first = before.tasks[task.kind];
167
- return (first.ref ? find(task, true) : Promise.resolve(first)).then(function (current) {
171
+ return (first.ref && task.kind !== "boot" ? find(task, true) : Promise.resolve(first)).then(function (current) {
168
172
  if (current.state === "ready") return;
169
173
  if (current.state === "blocked") throw refused(current);
170
174
  var deleted = current.ref ? remove(task, current.ref).then(function () { removed.push({kind: task.kind, name: task.name, header: task.header, script: current.script}); }) : Promise.resolve();
@@ -227,9 +231,10 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
227
231
  }
228
232
  // The task waits up to 35 s for the executor, whose account, AppPriv and
229
233
  // share lookups took 16-20 s on a measured NAS: read back for 40 s,
230
- // backing off from 500 ms to 2 s like health.
234
+ // backing off from 500 ms to 1 s, so the window closes within a second
235
+ // of the selection landing.
231
236
  function readSelection(budget, delay) {
232
- delay = Math.min(delay || 500, 2000);
237
+ delay = Math.min(delay || 500, 1000);
233
238
  verified.state = "ready";
234
239
  return health(verified, 0).then(function (current) {
235
240
  if (current.state === "ready" && current.executor.selection && current.executor.selection.scope === selectedScope && Number.isSafeInteger(current.executor.selection.generation) && current.executor.selection.generation > 0) return current;
@@ -323,8 +328,17 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
323
328
  // result reaches every view so neither shows a stale state.
324
329
  var listeners = [];
325
330
  function publish(result) { listeners.forEach(function (listener) { listener(result); }); return result; }
331
+ // TaskScheduler answers administrators only. Anyone else reads the approved
332
+ // executor through the App's executor.status, which answers every user: the
333
+ // pinned, running executor is ready; otherwise an administrator is needed.
334
+ function userStatus() {
335
+ var needed = block({tasks: {}}, fail("admin_required"));
336
+ return options.call("executor.status", {}).then(function (status) {
337
+ return pinned(status) ? {state: "ready", tasks: {}, executor: status} : needed;
338
+ }, function () { return needed; });
339
+ }
326
340
  return {
327
- status: function () { return read().then(function (verified) { return health(verified, 0); }).then(publish); },
341
+ status: function () { return (isAdmin() ? read().then(function (verified) { return health(verified, 0); }) : userStatus()).then(publish); },
328
342
  authorize: function (password, selectedScope) {
329
343
  if (!busy) busy = Promise.resolve().then(function () { return approve(password, selectedScope); }).catch(function (e) { var r = block({tasks: {}}, e); r.retry = !!(e && e.passwordConfirm); return r; }).then(function (r) { busy = null; return publish(r); });
330
344
  return busy;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synopackageland/cli",
3
- "version": "0.10.3",
3
+ "version": "0.10.4",
4
4
  "description": "Syno Package Land Developer Kit CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,12 +19,13 @@
19
19
  "scripts": {
20
20
  "build": "rm -rf dist && tsc",
21
21
  "test": "npm run build && node --import tsx --test test/**/*.test.ts",
22
- "test:scripts": "node --test scripts/test/gate.test.mjs scripts/test/release.test.mjs scripts/test/pack-docker.test.mjs scripts/test/npm-pack.test.mjs scripts/test/executor-bump.test.mjs scripts/test/smoke-exports.test.mjs scripts/test/executor-digests.test.mjs",
23
- "gates": "bash scripts/ci/gate.sh npm && bash scripts/ci/gate.sh native-go && bash scripts/ci/gate.sh daemon-gateway",
22
+ "test:scripts": "node --test scripts/test/gate.test.mjs scripts/test/release.test.mjs scripts/test/pack-docker.test.mjs scripts/test/npm-pack.test.mjs scripts/test/executor-bump.test.mjs scripts/test/smoke-exports.test.mjs scripts/test/executor-digests.test.mjs scripts/test/parallel.test.mjs scripts/test/scaffold-matrix.test.mjs scripts/test/frontend-deps.test.mjs scripts/test/docker-gate.test.mjs scripts/test/gate-plan.test.mjs scripts/test/gates.test.mjs",
23
+ "gates": "bash scripts/ci/gates.sh",
24
+ "gates:fast": "bash scripts/ci/gates.sh --fast",
24
25
  "prepublishOnly": "npm run build"
25
26
  },
26
27
  "dependencies": {
27
- "@synopackageland/templates": "0.10.3",
28
+ "@synopackageland/templates": "0.10.4",
28
29
  "ajv": "^8.17.1",
29
30
  "commander": "^13.1.0",
30
31
  "tar": "^7.4.3",