@synopackageland/cli 0.7.0 → 0.7.2

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.
Files changed (165) hide show
  1. package/CHANGELOG.md +301 -1
  2. package/assets/default-icon/README.md +19 -0
  3. package/assets/default-icon/default-icon-small.svg +9 -0
  4. package/assets/default-icon/default-icon.svg +10 -0
  5. package/assets/default-icon/icon_16.png +0 -0
  6. package/assets/default-icon/icon_24.png +0 -0
  7. package/assets/default-icon/icon_256.png +0 -0
  8. package/assets/default-icon/icon_32.png +0 -0
  9. package/assets/default-icon/icon_48.png +0 -0
  10. package/assets/default-icon/icon_64.png +0 -0
  11. package/assets/default-icon/icon_72.png +0 -0
  12. package/dist/add.d.ts +4 -1
  13. package/dist/add.js +14 -9
  14. package/dist/build-spk/dsm-icons.d.ts +31 -2
  15. package/dist/build-spk/dsm-icons.js +135 -56
  16. package/dist/build-spk/dsm-platforms.json +4 -1
  17. package/dist/build-spk/gateway.d.ts +1 -1
  18. package/dist/build-spk/gateway.js +8 -4
  19. package/dist/build-spk/generator.js +17 -3
  20. package/dist/build-spk/go-toolchain.d.ts +20 -0
  21. package/dist/build-spk/go-toolchain.js +44 -0
  22. package/dist/build-spk/png.d.ts +14 -0
  23. package/dist/build-spk/png.js +161 -0
  24. package/dist/build-spk/ui-texts.d.ts +20 -0
  25. package/dist/build-spk/ui-texts.js +42 -0
  26. package/dist/cli.js +39 -9
  27. package/dist/contract/i18n.d.ts +5 -0
  28. package/dist/contract/i18n.js +45 -0
  29. package/dist/contract/icon.d.ts +5 -0
  30. package/dist/contract/icon.js +27 -0
  31. package/dist/contract/validate.js +4 -0
  32. package/dist/contract/web-launch.js +6 -2
  33. package/dist/deploy/native-install-values.d.ts +17 -0
  34. package/dist/deploy/native-install-values.js +32 -0
  35. package/dist/deploy/plan.d.ts +6 -2
  36. package/dist/deploy/plan.js +57 -6
  37. package/dist/error-registry.js +15 -6
  38. package/dist/host/dsm-client.js +53 -17
  39. package/dist/host/dsm-http.js +24 -2
  40. package/dist/host/types.d.ts +1 -0
  41. package/dist/info.js +28 -9
  42. package/dist/init.js +1 -1
  43. package/dist/runtime/dsm-version.d.ts +6 -0
  44. package/dist/runtime/dsm-version.js +8 -0
  45. package/dist/runtime/executor-build.d.ts +1 -1
  46. package/dist/runtime/executor-build.js +8 -5
  47. package/dist/standalone/daemon.d.ts +2 -1
  48. package/dist/standalone/daemon.js +10 -6
  49. package/dist/standalone/dsm-window.d.ts +2 -0
  50. package/dist/standalone/dsm-window.js +26 -40
  51. package/dist/standalone/i18n.d.ts +12 -0
  52. package/dist/standalone/i18n.js +30 -0
  53. package/dist/standalone/inspect-spk.d.ts +5 -0
  54. package/dist/standalone/inspect-spk.js +41 -2
  55. package/dist/standalone/manifest.d.ts +5 -6
  56. package/dist/standalone/manifest.js +21 -34
  57. package/dist/standalone/native-authorization-window.d.ts +1 -1
  58. package/dist/standalone/native-authorization-window.js +7 -1
  59. package/dist/standalone/native-layout.d.ts +2 -0
  60. package/dist/standalone/native-layout.js +22 -1
  61. package/dist/standalone/native-root-window.d.ts +4 -2
  62. package/dist/standalone/native-root-window.js +33 -18
  63. package/dist/standalone/root-window.d.ts +4 -1
  64. package/dist/standalone/root-window.js +143 -33
  65. package/dist/standalone/spk.js +7 -4
  66. package/dist/templates/_daemon/go.mod +1 -1
  67. package/dist/templates/_daemon/main.go +6 -3
  68. package/dist/templates/_daemon/main_test.go +21 -1
  69. package/dist/templates/_daemon/testdata/child.go +3 -0
  70. package/dist/templates/_executor/README.md +4 -1
  71. package/dist/templates/_executor/anchor.go +10 -0
  72. package/dist/templates/_executor/anchor_test.go +40 -5
  73. package/dist/templates/_executor/digest_review_test.go +2 -2
  74. package/dist/templates/_executor/dsm.go +40 -3
  75. package/dist/templates/_executor/go.mod +2 -0
  76. package/dist/templates/_executor/main.go +11 -5
  77. package/dist/templates/_executor/managed.go +3 -3
  78. package/dist/templates/_executor/managed_test.go +3 -3
  79. package/dist/templates/_executor/path_test.go +18 -0
  80. package/dist/templates/_executor/pid.go +37 -0
  81. package/dist/templates/_executor/pid_test.go +113 -0
  82. package/dist/templates/_executor/platform_other.go +8 -0
  83. package/dist/templates/_executor/purge_log.go +18 -0
  84. package/dist/templates/_executor/tasks.go +45 -7
  85. package/dist/templates/_executor/tasks_dsm71_test.go +114 -0
  86. package/dist/templates/_executor/tasks_test.go +58 -1
  87. package/dist/templates/_executor/upgrade_watch_test.go +13 -5
  88. package/dist/templates/_gateway/go.mod +2 -0
  89. package/dist/templates/binary/dsm-package.yaml +3 -0
  90. package/dist/templates/compose/synology-app.yaml +3 -0
  91. package/dist/templates/native-go/backend/go.mod +1 -1
  92. package/dist/templates/native-go/backend/internal/filestation/client.go +39 -3
  93. package/dist/templates/native-go/backend/internal/filestation/download_dsm71_test.go +82 -0
  94. package/dist/templates/native-go/backend/internal/identity/README.md +5 -1
  95. package/dist/templates/native-go/backend/internal/identity/dsm.go +24 -5
  96. package/dist/templates/native-go/backend/internal/identity/http.go +1 -1
  97. package/dist/templates/native-go/backend/internal/identity/identity_test.go +48 -1
  98. package/dist/templates/native-go/backend/internal/identity/service.go +1 -1
  99. package/dist/templates/native-go/dsm-package.yaml +3 -0
  100. package/dist/templates/native-go-delegate/frontend/test/delegate.test.ts +15 -0
  101. package/dist/templates/native-go-root-ops/backend/internal/app/executor.go +30 -2
  102. package/dist/templates/native-go-root-ops/backend/internal/app/executor_test.go +64 -0
  103. package/dist/templates/native-go-vue/frontend/src/delegate/ExecutorSettings.vue +3 -2
  104. package/dist/templates/native-go-vue/frontend/src/delegate/executor-settings.test.ts +23 -0
  105. package/dist/templates/native-go-vue/frontend/src/delegate/executor-setup.ts +5 -1
  106. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/de.json +1 -0
  107. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/en.json +1 -0
  108. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/es.json +1 -0
  109. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/fr.json +1 -0
  110. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/it.json +1 -0
  111. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/ja.json +1 -0
  112. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/ko.json +1 -0
  113. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/zh-CN.json +1 -0
  114. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/zh-TW.json +1 -0
  115. package/package.json +2 -1
  116. package/schema/synology-app.schema.json +22 -0
  117. package/templates/_daemon/go.mod +1 -1
  118. package/templates/_daemon/main.go +6 -3
  119. package/templates/_daemon/main_test.go +21 -1
  120. package/templates/_daemon/testdata/child.go +3 -0
  121. package/templates/_executor/README.md +4 -1
  122. package/templates/_executor/anchor.go +10 -0
  123. package/templates/_executor/anchor_test.go +40 -5
  124. package/templates/_executor/digest_review_test.go +2 -2
  125. package/templates/_executor/dsm.go +40 -3
  126. package/templates/_executor/go.mod +2 -0
  127. package/templates/_executor/main.go +11 -5
  128. package/templates/_executor/managed.go +3 -3
  129. package/templates/_executor/managed_test.go +3 -3
  130. package/templates/_executor/path_test.go +18 -0
  131. package/templates/_executor/pid.go +37 -0
  132. package/templates/_executor/pid_test.go +113 -0
  133. package/templates/_executor/platform_other.go +8 -0
  134. package/templates/_executor/purge_log.go +18 -0
  135. package/templates/_executor/tasks.go +45 -7
  136. package/templates/_executor/tasks_dsm71_test.go +114 -0
  137. package/templates/_executor/tasks_test.go +58 -1
  138. package/templates/_executor/upgrade_watch_test.go +13 -5
  139. package/templates/_gateway/go.mod +2 -0
  140. package/templates/binary/dsm-package.yaml +3 -0
  141. package/templates/compose/synology-app.yaml +3 -0
  142. package/templates/native-go/backend/go.mod +1 -1
  143. package/templates/native-go/backend/internal/filestation/client.go +39 -3
  144. package/templates/native-go/backend/internal/filestation/download_dsm71_test.go +82 -0
  145. package/templates/native-go/backend/internal/identity/README.md +5 -1
  146. package/templates/native-go/backend/internal/identity/dsm.go +24 -5
  147. package/templates/native-go/backend/internal/identity/http.go +1 -1
  148. package/templates/native-go/backend/internal/identity/identity_test.go +48 -1
  149. package/templates/native-go/backend/internal/identity/service.go +1 -1
  150. package/templates/native-go/dsm-package.yaml +3 -0
  151. package/templates/native-go-delegate/frontend/test/delegate.test.ts +15 -0
  152. package/templates/native-go-root-ops/backend/internal/app/executor.go +30 -2
  153. package/templates/native-go-root-ops/backend/internal/app/executor_test.go +64 -0
  154. package/templates/native-go-vue/frontend/src/delegate/ExecutorSettings.vue +3 -2
  155. package/templates/native-go-vue/frontend/src/delegate/executor-settings.test.ts +23 -0
  156. package/templates/native-go-vue/frontend/src/delegate/executor-setup.ts +5 -1
  157. package/templates/native-go-vue/frontend/src/delegate/i18n/de.json +1 -0
  158. package/templates/native-go-vue/frontend/src/delegate/i18n/en.json +1 -0
  159. package/templates/native-go-vue/frontend/src/delegate/i18n/es.json +1 -0
  160. package/templates/native-go-vue/frontend/src/delegate/i18n/fr.json +1 -0
  161. package/templates/native-go-vue/frontend/src/delegate/i18n/it.json +1 -0
  162. package/templates/native-go-vue/frontend/src/delegate/i18n/ja.json +1 -0
  163. package/templates/native-go-vue/frontend/src/delegate/i18n/ko.json +1 -0
  164. package/templates/native-go-vue/frontend/src/delegate/i18n/zh-CN.json +1 -0
  165. package/templates/native-go-vue/frontend/src/delegate/i18n/zh-TW.json +1 -0
@@ -4,7 +4,7 @@ import { existsSync, readFileSync } from "node:fs";
4
4
  import { isAbsolute, join, relative, resolve } from "node:path";
5
5
  import { parse } from "yaml";
6
6
  import { isNativeArchFamily } from "../build-spk/dsm-arch.js";
7
- import { checkGlossary, isLocale, loadCatalogs } from "./i18n.js";
7
+ import { checkGlossary, loadCatalogs, readInfoTranslations } from "./i18n.js";
8
8
  import { loadRootOps, ROOT_OPS_DIR } from "./root-ops.js";
9
9
  import { readDaemon } from "./daemon.js";
10
10
  import { readBackground } from "./background.js";
@@ -12,6 +12,7 @@ import { DEFAULT_MIN_DSM, isDsmAtLeast, PORTAL_MIN_DSM, portalMinDsmMessage } fr
12
12
  import { readSharedFolder } from "./shared-folder.js";
13
13
  import { floorAtAdministrators, permitsOrdinaryUsers, readApiPermissions } from "./api-permissions.js";
14
14
  import { readPortalDeclaration } from "../portal/alias.js";
15
+ import { renderPackageIcons } from "../build-spk/dsm-icons.js";
15
16
  /**
16
17
  * `dsm-package.yaml`: a Standalone Package, i.e. an unsigned native SPK that
17
18
  * runs as its own DSM package account with a CGI backend and a DSM window UI.
@@ -19,10 +20,9 @@ import { readPortalDeclaration } from "../portal/alias.js";
19
20
  export const STANDALONE_MANIFEST = "dsm-package.yaml";
20
21
  export const KNOWN_FIELDS = new Set([
21
22
  "schema", "package", "version", "build", "displayname", "description",
22
- "metadata", "maintainer", "os_min_ver", "arches", "backend", "ui", "capabilities", "i18n", "daemon", "install", "entry", "portal",
23
+ "metadata", "maintainer", "os_min_ver", "arches", "backend", "ui", "capabilities", "i18n", "daemon", "install", "entry", "portal", "icon",
23
24
  ]);
24
25
  const I18N_FIELDS = new Set(["messages", "glossary", "info"]);
25
- const INFO_TEXT_FIELDS = new Set(["displayname", "description"]);
26
26
  const PACKAGE_ID = /^[A-Za-z][A-Za-z0-9]{1,63}$/;
27
27
  const VERSION = /^\d+(\.\d+){0,3}$/;
28
28
  const OS_MIN_VER = /^\d+\.\d+(\.\d+)?-\d+$/;
@@ -154,6 +154,7 @@ function readManifest(raw, c) {
154
154
  if (capabilities.delegate?.scope === "install-selected" && ui && !ui.build)
155
155
  c.fail("CAPABILITY_COMBINATION_INVALID", "delegate scope install-selected 只能在 Vue 面板確認;plain UI 不支援,請改用 vue UI(init --ui vue)。");
156
156
  const i18n = raw.i18n === undefined ? undefined : readI18n(raw.i18n, c);
157
+ const icon = raw.icon === undefined ? undefined : readIcon(raw.icon, c);
157
158
  const portal = raw.portal === undefined || !packageId ? undefined : readPortalDeclaration(raw.portal, packageId)
158
159
  ?? c.fail("STANDALONE_PORTAL_INVALID", "portal 必須是 {} 或 { alias: <小寫英數與 -,1–64 字,不可為 DSM 保留路徑> }。");
159
160
  if (portal && osMinVer && !isDsmAtLeast(osMinVer, PORTAL_MIN_DSM)) {
@@ -185,8 +186,21 @@ function readManifest(raw, c) {
185
186
  ...(sharedFolder ? { sharedFolder } : {}),
186
187
  ...(entry ? { entry } : {}),
187
188
  ...(portal ? { portal } : {}),
189
+ ...(icon ? { icon } : {}),
188
190
  };
189
191
  }
192
+ function readIcon(value, c) {
193
+ const path = c.path(value, "icon");
194
+ if (!path)
195
+ return undefined;
196
+ try {
197
+ renderPackageIcons(path);
198
+ return path;
199
+ }
200
+ catch (error) {
201
+ return c.fail("STANDALONE_ICON_INVALID", `icon:${error.message}`);
202
+ }
203
+ }
190
204
  /**
191
205
  * The CGI enforces the entry mode only when the project's backend carries the
192
206
  * SDK gate and main wires it; `-X main.entry` on an older main would be
@@ -282,7 +296,10 @@ function readI18n(value, c) {
282
296
  if (!isRecord(value) || Object.keys(value).some((key) => !I18N_FIELDS.has(key))) {
283
297
  return invalid("只接受 messages(訊息目錄)、glossary(用語表)與 info(各語言 INFO 文字)。");
284
298
  }
285
- const info = readInfoText(value.info, c);
299
+ const info = readInfoTranslations(value.info, {
300
+ invalid: (why) => c.fail("STANDALONE_I18N_INVALID", `i18n.info ${why}`),
301
+ text: (text, field) => c.text(text, field),
302
+ });
286
303
  if (value.messages === undefined) {
287
304
  return value.glossary === undefined ? info && { info } : invalid("glossary 需要搭配 messages。");
288
305
  }
@@ -300,36 +317,6 @@ function readI18n(value, c) {
300
317
  drift.forEach((e) => c.fail(e.code, e.message));
301
318
  return info && drift.length === 0 ? { catalogs: loaded.catalogs, info } : undefined;
302
319
  }
303
- function readInfoText(value, c) {
304
- if (value === undefined) {
305
- return {};
306
- }
307
- const invalid = (why) => c.fail("STANDALONE_I18N_INVALID", `i18n.info ${why}`);
308
- if (!isRecord(value)) {
309
- return invalid("必須以語言為 key,例如 zh-TW: { displayname, description }。");
310
- }
311
- const info = {};
312
- for (const [locale, text] of Object.entries(value)) {
313
- if (!isLocale(locale) || locale === "en") {
314
- return invalid(`的「${locale}」不是支援的翻譯語言;英文寫在頂層 displayname / description。`);
315
- }
316
- if (!isRecord(text) || Object.keys(text).some((key) => !INFO_TEXT_FIELDS.has(key))) {
317
- return invalid(`.${locale} 只接受 displayname 與 description。`);
318
- }
319
- const entry = {};
320
- for (const field of INFO_TEXT_FIELDS) {
321
- if (text[field] !== undefined) {
322
- const checked = c.text(text[field], `i18n.info.${locale}.${field}`);
323
- if (!checked) {
324
- return undefined;
325
- }
326
- entry[field] = checked;
327
- }
328
- }
329
- info[locale] = entry;
330
- }
331
- return info;
332
- }
333
320
  /**
334
321
  * The nearest directory at or above `start` that holds dsm-package.yaml,
335
322
  * unless a Managed Profile project (synology-app.yaml) is found first.
@@ -1,2 +1,2 @@
1
1
  /** Shared host flow. CGI plans are never accepted as completed authorization. */
2
- export declare const AUTHORIZATION_RUNTIME = "function createAuthorizationAccess(options) {\n var busy = null;\n function fail(code) { var error = new Error(code); error.code = code; return error; }\n function status() { return options.call(\"authorization.status\").then(function (d) {\n if (!d || d.command !== \"authorization\" || d.schema_version !== 1 || [\"ready\",\"authorization_required\",\"not_admin\",\"session_expired\",\"unavailable\"].indexOf(d.state) < 0) throw fail(\"authorization_bad_response\");\n // upgrade_reapproval is a reason-only tombstone: it can only explain a pending request.\n if (d.reason === \"upgrade_reapproval\" && d.state !== \"authorization_required\") throw fail(\"authorization_bad_response\");\n return d;\n }); }\n function plan(method) { return options.call(method).then(function (p) {\n if (!p || p.command !== \"authorization\" || p.schema_version !== 1 || p.phase !== \"plan\" || !Array.isArray(p.operations) || p.operations.length === 0 || p.operations.length > options.tasks.length) throw fail(\"authorization_bad_response\");\n var seen = {};\n p.operations.forEach(function (op) { if (seen[op] || !options.tasks.some(function (t) { return t.op === op; })) throw fail(\"authorization_bad_response\"); seen[op] = true; });\n return p;\n }); }\n function exclusive(fn) { if (busy) return busy; busy = Promise.resolve().then(fn).then(function (s) { busy = null; return s; },function (e) { busy = null; throw e; });return busy; }\n function authorize(ops,run) { return ops.reduce(function (previous,op) { return previous.then(function () {\n return options.root.authorize(op).then(function (state) { if (state !== \"ready\") throw fail(\"root_cancelled\"); return run ? options.root.run(op) : null; });\n }); },Promise.resolve()); }\n return {canManage:function () {return !!options.user && options.user().admin === true;},status:status,request:function () { return exclusive(function () {\n return plan(\"authorization.request\").then(function (p) { return authorize(p.operations,p.run === true); }).then(status,function (e) { if (e.code === \"root_cancelled\") return status();throw e; });\n }); },revoke:function () { return exclusive(function () {\n return plan(\"authorization.revoke\").then(function (p) {\n if (p.withdraw === true && typeof options.withdraw !== \"function\") throw fail(\"authorization_bad_response\");\n var disabled = p.withdraw === true ? options.withdraw() : p.disable === true ? authorize([\"helper_disable\"],true) : Promise.resolve();\n return disabled.then(function () { return p.purge === true ? authorize([\"helper_purge\"],true) : null; }).then(function () { return p.operations.reduce(function (previous,op) {return previous.then(function () {return options.root.remove(op);});},Promise.resolve()); });\n }).then(status,function (e) { if (e.code === \"root_cancelled\") return status();throw e; });\n }); }};\n}";
2
+ export declare const AUTHORIZATION_RUNTIME = "function createAuthorizationAccess(options) {\n var busy = null;\n function fail(code) { var error = new Error(code); error.code = code; return error; }\n function status() { return options.call(\"authorization.status\").then(function (d) {\n if (!d || d.command !== \"authorization\" || d.schema_version !== 1 || [\"ready\",\"authorization_required\",\"not_admin\",\"session_expired\",\"unavailable\"].indexOf(d.state) < 0) throw fail(\"authorization_bad_response\");\n // upgrade_reapproval is a reason-only tombstone: it can only explain a pending request.\n if (d.reason === \"upgrade_reapproval\" && d.state !== \"authorization_required\") throw fail(\"authorization_bad_response\");\n return d;\n }); }\n function plan(method) { return options.call(method).then(function (p) {\n if (!p || p.command !== \"authorization\" || p.schema_version !== 1 || p.phase !== \"plan\" || !Array.isArray(p.operations) || p.operations.length === 0 || p.operations.length > options.tasks.length) throw fail(\"authorization_bad_response\");\n var seen = {};\n p.operations.forEach(function (op) { if (seen[op] || !options.tasks.some(function (t) { return t.op === op; })) throw fail(\"authorization_bad_response\"); seen[op] = true; });\n return p;\n }); }\n // The backend's grant decides: a task still waiting at the window's limit, or\n // one signalled after it wrote the grant, has still authorized the package.\n function settled(e) {\n if (!e || (e.code !== \"root_timeout\" && e.code !== \"root_task_failed\")) throw e;\n return status().then(function (d) { if (d.state !== \"ready\") throw e; });\n }\n function exclusive(fn) { if (busy) return busy; busy = Promise.resolve().then(fn).then(function (s) { busy = null; return s; },function (e) { busy = null; throw e; });return busy; }\n function authorize(ops,run) { return ops.reduce(function (previous,op) { return previous.then(function () {\n return options.root.authorize(op).then(function (state) { if (state !== \"ready\") throw fail(\"root_cancelled\"); return run ? options.root.run(op).catch(settled) : null; });\n }); },Promise.resolve()); }\n return {canManage:function () {return !!options.user && options.user().admin === true;},status:status,request:function () { return exclusive(function () {\n return plan(\"authorization.request\").then(function (p) { return authorize(p.operations,p.run === true); }).then(status,function (e) { if (e.code === \"root_cancelled\") return status();throw e; });\n }); },revoke:function () { return exclusive(function () {\n return plan(\"authorization.revoke\").then(function (p) {\n if (p.withdraw === true && typeof options.withdraw !== \"function\") throw fail(\"authorization_bad_response\");\n var disabled = p.withdraw === true ? options.withdraw() : p.disable === true ? authorize([\"helper_disable\"],true) : Promise.resolve();\n return disabled.then(function () { return p.purge === true ? authorize([\"helper_purge\"],true) : null; }).then(function () { return p.operations.reduce(function (previous,op) {return previous.then(function () {return options.root.remove(op);});},Promise.resolve()); });\n }).then(status,function (e) { if (e.code === \"root_cancelled\") return status();throw e; });\n }); }};\n}";
@@ -14,9 +14,15 @@ export const AUTHORIZATION_RUNTIME = `function createAuthorizationAccess(options
14
14
  p.operations.forEach(function (op) { if (seen[op] || !options.tasks.some(function (t) { return t.op === op; })) throw fail("authorization_bad_response"); seen[op] = true; });
15
15
  return p;
16
16
  }); }
17
+ // The backend's grant decides: a task still waiting at the window's limit, or
18
+ // one signalled after it wrote the grant, has still authorized the package.
19
+ function settled(e) {
20
+ if (!e || (e.code !== "root_timeout" && e.code !== "root_task_failed")) throw e;
21
+ return status().then(function (d) { if (d.state !== "ready") throw e; });
22
+ }
17
23
  function exclusive(fn) { if (busy) return busy; busy = Promise.resolve().then(fn).then(function (s) { busy = null; return s; },function (e) { busy = null; throw e; });return busy; }
18
24
  function authorize(ops,run) { return ops.reduce(function (previous,op) { return previous.then(function () {
19
- return options.root.authorize(op).then(function (state) { if (state !== "ready") throw fail("root_cancelled"); return run ? options.root.run(op) : null; });
25
+ return options.root.authorize(op).then(function (state) { if (state !== "ready") throw fail("root_cancelled"); return run ? options.root.run(op).catch(settled) : null; });
20
26
  }); },Promise.resolve()); }
21
27
  return {canManage:function () {return !!options.user && options.user().admin === true;},status:status,request:function () { return exclusive(function () {
22
28
  return plan("authorization.request").then(function (p) { return authorize(p.operations,p.run === true); }).then(status,function (e) { if (e.code === "root_cancelled") return status();throw e; });
@@ -37,6 +37,8 @@ export interface NativeLayout {
37
37
  backend: {
38
38
  kind: 'native';
39
39
  };
40
+ /** Project-relative package icon; the CLI then owns PACKAGE_ICON*.PNG, the payload keeps its own window icons. */
41
+ icon?: string;
40
42
  layout: {
41
43
  payload: string;
42
44
  metadata: string;
@@ -8,6 +8,7 @@ import { parse } from 'yaml';
8
8
  import { createReproducibleTar } from '../build-spk/reproducible-archive.js';
9
9
  import { runCommand } from './build.js';
10
10
  import { resolveMinDsm } from '../build-spk/dsm-version.js';
11
+ import { renderPackageIcons, writePackageCenterIcons } from '../build-spk/dsm-icons.js';
11
12
  export const LAYOUT_ARCHES = { x86_64: [2, 62], aarch64: [2, 183], armv8: [2, 183], armv7: [1, 40] };
12
13
  function fail(message) { throw Object.assign(new Error(`NATIVE_LAYOUT_INVALID: ${message}`), { code: 'NATIVE_LAYOUT_INVALID' }); }
13
14
  const digest = (bytes) => createHash('sha256').update(bytes).digest('hex');
@@ -61,6 +62,18 @@ function hook(value) {
61
62
  fail('invalid hook environment');
62
63
  return { command: h.command, cwd: pathText(h.cwd ?? '.'), env };
63
64
  }
65
+ function icon(root, value) {
66
+ const path = pathText(value);
67
+ if (existsSync(resolve(root, path)))
68
+ inside(root, path);
69
+ try {
70
+ renderPackageIcons(resolve(root, path));
71
+ }
72
+ catch (error) {
73
+ fail(`icon: ${error.message}`);
74
+ }
75
+ return path;
76
+ }
64
77
  export function isNativeLayout(root) {
65
78
  try {
66
79
  return (() => { const raw = parse(readFileSync(join(root, 'dsm-package.yaml'), 'utf8')); return raw?.layout !== undefined || raw?.backend?.kind === 'native'; })();
@@ -71,7 +84,7 @@ export function isNativeLayout(root) {
71
84
  }
72
85
  export function loadNativeLayout(root) {
73
86
  try {
74
- const m = object(parse(readFileSync(join(root, 'dsm-package.yaml'), 'utf8')), ['schema', 'backend', 'package', 'version', 'build', 'displayname', 'description', 'maintainer', 'metadata', 'os_min_ver', 'privilege', 'arches', 'layout', 'native']);
87
+ const m = object(parse(readFileSync(join(root, 'dsm-package.yaml'), 'utf8')), ['schema', 'backend', 'package', 'version', 'build', 'displayname', 'description', 'maintainer', 'metadata', 'os_min_ver', 'privilege', 'arches', 'layout', 'native', 'icon']);
75
88
  if (m.schema !== 2)
76
89
  fail('schema 2 required; migrate schema: 1 to schema: 2 and add backend: {kind: native}');
77
90
  const backend = object(m.backend, ['kind']);
@@ -90,6 +103,8 @@ export function loadNativeLayout(root) {
90
103
  text(m[k]);
91
104
  if (!Array.isArray(m.arches) || !m.arches.length || new Set(m.arches).size !== m.arches.length || m.arches.some((a) => !Object.hasOwn(LAYOUT_ARCHES, a)))
92
105
  fail('invalid architecture list');
106
+ if (m.icon !== undefined)
107
+ m.icon = icon(root, m.icon);
93
108
  const layout = object(m.layout, ['payload', 'metadata', 'binaries', 'anchors']);
94
109
  layout.payload = pathText(layout.payload);
95
110
  layout.metadata = pathText(layout.metadata);
@@ -291,6 +306,7 @@ export async function buildNativeLayout(options) {
291
306
  const arches = options.arches ?? m.arches;
292
307
  if (!arches.length || new Set(arches).size !== arches.length || arches.some(a => !m.arches.includes(a)))
293
308
  fail('architecture not declared');
309
+ const icons = m.icon === undefined ? undefined : renderPackageIcons(inside(m.root, m.icon));
294
310
  const run = options.run ?? runCommand;
295
311
  const version = `${m.version}-${String(m.build).padStart(4, '0')}`;
296
312
  const env = { SYNOPKGLAND_PACKAGE: m.package, SYNOPKGLAND_VERSION: m.version, SYNOPKGLAND_BUILD: String(m.build), SYNOPKGLAND_SPK_VERSION: version, SYNOPKGLAND_PROJECT_DIR: m.root };
@@ -313,6 +329,11 @@ export async function buildNativeLayout(options) {
313
329
  for (const b of binaries)
314
330
  checkLayoutBinary(payload, b, arch);
315
331
  copyTree(metadata, stage);
332
+ if (icons) {
333
+ if (['PACKAGE_ICON.PNG', 'PACKAGE_ICON_256.PNG'].some(name => existsSync(join(metadata, name))))
334
+ fail('icon and PACKAGE_ICON files in the layout metadata are mutually exclusive; keep one');
335
+ writePackageCenterIcons(stage, icons);
336
+ }
316
337
  const target = join(stage, 'payload');
317
338
  mkdirSync(target);
318
339
  const files = copyTree(payload, target);
@@ -17,10 +17,12 @@ export interface RootTask {
17
17
  * with exactly the embedded script. The one-use token goes into that single
18
18
  * create call and is never kept.
19
19
  * - run: re-reads the task and runs it only if it is still exactly ours, then
20
- * waits for its history entry and asks the backend for the output. Repair
20
+ * waits for its history entry and asks the backend for the output. A queued
21
+ * or running task keeps the wait going, up to RUN_LIMIT_MS (loaded NAS took
22
+ * 4.5 minutes), and `options.progress({op, state, elapsedMs})` hears each poll. Repair
21
23
  * additionally requires fresh PasswordConfirm and another exact task check.
22
24
  * - an older version of our task is "outdated" and never runs; authorizing
23
25
  * again replaces it. A same-named task that is not ours is a "conflict" and
24
26
  * is never run, replaced or deleted.
25
27
  */
26
- export declare const ROOT_RUNTIME = "function createRootAccess(options) {\n var API = \"SYNO.Core.TaskScheduler\";\n var POLL_MS = 500, POLLS = 60;\n var byOp = {}, running = {};\n var legacy = null, probe = null;\n options.tasks.forEach(function (task) { byOp[task.op] = task; });\n\n function fail(code) {\n var error = new Error(code);\n error.code = code;\n return error;\n }\n function begin(op) {\n if (!Object.prototype.hasOwnProperty.call(byOp, op)) return Promise.reject(fail(\"root_unknown_op\"));\n if (options.user().admin !== true) return Promise.reject(fail(\"admin_required\"));\n return Promise.resolve(byOp[op]);\n }\n function readOutput(task) {\n return task.output ? options.call(\"root.output\", { op: task.op }) : Promise.resolve(null);\n }\n function isRootTask(summary) {\n return summary.owner === \"root\" && summary.real_owner === \"root\" && summary.enable === false && summary.type === \"script\";\n }\n function listTasks() {\n if (legacy === false) return options.send(API, \"list\", 3, { start: 0, limit: 500 });\n if (legacy === true) return legacyList();\n if (!probe) {\n probe = options.send(API, \"list\", 3, { start: 0, limit: 500 }).then(function (data) {\n legacy = false;\n return data;\n }, function (error) {\n if (!error || (error.code !== \"dsm_103\" && error.code !== 103)) {\n probe = null;\n throw error;\n }\n legacy = true;\n return legacyList();\n });\n }\n return probe;\n }\n function legacyList() {\n return options.send(API, \"list\", 2, { offset: 0, limit: 500 }).then(function (data) {\n // Refuse a truncated snapshot: a same-named task could lie outside it.\n if (!data || !Array.isArray(data.tasks) || data.tasks.length > 500\n || !(typeof data.total === \"number\" && isFinite(data.total) && Math.floor(data.total) === data.total) || data.total !== data.tasks.length) throw fail(\"root_bad_response\");\n return data;\n });\n }\n function readTask(ref) {\n return options.send(API, \"get\", legacy ? 3 : 4, ref);\n }\n function createTask(params) {\n return options.send(legacy ? API : API + \".Root\", \"create\", legacy ? 3 : 4, params);\n }\n function deleteTask(ref) {\n return options.send(API, \"delete\", 2, { tasks: [ref] });\n }\n function runTask(ref) {\n return options.send(API, \"run\", 2, { tasks: [ref] });\n }\n function find(task) {\n return listTasks().then(function (data) {\n if (!data || !Array.isArray(data.tasks)) throw fail(\"root_bad_response\");\n var named = data.tasks.filter(function (summary) { return summary && summary.name === task.name; });\n if (named.length === 0) return { state: \"missing\" };\n if (named.length > 1 || !isRootTask(named[0])) return { state: \"conflict\" };\n var ref = { id: named[0].id, real_owner: named[0].real_owner };\n return readTask(ref).then(function (detail) {\n var script = detail && detail.extra && detail.extra.script;\n if (!detail || String(detail.id) !== String(ref.id) || detail.name !== task.name\n || detail.owner !== \"root\" || detail.real_owner !== ref.real_owner || detail.enable !== false || detail.type !== \"script\" || typeof script !== \"string\") {\n return { state: \"conflict\" };\n }\n if (script === task.script) return { state: \"ready\", ref: ref };\n if (script.indexOf(task.header) === 0) return { state: \"outdated\", ref: ref };\n return { state: \"conflict\" };\n });\n });\n }\n function definition(task, user) {\n var hours = [];\n for (var h = 1; h <= 23; h++) hours.push(h);\n return {\n name: task.name,\n real_owner: user,\n owner: \"root\",\n enable: false,\n type: \"script\",\n schedule: {\n date_type: 0, week_day: \"0,1,2,3,4,5,6\", repeat_date: 1001, monthly_week: [], hour: 0, minute: 0,\n repeat_hour: 0, repeat_min: 0, last_work_hour: 0,\n repeat_min_store_config: [1, 5, 10, 15, 20, 30], repeat_hour_store_config: hours\n },\n extra: { notify_enable: false, script: task.script, notify_mail: \"\", notify_if_error: false }\n };\n }\n function newest(history) {\n if (!Array.isArray(history)) throw fail(\"root_bad_response\");\n return history.reduce(function (latest, entry) {\n return entry && (!latest || Number(entry.timestamp) > Number(latest.timestamp)) ? entry : latest;\n }, null);\n }\n function history(ref) {\n return options.send(API, \"get_history_status_list\", 1, { id: ref.id }).then(newest);\n }\n function waitForRun(ref, before, polls) {\n return history(ref).then(function (latest) {\n var finished = latest && (!before || latest.timestamp !== before.timestamp)\n && latest.exit_type !== \"running\" && latest.stop_time;\n if (finished) {\n if (latest.exit_code !== 0) throw fail(\"root_task_failed\");\n return;\n }\n if (polls <= 1) throw fail(\"root_timeout\");\n return options.wait(POLL_MS).then(function () { return waitForRun(ref, before, polls - 1); });\n });\n }\n\n function validTime(value) {\n if (typeof value !== \"string\" || !/^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$/.test(value)) return false;\n var normalized = value.replace(\" \", \"T\") + \"Z\";\n var date = new Date(normalized);\n return !isNaN(date.getTime()) && date.toISOString().slice(0, 19) === normalized.slice(0, 19);\n }\n function bounded(call, deadline) {\n // A poll count alone cannot bound a stalled DSM callback. Discard late replies.\n return new Promise(function (resolve, reject) {\n var remaining = deadline - Date.now();\n if (remaining <= 0) { reject(fail(\"root_timeout\")); return; }\n var timer = setTimeout(function () { reject(fail(\"root_timeout\")); }, remaining);\n Promise.resolve().then(call).then(function (value) {\n clearTimeout(timer);\n if (Date.now() >= deadline) reject(fail(\"root_timeout\"));\n else resolve(value);\n }, function (error) { clearTimeout(timer); reject(error); });\n });\n }\n function legacyHistory(ref, deadline) {\n return bounded(function () { return options.send(API, \"view\", 1, { id: ref.id }); }, deadline).then(function (records) {\n // DSM 7.1 omits data entirely when a task has never run.\n if (records === undefined) records = [];\n if (!Array.isArray(records) || records.length > 500) throw fail(\"root_bad_response\");\n var seen = {};\n records.forEach(function (record) {\n if (!record || !validTime(record.start_time) || typeof record.stop_time !== \"string\"\n || (record.stop_time && (!validTime(record.stop_time) || record.stop_time < record.start_time))\n || typeof record.exit_type !== \"string\" || !(typeof record.exit_code === \"number\" && isFinite(record.exit_code) && Math.floor(record.exit_code) === record.exit_code)\n || seen[record.start_time]) throw fail(\"root_bad_response\");\n seen[record.start_time] = true;\n });\n return records;\n });\n }\n function waitForLegacyRun(ref, before, deadline, polls) {\n return legacyHistory(ref, deadline).then(function (records) {\n var previous = before.map(function (record) { return record.start_time; });\n var newestBefore = previous.slice().sort().pop();\n var fresh = records.filter(function (record) { return previous.indexOf(record.start_time) === -1; });\n if (fresh.length > 1) throw fail(\"root_bad_response\");\n var latest = fresh[0];\n if (latest && (!newestBefore || latest.start_time > newestBefore) && latest.stop_time) {\n if (latest.exit_code !== 0 || latest.exit_type !== \"normal\") throw fail(\"root_task_failed\");\n return;\n }\n if (polls <= 1 || Date.now() >= deadline) throw fail(\"root_timeout\");\n return options.wait(Math.min(POLL_MS, deadline - Date.now())).then(function () {\n return waitForLegacyRun(ref, before, deadline, polls - 1);\n });\n });\n }\n function execute(task, verified) {\n if (!legacy) {\n return history(verified.ref).then(function (before) {\n return runTask(verified.ref).then(function () { return waitForRun(verified.ref, before, POLLS); });\n });\n }\n var deadline = Date.now() + POLL_MS * POLLS;\n return legacyHistory(verified.ref, deadline).then(function (before) {\n if (before.some(function (record) { return !record.stop_time || record.exit_type === \"running\"; })) throw fail(\"root_not_ready\");\n // Mutating calls are sent exactly once, never retried with another profile.\n return bounded(function () { return runTask(verified.ref); }, deadline).then(function () {\n return waitForLegacyRun(verified.ref, before, deadline, POLLS);\n });\n }).then(function () {\n return bounded(function () { return find(task); }, deadline).then(function (after) {\n if (after.state !== \"ready\" || String(after.ref.id) !== String(verified.ref.id)) throw fail(\"root_verify_failed\");\n });\n });\n }\n function performRun(op) {\n return begin(op).then(function (task) {\n return find(task).then(function (found) {\n if (found.state !== \"ready\") throw fail(\"root_not_ready\");\n var consent = op === \"helper_repair\" ? options.confirmPassword() : Promise.resolve({ confirmed: true });\n return consent.then(function (confirmation) {\n if (!confirmation || confirmation.confirmed !== true) throw fail(\"root_cancelled\");\n return op === \"helper_repair\" ? begin(op).then(find) : found;\n }).then(function (verified) {\n if (verified.state !== \"ready\") throw fail(\"root_not_ready\");\n return execute(task, verified);\n });\n }).then(function () { return readOutput(task); });\n });\n }\n\n return {\n state: function (op) {\n return begin(op).then(find).then(function (found) { return found.state; });\n },\n authorize: function (op) {\n return begin(op).then(function (task) {\n return find(task).then(function (found) {\n if (found.state === \"ready\") return \"ready\";\n if (found.state === \"conflict\") throw fail(\"root_conflict\");\n return options.confirmPassword().then(function (confirmation) {\n if (!confirmation || confirmation.confirmed !== true) return \"cancelled\";\n if (!legacy && (typeof confirmation.token !== \"string\" || !confirmation.token)) throw fail(\"root_verify_failed\");\n var params = definition(task, options.user().name);\n if (!legacy) params.SynoConfirmPWToken = confirmation.token;\n // Re-read after the legacy client-side dialog before replacing a task.\n var checked = legacy ? begin(op).then(find) : Promise.resolve(found);\n return checked.then(function (current) {\n if (current.state === \"conflict\") throw fail(\"root_conflict\");\n if (current.state === \"ready\") return;\n var replaced = current.state === \"outdated\" ? deleteTask(current.ref) : Promise.resolve();\n return replaced.then(function () { return createTask(params); });\n }).then(function () {\n return find(task);\n }).then(function (after) {\n if (after.state !== \"ready\") throw fail(\"root_verify_failed\");\n return \"ready\";\n });\n });\n });\n });\n },\n run: function (op) {\n if (!Object.prototype.hasOwnProperty.call(byOp, op)) return Promise.reject(fail(\"root_unknown_op\"));\n if (options.user().admin !== true) return Promise.reject(fail(\"admin_required\"));\n if (running[op]) return running[op];\n var pending = performRun(op).then(function (value) {\n delete running[op];\n return value;\n }, function (error) { delete running[op]; throw error; });\n running[op] = pending;\n return pending;\n },\n remove: function (op) {\n return begin(op).then(function (task) {\n return find(task).then(function (found) {\n if (found.state === \"missing\") return \"missing\";\n if (found.state === \"conflict\") throw fail(\"root_conflict\");\n return deleteTask(found.ref).then(function () {\n return find(task);\n }).then(function (after) {\n if (after.state !== \"missing\") throw fail(\"root_verify_failed\");\n return \"missing\";\n });\n });\n });\n },\n output: function (op) {\n return begin(op).then(readOutput);\n }\n };\n}";
28
+ export declare const ROOT_RUNTIME: string;
@@ -1,3 +1,4 @@
1
+ import { versionUnsupported } from "../runtime/dsm-version.js";
1
2
  /**
2
3
  * `createRootAccess(options)` → `{ state, authorize, run, remove }`, plain
3
4
  * ES5 for DSM's desktop. Every DSM call goes through `options.send` (the
@@ -7,7 +8,9 @@
7
8
  * with exactly the embedded script. The one-use token goes into that single
8
9
  * create call and is never kept.
9
10
  * - run: re-reads the task and runs it only if it is still exactly ours, then
10
- * waits for its history entry and asks the backend for the output. Repair
11
+ * waits for its history entry and asks the backend for the output. A queued
12
+ * or running task keeps the wait going, up to RUN_LIMIT_MS (loaded NAS took
13
+ * 4.5 minutes), and `options.progress({op, state, elapsedMs})` hears each poll. Repair
11
14
  * additionally requires fresh PasswordConfirm and another exact task check.
12
15
  * - an older version of our task is "outdated" and never runs; authorizing
13
16
  * again replaces it. A same-named task that is not ours is a "conflict" and
@@ -15,11 +18,20 @@
15
18
  */
16
19
  export const ROOT_RUNTIME = `function createRootAccess(options) {
17
20
  var API = "SYNO.Core.TaskScheduler";
18
- var POLL_MS = 500, POLLS = 60;
21
+ var POLL_MS = 500, MAX_POLL_MS = 2000, RUN_LIMIT_MS = 10 * 60 * 1000;
19
22
  var byOp = {}, running = {};
20
23
  var legacy = null, probe = null;
24
+ var versionUnsupported = ${versionUnsupported.toString()};
21
25
  options.tasks.forEach(function (task) { byOp[task.op] = task; });
22
26
 
27
+ function now() { return options.now ? options.now() : Date.now(); }
28
+ function report(task, state, started) {
29
+ if (typeof options.progress !== "function") return;
30
+ try { options.progress({ op: task.op, state: state, elapsedMs: now() - started }); } catch (error) {}
31
+ }
32
+ function pause(interval, deadline) {
33
+ return options.wait(Math.max(0, Math.min(interval, deadline - now())));
34
+ }
23
35
  function fail(code) {
24
36
  var error = new Error(code);
25
37
  error.code = code;
@@ -44,7 +56,7 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
44
56
  legacy = false;
45
57
  return data;
46
58
  }, function (error) {
47
- if (!error || (error.code !== "dsm_103" && error.code !== 103)) {
59
+ if (!error || !versionUnsupported(error.code)) {
48
60
  probe = null;
49
61
  throw error;
50
62
  }
@@ -119,16 +131,18 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
119
131
  function history(ref) {
120
132
  return options.send(API, "get_history_status_list", 1, { id: ref.id }).then(newest);
121
133
  }
122
- function waitForRun(ref, before, polls) {
134
+ function waitForRun(task, ref, before, started, deadline, interval) {
123
135
  return history(ref).then(function (latest) {
124
- var finished = latest && (!before || latest.timestamp !== before.timestamp)
125
- && latest.exit_type !== "running" && latest.stop_time;
126
- if (finished) {
136
+ var fresh = latest && (!before || latest.timestamp !== before.timestamp);
137
+ if (fresh && latest.exit_type !== "running" && latest.stop_time) {
127
138
  if (latest.exit_code !== 0) throw fail("root_task_failed");
128
139
  return;
129
140
  }
130
- if (polls <= 1) throw fail("root_timeout");
131
- return options.wait(POLL_MS).then(function () { return waitForRun(ref, before, polls - 1); });
141
+ report(task, fresh ? "running" : "queued", started);
142
+ if (now() >= deadline) throw fail("root_timeout");
143
+ return pause(interval, deadline).then(function () {
144
+ return waitForRun(task, ref, before, started, deadline, Math.min(interval * 2, MAX_POLL_MS));
145
+ });
132
146
  });
133
147
  }
134
148
 
@@ -141,12 +155,12 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
141
155
  function bounded(call, deadline) {
142
156
  // A poll count alone cannot bound a stalled DSM callback. Discard late replies.
143
157
  return new Promise(function (resolve, reject) {
144
- var remaining = deadline - Date.now();
158
+ var remaining = deadline - now();
145
159
  if (remaining <= 0) { reject(fail("root_timeout")); return; }
146
160
  var timer = setTimeout(function () { reject(fail("root_timeout")); }, remaining);
147
161
  Promise.resolve().then(call).then(function (value) {
148
162
  clearTimeout(timer);
149
- if (Date.now() >= deadline) reject(fail("root_timeout"));
163
+ if (now() >= deadline) reject(fail("root_timeout"));
150
164
  else resolve(value);
151
165
  }, function (error) { clearTimeout(timer); reject(error); });
152
166
  });
@@ -167,7 +181,7 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
167
181
  return records;
168
182
  });
169
183
  }
170
- function waitForLegacyRun(ref, before, deadline, polls) {
184
+ function waitForLegacyRun(task, ref, before, started, deadline, interval) {
171
185
  return legacyHistory(ref, deadline).then(function (records) {
172
186
  var previous = before.map(function (record) { return record.start_time; });
173
187
  var newestBefore = previous.slice().sort().pop();
@@ -178,24 +192,25 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
178
192
  if (latest.exit_code !== 0 || latest.exit_type !== "normal") throw fail("root_task_failed");
179
193
  return;
180
194
  }
181
- if (polls <= 1 || Date.now() >= deadline) throw fail("root_timeout");
182
- return options.wait(Math.min(POLL_MS, deadline - Date.now())).then(function () {
183
- return waitForLegacyRun(ref, before, deadline, polls - 1);
195
+ report(task, latest ? "running" : "queued", started);
196
+ if (now() >= deadline) throw fail("root_timeout");
197
+ return pause(interval, deadline).then(function () {
198
+ return waitForLegacyRun(task, ref, before, started, deadline, Math.min(interval * 2, MAX_POLL_MS));
184
199
  });
185
200
  });
186
201
  }
187
202
  function execute(task, verified) {
203
+ var started = now(), deadline = started + RUN_LIMIT_MS;
188
204
  if (!legacy) {
189
205
  return history(verified.ref).then(function (before) {
190
- return runTask(verified.ref).then(function () { return waitForRun(verified.ref, before, POLLS); });
206
+ return runTask(verified.ref).then(function () { return waitForRun(task, verified.ref, before, started, deadline, POLL_MS); });
191
207
  });
192
208
  }
193
- var deadline = Date.now() + POLL_MS * POLLS;
194
209
  return legacyHistory(verified.ref, deadline).then(function (before) {
195
210
  if (before.some(function (record) { return !record.stop_time || record.exit_type === "running"; })) throw fail("root_not_ready");
196
211
  // Mutating calls are sent exactly once, never retried with another profile.
197
212
  return bounded(function () { return runTask(verified.ref); }, deadline).then(function () {
198
- return waitForLegacyRun(verified.ref, before, deadline, POLLS);
213
+ return waitForLegacyRun(task, verified.ref, before, started, deadline, POLL_MS);
199
214
  });
200
215
  }).then(function () {
201
216
  return bounded(function () { return find(task); }, deadline).then(function (after) {
@@ -2,4 +2,7 @@ export { executorTasks as rootTasks, type ExecutorTask as RootTask } from '../ru
2
2
  /** Exact readback is authoritative. PasswordConfirm tokens are reused immediately,
3
3
  * across both Root APIs, and never persisted (DSM validity is about 300 seconds). */
4
4
  export declare const ROOT_RUNTIME: string;
5
- export declare const ROOT_PANEL = "function mountRootPanel(host, access, i18n, tasks, admin) {\n if (!admin) return;\n var status = document.createElement(\"p\"), approve = document.createElement(\"button\"), retry = document.createElement(\"button\");\n approve.textContent = \"Approve background access\"; retry.textContent = \"Check status\";\n host.appendChild(status); host.appendChild(approve); host.appendChild(retry);\n function show(result) { status.textContent = result.state + (result.reason ? \": \" + result.reason : \"\"); approve.hidden = result.state !== \"needs-admin-approval\"; }\n function run(fn) { approve.disabled = retry.disabled = true; return fn().then(show, function(e) { show({state: \"blocked\", reason: e.message || e.code}); }).then(function() { approve.disabled = retry.disabled = false; }); }\n approve.onclick = function() { return run(function() { return access.authorize(); }); };\n retry.onclick = function() { return run(function() { return access.status(); }); };\n run(function() { return access.status().then(function(result) { return result.tasks && result.tasks.boot && result.tasks.control && result.tasks.boot.state === \"ready\" && result.tasks.control.state === \"ready\" ? access.control(\"start\") : result; }); });\n}";
5
+ export declare const ROOT_PANEL = "function mountRootPanel(host, access, i18n, tasks, admin) {\n if (!admin) return;\n var status = document.createElement(\"p\"), approve = document.createElement(\"button\"), retry = document.createElement(\"button\");\n approve.textContent = \"Approve background access\"; retry.textContent = \"Check status\";\n host.appendChild(status); host.appendChild(approve); host.appendChild(retry);\n function show(result) { status.textContent = result.state + (result.reason ? \": \" + result.reason : \"\"); approve.hidden = result.state !== \"needs-admin-approval\" && result.retry !== true; }\n function run(fn) { approve.disabled = retry.disabled = true; return fn().then(show, function(e) { show({state: \"blocked\", reason: e.message || e.code}); }).then(function() { approve.disabled = retry.disabled = false; }); }\n access.subscribe(show);\n approve.onclick = function() { return run(function() { return access.authorize(); }); };\n retry.onclick = function() { return run(function() { return access.status(); }); };\n run(function() { return access.status().then(function(result) { return result.tasks && result.tasks.boot && result.tasks.control && result.tasks.boot.state === \"ready\" && result.tasks.control.state === \"ready\" ? access.control(\"start\") : result; }); });\n}";
6
+ /** Administrator password confirmation in the window's own session: a supplied
7
+ * password, or a masked form when none is given. */
8
+ export declare const PASSWORD_CONFIRM: string;