@zaalipro/dsh-workflows 0.1.0-rc.3

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 (229) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +224 -0
  3. package/README.i18n.yaml +4 -0
  4. package/README.md +109 -0
  5. package/README.zh.md +109 -0
  6. package/cordis.patch.yml +10 -0
  7. package/docs/architecture.i18n.yaml +4 -0
  8. package/docs/architecture.md +177 -0
  9. package/docs/architecture.zh.md +177 -0
  10. package/docs/testing.i18n.yaml +4 -0
  11. package/docs/testing.md +125 -0
  12. package/docs/testing.zh.md +125 -0
  13. package/docs/user-guide.i18n.yaml +4 -0
  14. package/docs/user-guide.md +351 -0
  15. package/docs/user-guide.zh.md +351 -0
  16. package/lib/client-types/WorkflowMemberInspector.d.ts +28 -0
  17. package/lib/client-types/WorkflowMemberInspector.d.ts.map +1 -0
  18. package/lib/client-types/WorkflowMemberInspector.js +181 -0
  19. package/lib/client-types/WorkflowMemberInspector.js.map +1 -0
  20. package/lib/client-types/WorkflowRunPanel.d.ts +43 -0
  21. package/lib/client-types/WorkflowRunPanel.d.ts.map +1 -0
  22. package/lib/client-types/WorkflowRunPanel.js +130 -0
  23. package/lib/client-types/WorkflowRunPanel.js.map +1 -0
  24. package/lib/client-types/WorkflowsDashboard.d.ts +49 -0
  25. package/lib/client-types/WorkflowsDashboard.d.ts.map +1 -0
  26. package/lib/client-types/WorkflowsDashboard.js +942 -0
  27. package/lib/client-types/WorkflowsDashboard.js.map +1 -0
  28. package/lib/client-types/adapter.d.ts +60 -0
  29. package/lib/client-types/adapter.d.ts.map +1 -0
  30. package/lib/client-types/adapter.js +117 -0
  31. package/lib/client-types/adapter.js.map +1 -0
  32. package/lib/client-types/chat-renderer.d.ts +10 -0
  33. package/lib/client-types/chat-renderer.d.ts.map +1 -0
  34. package/lib/client-types/chat-renderer.js +11 -0
  35. package/lib/client-types/chat-renderer.js.map +1 -0
  36. package/lib/client-types/contract.d.ts +210 -0
  37. package/lib/client-types/contract.d.ts.map +1 -0
  38. package/lib/client-types/contract.js +37 -0
  39. package/lib/client-types/contract.js.map +1 -0
  40. package/lib/client-types/controller.d.ts +83 -0
  41. package/lib/client-types/controller.d.ts.map +1 -0
  42. package/lib/client-types/controller.js +705 -0
  43. package/lib/client-types/controller.js.map +1 -0
  44. package/lib/client-types/index.d.ts +19 -0
  45. package/lib/client-types/index.d.ts.map +1 -0
  46. package/lib/client-types/index.js +730 -0
  47. package/lib/client-types/index.js.map +1 -0
  48. package/lib/client-types/locales.d.ts +257 -0
  49. package/lib/client-types/locales.d.ts.map +1 -0
  50. package/lib/client-types/locales.js +270 -0
  51. package/lib/client-types/locales.js.map +1 -0
  52. package/lib/client-types/slot-components.d.ts +38 -0
  53. package/lib/client-types/slot-components.d.ts.map +1 -0
  54. package/lib/client-types/slot-components.js +22 -0
  55. package/lib/client-types/slot-components.js.map +1 -0
  56. package/lib/client-types/store.d.ts +69 -0
  57. package/lib/client-types/store.d.ts.map +1 -0
  58. package/lib/client-types/store.js +135 -0
  59. package/lib/client-types/store.js.map +1 -0
  60. package/lib/client-types/workflow-definition.d.ts +52 -0
  61. package/lib/client-types/workflow-definition.d.ts.map +1 -0
  62. package/lib/client-types/workflow-definition.js +135 -0
  63. package/lib/client-types/workflow-definition.js.map +1 -0
  64. package/lib/client.js +11064 -0
  65. package/lib/client.js.map +1 -0
  66. package/lib/compat-engine/index.js +1926 -0
  67. package/lib/compat-engine/index.js.map +1 -0
  68. package/lib/compat-engine/worker.cjs +1500 -0
  69. package/lib/compat-engine/worker.cjs.map +1 -0
  70. package/lib/typert.host.d.ts +3 -0
  71. package/lib/typert.host.js +1709 -0
  72. package/lib/typert.remote-client.d.ts +57 -0
  73. package/lib/typert.remote-client.js +1662 -0
  74. package/lib/types/commands/aliases.d.ts +4 -0
  75. package/lib/types/commands/aliases.d.ts.map +1 -0
  76. package/lib/types/commands/aliases.js +25 -0
  77. package/lib/types/commands/aliases.js.map +1 -0
  78. package/lib/types/commands/index.d.ts +66 -0
  79. package/lib/types/commands/index.d.ts.map +1 -0
  80. package/lib/types/commands/index.js +550 -0
  81. package/lib/types/commands/index.js.map +1 -0
  82. package/lib/types/commands/parser.d.ts +17 -0
  83. package/lib/types/commands/parser.d.ts.map +1 -0
  84. package/lib/types/commands/parser.js +45 -0
  85. package/lib/types/commands/parser.js.map +1 -0
  86. package/lib/types/config.d.ts +71 -0
  87. package/lib/types/config.d.ts.map +1 -0
  88. package/lib/types/config.js +109 -0
  89. package/lib/types/config.js.map +1 -0
  90. package/lib/types/index.d.ts +41 -0
  91. package/lib/types/index.d.ts.map +1 -0
  92. package/lib/types/index.js +483 -0
  93. package/lib/types/index.js.map +1 -0
  94. package/lib/types/invariant.d.ts +21 -0
  95. package/lib/types/invariant.d.ts.map +1 -0
  96. package/lib/types/invariant.js +85 -0
  97. package/lib/types/invariant.js.map +1 -0
  98. package/lib/types/registry/definition.d.ts +24 -0
  99. package/lib/types/registry/definition.d.ts.map +1 -0
  100. package/lib/types/registry/definition.js +189 -0
  101. package/lib/types/registry/definition.js.map +1 -0
  102. package/lib/types/registry/index.d.ts +100 -0
  103. package/lib/types/registry/index.d.ts.map +1 -0
  104. package/lib/types/registry/index.js +899 -0
  105. package/lib/types/registry/index.js.map +1 -0
  106. package/lib/types/registry/names.d.ts +19 -0
  107. package/lib/types/registry/names.d.ts.map +1 -0
  108. package/lib/types/registry/names.js +69 -0
  109. package/lib/types/registry/names.js.map +1 -0
  110. package/lib/types/registry/remote.d.ts +15 -0
  111. package/lib/types/registry/remote.d.ts.map +1 -0
  112. package/lib/types/registry/remote.js +128 -0
  113. package/lib/types/registry/remote.js.map +1 -0
  114. package/lib/types/registry/roots.d.ts +44 -0
  115. package/lib/types/registry/roots.d.ts.map +1 -0
  116. package/lib/types/registry/roots.js +118 -0
  117. package/lib/types/registry/roots.js.map +1 -0
  118. package/lib/types/registry/types.d.ts +62 -0
  119. package/lib/types/registry/types.d.ts.map +1 -0
  120. package/lib/types/registry/types.js +2 -0
  121. package/lib/types/registry/types.js.map +1 -0
  122. package/lib/types/registry/watchers.d.ts +70 -0
  123. package/lib/types/registry/watchers.d.ts.map +1 -0
  124. package/lib/types/registry/watchers.js +152 -0
  125. package/lib/types/registry/watchers.js.map +1 -0
  126. package/lib/types/remote-events.d.ts +42 -0
  127. package/lib/types/remote-events.d.ts.map +1 -0
  128. package/lib/types/remote-events.js +21 -0
  129. package/lib/types/remote-events.js.map +1 -0
  130. package/lib/types/run-recorder.d.ts +44 -0
  131. package/lib/types/run-recorder.d.ts.map +1 -0
  132. package/lib/types/run-recorder.js +356 -0
  133. package/lib/types/run-recorder.js.map +1 -0
  134. package/lib/types/supervisor/canned-validate.d.ts +25 -0
  135. package/lib/types/supervisor/canned-validate.d.ts.map +1 -0
  136. package/lib/types/supervisor/canned-validate.js +122 -0
  137. package/lib/types/supervisor/canned-validate.js.map +1 -0
  138. package/lib/types/supervisor/completion-notice.d.ts +49 -0
  139. package/lib/types/supervisor/completion-notice.d.ts.map +1 -0
  140. package/lib/types/supervisor/completion-notice.js +281 -0
  141. package/lib/types/supervisor/completion-notice.js.map +1 -0
  142. package/lib/types/supervisor/cursors.d.ts +68 -0
  143. package/lib/types/supervisor/cursors.d.ts.map +1 -0
  144. package/lib/types/supervisor/cursors.js +158 -0
  145. package/lib/types/supervisor/cursors.js.map +1 -0
  146. package/lib/types/supervisor/engine-compat.d.ts +48 -0
  147. package/lib/types/supervisor/engine-compat.d.ts.map +1 -0
  148. package/lib/types/supervisor/engine-compat.js +116 -0
  149. package/lib/types/supervisor/engine-compat.js.map +1 -0
  150. package/lib/types/supervisor/index.d.ts +183 -0
  151. package/lib/types/supervisor/index.d.ts.map +1 -0
  152. package/lib/types/supervisor/index.js +2040 -0
  153. package/lib/types/supervisor/index.js.map +1 -0
  154. package/lib/types/supervisor/parallel-compat.d.ts +36 -0
  155. package/lib/types/supervisor/parallel-compat.d.ts.map +1 -0
  156. package/lib/types/supervisor/parallel-compat.js +180 -0
  157. package/lib/types/supervisor/parallel-compat.js.map +1 -0
  158. package/lib/types/supervisor/remote.d.ts +25 -0
  159. package/lib/types/supervisor/remote.d.ts.map +1 -0
  160. package/lib/types/supervisor/remote.js +478 -0
  161. package/lib/types/supervisor/remote.js.map +1 -0
  162. package/lib/types/supervisor/storage/bounded-file.d.ts +41 -0
  163. package/lib/types/supervisor/storage/bounded-file.d.ts.map +1 -0
  164. package/lib/types/supervisor/storage/bounded-file.js +186 -0
  165. package/lib/types/supervisor/storage/bounded-file.js.map +1 -0
  166. package/lib/types/supervisor/storage/details-codec.d.ts +14 -0
  167. package/lib/types/supervisor/storage/details-codec.d.ts.map +1 -0
  168. package/lib/types/supervisor/storage/details-codec.js +359 -0
  169. package/lib/types/supervisor/storage/details-codec.js.map +1 -0
  170. package/lib/types/supervisor/storage/index.d.ts +26 -0
  171. package/lib/types/supervisor/storage/index.d.ts.map +1 -0
  172. package/lib/types/supervisor/storage/index.js +100 -0
  173. package/lib/types/supervisor/storage/index.js.map +1 -0
  174. package/lib/types/supervisor/storage/lease.d.ts +34 -0
  175. package/lib/types/supervisor/storage/lease.d.ts.map +1 -0
  176. package/lib/types/supervisor/storage/lease.js +289 -0
  177. package/lib/types/supervisor/storage/lease.js.map +1 -0
  178. package/lib/types/supervisor/storage/manifest-codec.d.ts +10 -0
  179. package/lib/types/supervisor/storage/manifest-codec.d.ts.map +1 -0
  180. package/lib/types/supervisor/storage/manifest-codec.js +335 -0
  181. package/lib/types/supervisor/storage/manifest-codec.js.map +1 -0
  182. package/lib/types/supervisor/storage/manifest-store.d.ts +99 -0
  183. package/lib/types/supervisor/storage/manifest-store.d.ts.map +1 -0
  184. package/lib/types/supervisor/storage/manifest-store.js +1606 -0
  185. package/lib/types/supervisor/storage/manifest-store.js.map +1 -0
  186. package/lib/types/supervisor/storage/manifest-types.d.ts +269 -0
  187. package/lib/types/supervisor/storage/manifest-types.d.ts.map +1 -0
  188. package/lib/types/supervisor/storage/manifest-types.js +2 -0
  189. package/lib/types/supervisor/storage/manifest-types.js.map +1 -0
  190. package/lib/types/supervisor/storage/private-root.d.ts +143 -0
  191. package/lib/types/supervisor/storage/private-root.d.ts.map +1 -0
  192. package/lib/types/supervisor/storage/private-root.js +860 -0
  193. package/lib/types/supervisor/storage/private-root.js.map +1 -0
  194. package/lib/types/supervisor/storage/recovery.d.ts +14 -0
  195. package/lib/types/supervisor/storage/recovery.d.ts.map +1 -0
  196. package/lib/types/supervisor/storage/recovery.js +126 -0
  197. package/lib/types/supervisor/storage/recovery.js.map +1 -0
  198. package/lib/types/supervisor/storage/run-files.d.ts +67 -0
  199. package/lib/types/supervisor/storage/run-files.d.ts.map +1 -0
  200. package/lib/types/supervisor/storage/run-files.js +346 -0
  201. package/lib/types/supervisor/storage/run-files.js.map +1 -0
  202. package/lib/types/supervisor/types.d.ts +295 -0
  203. package/lib/types/supervisor/types.d.ts.map +1 -0
  204. package/lib/types/supervisor/types.js +3 -0
  205. package/lib/types/supervisor/types.js.map +1 -0
  206. package/lib/types/supervisor/value-view.d.ts +19 -0
  207. package/lib/types/supervisor/value-view.d.ts.map +1 -0
  208. package/lib/types/supervisor/value-view.js +183 -0
  209. package/lib/types/supervisor/value-view.js.map +1 -0
  210. package/lib/types/tool/index.d.ts +86 -0
  211. package/lib/types/tool/index.d.ts.map +1 -0
  212. package/lib/types/tool/index.js +810 -0
  213. package/lib/types/tool/index.js.map +1 -0
  214. package/lib/types/tool/schema.d.ts +158 -0
  215. package/lib/types/tool/schema.d.ts.map +1 -0
  216. package/lib/types/tool/schema.js +137 -0
  217. package/lib/types/tool/schema.js.map +1 -0
  218. package/lib/types/types.d.ts +9 -0
  219. package/lib/types/types.d.ts.map +1 -0
  220. package/lib/types/types.js +3 -0
  221. package/lib/types/types.js.map +1 -0
  222. package/lib/types/user-questions.d.ts +9 -0
  223. package/lib/types/user-questions.d.ts.map +1 -0
  224. package/lib/types/user-questions.js +91 -0
  225. package/lib/types/user-questions.js.map +1 -0
  226. package/package.json +273 -0
  227. package/skills/create-workflow/SKILL.md +166 -0
  228. package/vendor/workflow-engine/LICENSE +21 -0
  229. package/vendor/workflow-engine/README.md +1 -0
@@ -0,0 +1,899 @@
1
+ /** Saved workflow-definition registry and its bounded Host-facing adapter. */
2
+ import { constants as fsConstants } from 'node:fs';
3
+ import { lstat as localLstat, mkdir as localMkdir, open as localOpen, readdir as localReaddir, unlink as localUnlink, link as localLink, chmod as localChmod, } from 'node:fs/promises';
4
+ import { filenameStem, parseWorkflowDefinition, serializeWorkflowDefinition, validateDefinitionEnvelope, } from './definition.js';
5
+ import { assertWorkflowDefinitionName, isWorkflowDefinitionName } from './names.js';
6
+ import { resolveWorkflowRoots, workflowPathApi } from './roots.js';
7
+ import { WorkflowDefinitionWatchers } from './watchers.js';
8
+ export * from './types.js';
9
+ export * from './names.js';
10
+ export * from './definition.js';
11
+ export * from './roots.js';
12
+ export * from './watchers.js';
13
+ /** Stable package error for malformed/unsafe registry observations. */
14
+ export class WorkflowRegistryError extends Error {
15
+ code;
16
+ constructor(message, code = 'WORKFLOW_DEFINITION_INVALID', options) {
17
+ super(message, options);
18
+ this.name = 'WorkflowRegistryError';
19
+ this.code = code;
20
+ }
21
+ }
22
+ const utf8Decoder = new TextDecoder('utf-8', { fatal: true });
23
+ function isHostContext(value) {
24
+ return typeof value === 'object' && value !== null && ('fs' in value || 'emit' in value || 'logger' in value);
25
+ }
26
+ function aborted(signal) {
27
+ signal?.throwIfAborted();
28
+ }
29
+ /** Stable code-unit ordering independent of host locale. */
30
+ function compareCodeUnits(left, right) {
31
+ if (left < right)
32
+ return -1;
33
+ if (left > right)
34
+ return 1;
35
+ /* c8 ignore start -- catalog names are unique after first-wins merge */
36
+ return 0;
37
+ /* c8 ignore stop */
38
+ }
39
+ function joinRoot(root, name) {
40
+ return workflowPathApi(root.path).join(root.path, name);
41
+ }
42
+ function localPathContains(base, child) {
43
+ const api = workflowPathApi(base);
44
+ const rel = api.relative(api.normalize(base), api.normalize(child));
45
+ return rel === '' || (rel !== '..' && !rel.startsWith(`..${api.sep}`) && !api.isAbsolute(rel));
46
+ }
47
+ function errorCode(error) {
48
+ return typeof error === 'object' && error !== null && 'code' in error
49
+ ? String(error.code)
50
+ : undefined;
51
+ }
52
+ function errorMessage(error) {
53
+ return error instanceof Error ? error.message : String(error);
54
+ }
55
+ function isAbortError(error) {
56
+ return error instanceof DOMException && error.name === 'AbortError'
57
+ || errorCode(error) === 'ABORT_ERR'
58
+ || errorCode(error) === 'FS_ABORTED';
59
+ }
60
+ function asRegistryError(path, error) {
61
+ if (error instanceof WorkflowRegistryError)
62
+ return error;
63
+ if (isAbortError(error))
64
+ throw error;
65
+ const code = errorCode(error);
66
+ const message = errorMessage(error);
67
+ const prefixed = message === path || message.startsWith(`${path}:`) ? message : `${path}: ${message}`;
68
+ if (code === 'FS_NOT_REGULAR_FILE') {
69
+ return new WorkflowRegistryError(`${path}: workflow definition must be a regular file; symbolic-link definitions are not allowed`, 'WORKFLOW_DEFINITION_INVALID', { cause: error });
70
+ }
71
+ if (code === 'FS_TOO_LARGE') {
72
+ const limit = /(?:exceeds the |limit )(\d+)/u.exec(message)?.[1];
73
+ return new WorkflowRegistryError(`${path}: definition exceeds the ${limit ?? 'configured'}-byte limit`, 'WORKFLOW_DEFINITION_INVALID', { cause: error });
74
+ }
75
+ return new WorkflowRegistryError(prefixed, code ?? 'WORKFLOW_DEFINITION_INVALID', { cause: error });
76
+ }
77
+ /** One unreadable `.workflow.json` must not hide the rest of the catalog. */
78
+ function skipInvalidDefinition(path, error, onInvalid) {
79
+ if (error instanceof WorkflowRegistryError
80
+ && error.code !== 'WORKFLOW_DEFINITION_INVALID'
81
+ && error.code !== 'WORKFLOW_DEFINITION_MISSING') {
82
+ throw error;
83
+ }
84
+ const wrapped = asRegistryError(path, error);
85
+ if (errorCode(error) === 'FS_NOT_REGULAR_FILE')
86
+ throw wrapped;
87
+ if (wrapped.code === 'WORKFLOW_DEFINITION_INVALID' || wrapped.code === 'WORKFLOW_DEFINITION_MISSING') {
88
+ onInvalid?.(wrapped);
89
+ return true;
90
+ }
91
+ throw wrapped;
92
+ }
93
+ async function localEntry(path) {
94
+ try {
95
+ const info = await localLstat(path);
96
+ return {
97
+ type: info.isSymbolicLink() ? 'symlink' : info.isFile() ? 'file' : info.isDirectory() ? 'directory' : 'other',
98
+ size: info.size,
99
+ nlink: info.nlink,
100
+ };
101
+ }
102
+ catch (error) {
103
+ if (errorCode(error) === 'ENOENT')
104
+ return undefined;
105
+ /* c8 ignore start -- unexpected local lstat faults stay loud */
106
+ throw error;
107
+ /* c8 ignore stop */
108
+ }
109
+ }
110
+ function localVersionFromStats(info) {
111
+ return {
112
+ dev: info.dev,
113
+ ino: info.ino,
114
+ size: info.size,
115
+ mtimeNs: info.mtimeNs,
116
+ ctimeNs: info.ctimeNs,
117
+ mode: info.mode,
118
+ nlink: info.nlink,
119
+ };
120
+ }
121
+ /** Capture the same final-entry identity/version fields used by the stock Host fs. */
122
+ async function localFileVersion(path) {
123
+ try {
124
+ const info = await localLstat(path, { bigint: true });
125
+ if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1n) {
126
+ throw new WorkflowRegistryError(`${path}: workflow definition must be a regular file; symbolic-link definitions are not allowed`, 'WORKFLOW_DEFINITION_INVALID');
127
+ }
128
+ return localVersionFromStats(info);
129
+ }
130
+ catch (error) {
131
+ if (errorCode(error) === 'ENOENT')
132
+ return undefined;
133
+ throw error;
134
+ }
135
+ }
136
+ function sameLocalFileVersion(left, right) {
137
+ return left.dev === right.dev
138
+ && left.ino === right.ino
139
+ && left.size === right.size
140
+ && left.mtimeNs === right.mtimeNs
141
+ && left.ctimeNs === right.ctimeNs
142
+ && left.mode === right.mode
143
+ && left.nlink === right.nlink;
144
+ }
145
+ /** Exchange changes ctime itself; these fields identify the entry across it. */
146
+ function sameLocalFileAfterExchange(left, right) {
147
+ return left.dev === right.dev
148
+ && left.ino === right.ino
149
+ && left.size === right.size
150
+ && left.mtimeNs === right.mtimeNs
151
+ && left.mode === right.mode
152
+ && left.nlink === right.nlink;
153
+ }
154
+ function sameBytes(left, right) {
155
+ if (left.byteLength !== right.byteLength)
156
+ return false;
157
+ for (let index = 0; index < left.byteLength; index += 1) {
158
+ if (left[index] !== right[index])
159
+ return false;
160
+ }
161
+ return true;
162
+ }
163
+ // Path-local serialization avoids needless same-process exchange/rollback while
164
+ // preserving optimistic semantics: every writer observes before joining this
165
+ // queue, then must still match that exact observation at its commit point.
166
+ const localPublicationTails = new Map();
167
+ async function withLocalPublicationLock(path, action) {
168
+ const previous = localPublicationTails.get(path) ?? Promise.resolve();
169
+ let release;
170
+ const gate = new Promise(resolve => { release = resolve; });
171
+ const tail = previous.then(() => gate);
172
+ localPublicationTails.set(path, tail);
173
+ await previous;
174
+ try {
175
+ return await action();
176
+ }
177
+ finally {
178
+ release();
179
+ if (localPublicationTails.get(path) === tail)
180
+ localPublicationTails.delete(path);
181
+ }
182
+ }
183
+ /** Make the destination directory entry durable before reporting success. */
184
+ async function fsyncLocalDirectory(path) {
185
+ /* c8 ignore start -- local durable publication requires POSIX directory fsync */
186
+ if (process.platform === 'win32' || (fsConstants.O_DIRECTORY ?? 0) === 0 || (fsConstants.O_NOFOLLOW ?? 0) === 0) {
187
+ throw new WorkflowRegistryError(`${path}: durable no-follow definition publication is unavailable on ${process.platform}`, 'WORKFLOW_REGISTRY_UNSUPPORTED');
188
+ }
189
+ /* c8 ignore stop */
190
+ const handle = await localOpen(path, fsConstants.O_RDONLY | fsConstants.O_DIRECTORY | fsConstants.O_NOFOLLOW);
191
+ try {
192
+ const info = await handle.stat();
193
+ if (!info.isDirectory())
194
+ throw new WorkflowRegistryError(`${path}: workflow root must be a directory`, 'WORKFLOW_ROOT_UNSAFE');
195
+ await handle.sync();
196
+ }
197
+ finally {
198
+ await handle.close().catch(() => undefined);
199
+ }
200
+ }
201
+ async function requireNativePublication(path) {
202
+ /* c8 ignore start -- native exchange is intentionally unsupported elsewhere */
203
+ if (process.platform !== 'linux' && process.platform !== 'darwin') {
204
+ throw new WorkflowRegistryError(`${path}: atomic definition exchange is unavailable on ${process.platform}`, 'WORKFLOW_REGISTRY_UNSUPPORTED');
205
+ }
206
+ try {
207
+ const native = await import('fs-native-extensions');
208
+ if (typeof native.swap !== 'function'
209
+ || typeof native.waitForLock !== 'function'
210
+ || typeof native.unlock !== 'function') {
211
+ throw new TypeError('atomic swap/lock exports are unavailable');
212
+ }
213
+ return native;
214
+ }
215
+ catch (error) {
216
+ throw new WorkflowRegistryError(`${path}: atomic definition exchange is unavailable`, 'WORKFLOW_REGISTRY_UNSUPPORTED', { cause: error });
217
+ }
218
+ /* c8 ignore stop */
219
+ }
220
+ /**
221
+ * Lock a stable, never-unlinked sibling entry. Locking the destination inode
222
+ * is insufficient because an exchange changes which inode future writers see.
223
+ */
224
+ async function acquireLocalPublicationLock(lockPath, native) {
225
+ let handle;
226
+ let locked = false;
227
+ try {
228
+ handle = await localOpen(lockPath, fsConstants.O_RDWR | fsConstants.O_CREAT | fsConstants.O_NOFOLLOW, 0o600);
229
+ const openedStats = await handle.stat({ bigint: true });
230
+ const opened = localVersionFromStats(openedStats);
231
+ if (!openedStats.isFile() || opened.nlink !== 1n) {
232
+ throw new WorkflowRegistryError(`${lockPath}: publication lock must be a regular single-link file`, 'WORKFLOW_ROOT_UNSAFE');
233
+ }
234
+ await native.waitForLock(handle.fd);
235
+ locked = true;
236
+ const held = localVersionFromStats(await handle.stat({ bigint: true }));
237
+ const lexical = await localFileVersion(lockPath);
238
+ if (lexical === undefined
239
+ || !sameLocalFileVersion(opened, held)
240
+ || !sameLocalFileVersion(held, lexical)) {
241
+ throw new WorkflowRegistryError(`${lockPath}: publication lock identity changed while held`, 'WORKFLOW_ROOT_UNSAFE');
242
+ }
243
+ const retained = handle;
244
+ const assertIdentity = async () => {
245
+ const descriptor = localVersionFromStats(await retained.stat({ bigint: true }));
246
+ const entry = await localFileVersion(lockPath);
247
+ if (entry === undefined
248
+ || !sameLocalFileVersion(held, descriptor)
249
+ || !sameLocalFileVersion(descriptor, entry)) {
250
+ throw new WorkflowRegistryError(`${lockPath}: publication lock identity changed while held`, 'WORKFLOW_ROOT_UNSAFE');
251
+ }
252
+ };
253
+ return {
254
+ handle: retained,
255
+ assertIdentity,
256
+ release: async () => {
257
+ try {
258
+ native.unlock(retained.fd);
259
+ }
260
+ catch { /* close also releases the advisory lock */ }
261
+ await retained.close().catch(() => undefined);
262
+ },
263
+ };
264
+ }
265
+ catch (error) {
266
+ if (locked && handle !== undefined) {
267
+ try {
268
+ native.unlock(handle.fd);
269
+ }
270
+ catch { /* close below releases it */ }
271
+ }
272
+ await handle?.close().catch(() => undefined);
273
+ if (errorCode(error) === 'ELOOP') {
274
+ throw new WorkflowRegistryError(`${lockPath}: symbolic-link publication lock is not allowed`, 'WORKFLOW_ROOT_UNSAFE', { cause: error });
275
+ }
276
+ throw error;
277
+ }
278
+ }
279
+ /** Reject symlink ancestors and lexical escapes before local fallback I/O. */
280
+ async function assertLocalRootSafe(root) {
281
+ /* c8 ignore start -- resolveWorkflowRoots never emits a lexically escaped root */
282
+ if (!localPathContains(root.basePath, root.path)) {
283
+ throw new WorkflowRegistryError(`${root.path}: workflow root escapes its ${root.scope} scope through a symbolic-link ancestor`, 'WORKFLOW_ROOT_UNSAFE');
284
+ }
285
+ /* c8 ignore stop */
286
+ const api = workflowPathApi(root.path);
287
+ const normalizedBase = api.normalize(root.basePath);
288
+ const normalizedRoot = api.normalize(root.path);
289
+ const rel = api.relative(normalizedBase, normalizedRoot);
290
+ const parts = rel === '' ? [] : rel.split(/[\\/]+/u).filter(Boolean);
291
+ let current = normalizedBase;
292
+ for (const part of parts) {
293
+ current = api.join(current, part);
294
+ const info = await localEntry(current);
295
+ if (info?.type === 'symlink') {
296
+ throw new WorkflowRegistryError(`${current}: symbolic-link workflow root ancestor is not allowed`, 'WORKFLOW_ROOT_UNSAFE');
297
+ }
298
+ if (info !== undefined && info.type !== 'directory') {
299
+ throw new WorkflowRegistryError(`${current}: workflow root ancestor must be a directory`, 'WORKFLOW_ROOT_UNSAFE');
300
+ }
301
+ if (info === undefined)
302
+ break;
303
+ }
304
+ }
305
+ async function readLocalNoFollow(path, maxBytes, signal) {
306
+ aborted(signal);
307
+ /* c8 ignore start -- local no-follow reads require POSIX O_NOFOLLOW */
308
+ if (process.platform === 'win32' || (fsConstants.O_NOFOLLOW ?? 0) === 0) {
309
+ throw new WorkflowRegistryError(`${path}: safe no-follow definition reads are unavailable on ${process.platform}`, 'WORKFLOW_REGISTRY_UNSUPPORTED');
310
+ }
311
+ /* c8 ignore stop */
312
+ let handle;
313
+ try {
314
+ handle = await localOpen(path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
315
+ const info = await handle.stat();
316
+ if (!info.isFile() || info.nlink !== 1) {
317
+ /* c8 ignore next -- discoverLocalRoot already rejected non-regular final entries */
318
+ throw new WorkflowRegistryError(`${path}: workflow definition must be a regular file; symbolic-link definitions are not allowed`, 'WORKFLOW_DEFINITION_INVALID');
319
+ }
320
+ if (info.size > maxBytes)
321
+ throw new WorkflowRegistryError(`${path}: definition exceeds the ${maxBytes}-byte limit`, 'WORKFLOW_DEFINITION_INVALID');
322
+ const chunks = [];
323
+ let total = 0;
324
+ while (true) {
325
+ aborted(signal);
326
+ const chunk = Buffer.alloc(Math.min(64 * 1024, maxBytes + 1 - total));
327
+ /* c8 ignore next -- the size check above rejects oversized files before the loop */
328
+ if (chunk.length === 0)
329
+ throw new WorkflowRegistryError(`${path}: definition exceeds the ${maxBytes}-byte limit`, 'WORKFLOW_DEFINITION_INVALID');
330
+ const { bytesRead } = await handle.read(chunk, 0, chunk.length, null);
331
+ if (bytesRead === 0)
332
+ break;
333
+ total += bytesRead;
334
+ chunks.push(chunk.subarray(0, bytesRead));
335
+ /* c8 ignore next -- handle.stat size already rejected files above the cap */
336
+ if (total > maxBytes)
337
+ throw new WorkflowRegistryError(`${path}: definition exceeds the ${maxBytes}-byte limit`, 'WORKFLOW_DEFINITION_INVALID');
338
+ }
339
+ return new Uint8Array(Buffer.concat(chunks));
340
+ }
341
+ catch (error) {
342
+ if (error instanceof WorkflowRegistryError)
343
+ throw error;
344
+ /* c8 ignore start -- raced unlink between lstat and open */
345
+ if (errorCode(error) === 'ENOENT')
346
+ throw new WorkflowRegistryError(`${path}: definition not found`, 'WORKFLOW_DEFINITION_MISSING', { cause: error });
347
+ throw error;
348
+ /* c8 ignore stop */
349
+ }
350
+ finally {
351
+ await handle?.close().catch(() => undefined);
352
+ }
353
+ }
354
+ async function discoverLocalRoot(root, maxDefinitions, maxBytes, signal, onInvalid) {
355
+ aborted(signal);
356
+ await assertLocalRootSafe(root);
357
+ const info = await localEntry(root.path);
358
+ if (info === undefined)
359
+ return [];
360
+ /* c8 ignore start -- assertLocalRootSafe already rejected symlink/file roots */
361
+ if (info.type === 'symlink')
362
+ throw new WorkflowRegistryError(`${root.path}: symbolic-link workflow roots are not allowed`, 'WORKFLOW_ROOT_UNSAFE');
363
+ if (info.type !== 'directory')
364
+ throw new WorkflowRegistryError(`${root.path}: workflow root must be a directory`, 'WORKFLOW_ROOT_UNSAFE');
365
+ /* c8 ignore stop */
366
+ const entries = (await localReaddir(root.path, { withFileTypes: true }))
367
+ .filter(entry => entry.name.endsWith('.workflow.json'))
368
+ .sort((left, right) => compareCodeUnits(left.name, right.name));
369
+ if (entries.length > maxDefinitions)
370
+ throw new WorkflowRegistryError(`${root.path}: found ${entries.length} workflow definitions; maximum is ${maxDefinitions}`, 'WORKFLOW_DEFINITION_LIMIT');
371
+ const definitions = [];
372
+ for (const entry of entries) {
373
+ aborted(signal);
374
+ const path = joinRoot(root, entry.name);
375
+ const name = filenameStem(entry.name);
376
+ if (!isWorkflowDefinitionName(name))
377
+ throw new WorkflowRegistryError(`${path}: filename stem "${name}" is not a valid workflow name`, 'WORKFLOW_DEFINITION_INVALID');
378
+ const finalInfo = await localEntry(path);
379
+ if (finalInfo?.type === 'symlink' || finalInfo?.type !== 'file' || finalInfo.nlink !== 1) {
380
+ throw new WorkflowRegistryError(`${path}: workflow definition must be a regular file; symbolic-link definitions are not allowed`, 'WORKFLOW_DEFINITION_INVALID');
381
+ }
382
+ try {
383
+ definitions.push(parseWorkflowDefinition(await readLocalNoFollow(path, maxBytes, signal), path, root.scope, name, maxBytes));
384
+ }
385
+ catch (error) {
386
+ if (skipInvalidDefinition(path, error, onInvalid))
387
+ continue;
388
+ }
389
+ }
390
+ return definitions;
391
+ }
392
+ async function discoverHostRoot(fs, root, maxDefinitions, maxBytes, signal, onInvalid) {
393
+ aborted(signal);
394
+ let rootInfo;
395
+ try {
396
+ rootInfo = await fs.lstat(root.path, {}, signal);
397
+ }
398
+ catch (error) {
399
+ throw asRegistryError(root.path, error);
400
+ }
401
+ if (rootInfo === undefined)
402
+ return [];
403
+ if (rootInfo.type === 'symlink')
404
+ throw new WorkflowRegistryError(`${root.path}: symbolic-link workflow roots are not allowed`, 'WORKFLOW_ROOT_UNSAFE');
405
+ if (rootInfo.type !== 'directory')
406
+ throw new WorkflowRegistryError(`${root.path}: workflow root must be a directory`, 'WORKFLOW_ROOT_UNSAFE');
407
+ if (typeof fs.openPrivateDirectory !== 'function') {
408
+ return discoverLocalRoot(root, maxDefinitions, maxBytes, signal);
409
+ }
410
+ let baseTarget;
411
+ let rootTarget;
412
+ try {
413
+ [baseTarget, rootTarget] = await Promise.all([
414
+ fs.resolve(root.basePath, { signal }),
415
+ fs.resolve(root.path, { signal }),
416
+ ]);
417
+ if (!fs.contains(baseTarget, rootTarget)) {
418
+ throw new WorkflowRegistryError(`${root.path}: workflow root escapes its ${root.scope} scope through a symbolic-link ancestor`, 'WORKFLOW_ROOT_UNSAFE');
419
+ }
420
+ }
421
+ catch (error) {
422
+ throw asRegistryError(root.path, error);
423
+ }
424
+ let directory;
425
+ try {
426
+ directory = await fs.openPrivateDirectory(root.path, { create: false }, signal);
427
+ await directory.assertIdentity(signal);
428
+ const descriptor = directory;
429
+ const descriptorList = descriptor.listEntries ?? descriptor.listDir;
430
+ if (descriptorList === undefined) {
431
+ throw new WorkflowRegistryError(`${root.path}: descriptor-rooted workflow definition listing is unavailable`, 'WORKFLOW_REGISTRY_UNSUPPORTED');
432
+ }
433
+ const entries = (await descriptorList.call(directory, signal))
434
+ .filter(entry => entry.name.endsWith('.workflow.json'))
435
+ .sort((left, right) => compareCodeUnits(left.name, right.name));
436
+ if (entries.length > maxDefinitions)
437
+ throw new WorkflowRegistryError(`${root.path}: found ${entries.length} workflow definitions; maximum is ${maxDefinitions}`, 'WORKFLOW_DEFINITION_LIMIT');
438
+ const definitions = [];
439
+ for (const entry of entries) {
440
+ aborted(signal);
441
+ if (entry.name.includes('/') || entry.name.includes('\\') || entry.name === '.' || entry.name === '..') {
442
+ throw new WorkflowRegistryError(`${joinRoot(root, entry.name)}: unsafe workflow filename`, 'WORKFLOW_DEFINITION_INVALID');
443
+ }
444
+ const path = joinRoot(root, entry.name);
445
+ const name = filenameStem(entry.name);
446
+ if (!isWorkflowDefinitionName(name))
447
+ throw new WorkflowRegistryError(`${path}: filename stem "${name}" is not a valid workflow name`, 'WORKFLOW_DEFINITION_INVALID');
448
+ if (entry.type !== 'file')
449
+ throw new WorkflowRegistryError(`${path}: workflow definition must be a regular file; symbolic-link definitions are not allowed`, 'WORKFLOW_DEFINITION_INVALID');
450
+ try {
451
+ const bytes = await directory.readBytes(entry.name, signal, maxBytes);
452
+ definitions.push(parseWorkflowDefinition(bytes, path, root.scope, name, maxBytes));
453
+ }
454
+ catch (error) {
455
+ if (skipInvalidDefinition(path, error, onInvalid))
456
+ continue;
457
+ }
458
+ }
459
+ await directory.assertIdentity(signal);
460
+ return definitions;
461
+ }
462
+ catch (error) {
463
+ throw asRegistryError(root.path, error);
464
+ }
465
+ finally {
466
+ await directory?.close().catch(() => undefined);
467
+ }
468
+ }
469
+ function summarize(definition) {
470
+ return {
471
+ name: definition.name,
472
+ description: definition.description,
473
+ ...(definition.whenToUse === undefined ? {} : { whenToUse: definition.whenToUse }),
474
+ ...(definition.phases === undefined ? {} : { phases: definition.phases }),
475
+ scope: definition.scope,
476
+ path: definition.path,
477
+ };
478
+ }
479
+ /** Saved-definition registry. Host capability I/O is authoritative when supplied. */
480
+ export class WorkflowRegistry {
481
+ static inject = ['fs'];
482
+ config;
483
+ ctx;
484
+ dshHome;
485
+ fs;
486
+ watchers;
487
+ revision = 0;
488
+ disposed = false;
489
+ watcherGeneration = 0;
490
+ constructor(ctxOrConfig, config) {
491
+ this.ctx = config === undefined && isHostContext(ctxOrConfig) ? ctxOrConfig : config === undefined ? undefined : ctxOrConfig;
492
+ const supplied = config ?? (isHostContext(ctxOrConfig) ? {} : ctxOrConfig) ?? {};
493
+ this.fs = this.ctx?.fs;
494
+ const homeValue = supplied.dshHome ?? process.env.DSH_HOME ?? (typeof process.env.HOME === 'string' ? `${process.env.HOME}/.dsh` : '/.dsh');
495
+ this.dshHome = workflowPathApi(homeValue).normalize(homeValue);
496
+ this.config = {
497
+ enabled: supplied.enabled ?? true,
498
+ definitionWatch: supplied.definitionWatch ?? true,
499
+ definitionMaxBytes: supplied.definitionMaxBytes ?? 1_048_576,
500
+ maxDefinitionsPerRoot: supplied.maxDefinitionsPerRoot ?? 256,
501
+ watchMaxProjects: supplied.watchMaxProjects ?? 128,
502
+ watchStabilityThresholdMs: supplied.watchStabilityThresholdMs ?? 200,
503
+ watchPollIntervalMs: supplied.watchPollIntervalMs ?? 100,
504
+ ...supplied,
505
+ };
506
+ if (!Number.isSafeInteger(this.config.definitionMaxBytes) || this.config.definitionMaxBytes < 1)
507
+ throw new TypeError('definitionMaxBytes must be a positive safe integer');
508
+ if (!Number.isSafeInteger(this.config.maxDefinitionsPerRoot) || this.config.maxDefinitionsPerRoot < 1)
509
+ throw new TypeError('maxDefinitionsPerRoot must be a positive safe integer');
510
+ if (this.config.definitionWatch && this.config.enabled && this.ctx?.emit !== undefined) {
511
+ this.watchers = new WorkflowDefinitionWatchers(() => {
512
+ if (this.disposed)
513
+ return;
514
+ this.watcherGeneration = this.watchers === undefined ? this.watcherGeneration + 1 : this.watchers.generation;
515
+ this.revision += 1;
516
+ this.ctx?.emit?.('workflows/change');
517
+ }, {
518
+ maxProjects: this.config.watchMaxProjects,
519
+ usePolling: this.config.watchUsePolling,
520
+ stabilityThresholdMs: this.config.watchStabilityThresholdMs,
521
+ pollIntervalMs: this.config.watchPollIntervalMs,
522
+ logger: this.ctx?.logger,
523
+ watchFactory: this.config.watchFactory,
524
+ scheduler: this.config.watchScheduler,
525
+ });
526
+ }
527
+ }
528
+ async roots(options = {}) {
529
+ return resolveWorkflowRoots({
530
+ fileSystem: this.fs,
531
+ cwd: options.cwd,
532
+ dshHome: this.dshHome,
533
+ bundledDefinitionsDir: this.config.bundledDefinitionsDir,
534
+ signal: options.signal,
535
+ });
536
+ }
537
+ async ensureWatchers(roots) {
538
+ if (this.watchers === undefined || this.disposed)
539
+ return;
540
+ const project = roots.find(root => root.scope === 'project')?.projectRoot
541
+ ?? roots.find(root => root.scope === 'project')?.basePath;
542
+ /* c8 ignore start -- resolveWorkflowRoots always emits a project root */
543
+ if (project === undefined)
544
+ return;
545
+ /* c8 ignore stop */
546
+ await this.watchers.observeProject(project, roots);
547
+ this.watcherGeneration = this.watchers.generation;
548
+ }
549
+ async discoverRoot(root, signal) {
550
+ const onInvalid = error => {
551
+ this.ctx?.logger?.warn?.(error.message);
552
+ };
553
+ return this.fs === undefined
554
+ ? discoverLocalRoot(root, this.config.maxDefinitionsPerRoot, this.config.definitionMaxBytes, signal, onInvalid)
555
+ : discoverHostRoot(this.fs, root, this.config.maxDefinitionsPerRoot, this.config.definitionMaxBytes, signal, onInvalid);
556
+ }
557
+ requireLookupCwd(options, action) {
558
+ if (typeof options.cwd !== 'string' || options.cwd.length === 0) {
559
+ throw new WorkflowRegistryError(action === 'save'
560
+ ? 'workflow definition save requires a session cwd'
561
+ : 'workflow definition listing requires a session cwd', 'WORKFLOW_ROOT_UNSAFE');
562
+ }
563
+ return options.cwd;
564
+ }
565
+ /** List all winning definitions, sorted by UTF-16 code units. */
566
+ async list(options = {}) {
567
+ if (!this.config.enabled)
568
+ return [];
569
+ this.requireLookupCwd(options, 'listing');
570
+ aborted(options.signal);
571
+ const roots = await this.roots(options);
572
+ await this.ensureWatchers(roots);
573
+ const byName = new Map();
574
+ for (const root of roots) {
575
+ for (const definition of await this.discoverRoot(root, options.signal)) {
576
+ if (!byName.has(definition.name))
577
+ byName.set(definition.name, definition);
578
+ }
579
+ }
580
+ aborted(options.signal);
581
+ return [...byName.values()]
582
+ .sort((left, right) => compareCodeUnits(left.name, right.name))
583
+ .map(summarize);
584
+ }
585
+ /** Return a bounded catalog snapshot; complete is false if a watcher raced it. */
586
+ async snapshot(options = {}) {
587
+ if (!this.config.enabled)
588
+ return { definitions: [], complete: true };
589
+ const revision = this.revision;
590
+ const generation = this.watcherGeneration;
591
+ const definitions = await this.list(options);
592
+ return {
593
+ definitions,
594
+ complete: revision === this.revision && generation === this.watcherGeneration,
595
+ revision,
596
+ };
597
+ }
598
+ /** Resolve one full winning definition (script is never exposed by list). */
599
+ async get(name, options = {}) {
600
+ if (!this.config.enabled)
601
+ return undefined;
602
+ this.requireLookupCwd(options, 'listing');
603
+ assertWorkflowDefinitionName(name, 'workflow lookup');
604
+ aborted(options.signal);
605
+ const roots = await this.roots(options);
606
+ await this.ensureWatchers(roots);
607
+ let winner;
608
+ for (const root of roots) {
609
+ const definitions = await this.discoverRoot(root, options.signal);
610
+ if (winner === undefined)
611
+ winner = definitions.find(definition => definition.name === name);
612
+ }
613
+ return winner;
614
+ }
615
+ /**
616
+ * Save a canonical definition through the Host descriptor capability. The
617
+ * guarded local path is also the stock RC2 compatibility seam when its Host
618
+ * filesystem does not expose retained private-directory descriptors.
619
+ */
620
+ async save(envelope, options) {
621
+ if (!this.config.enabled)
622
+ throw new WorkflowRegistryError('workflow registry is disabled', 'WORKFLOW_REGISTRY_DISABLED');
623
+ if (options.scope !== 'project' && options.scope !== 'user') {
624
+ throw new WorkflowRegistryError('workflow definitions can only be saved to project or user scope', 'WORKFLOW_SAVE_SCOPE_INVALID');
625
+ }
626
+ if (options.scope === 'project')
627
+ this.requireLookupCwd(options, 'save');
628
+ const clean = validateDefinitionEnvelope(envelope);
629
+ const bytes = serializeWorkflowDefinition(clean);
630
+ if (bytes.byteLength > this.config.definitionMaxBytes) {
631
+ throw new WorkflowRegistryError(`definition exceeds the ${this.config.definitionMaxBytes}-byte limit`, 'WORKFLOW_DEFINITION_INVALID');
632
+ }
633
+ const root = options.scope === 'user'
634
+ ? {
635
+ scope: 'user',
636
+ path: workflowPathApi(this.dshHome).join(this.dshHome, 'workflows'),
637
+ basePath: this.dshHome,
638
+ }
639
+ : (await this.roots(options)).find(candidate => candidate.scope === options.scope);
640
+ if (root === undefined)
641
+ throw new WorkflowRegistryError(`workflow ${options.scope} root is unavailable`, 'WORKFLOW_ROOT_UNSAFE');
642
+ return this.fs === undefined
643
+ ? this.saveLocal(clean, bytes, root, options.signal)
644
+ : this.saveHost(clean, bytes, root, options.signal);
645
+ }
646
+ async saveHost(envelope, bytes, root, signal) {
647
+ const fs = this.fs;
648
+ if (typeof fs.openPrivateDirectory !== 'function') {
649
+ return this.saveLocal(envelope, bytes, root, signal);
650
+ }
651
+ // Pin/check the allowed base before any directory creation.
652
+ let baseTarget;
653
+ let rootTarget;
654
+ try {
655
+ [baseTarget, rootTarget] = await Promise.all([
656
+ fs.resolve(root.basePath, { signal }),
657
+ fs.resolve(root.path, { signal }),
658
+ ]);
659
+ if (!fs.contains(baseTarget, rootTarget)) {
660
+ throw new WorkflowRegistryError(`${root.path}: workflow root escapes its ${root.scope} scope through a symbolic-link ancestor`, 'WORKFLOW_ROOT_UNSAFE');
661
+ }
662
+ }
663
+ catch (error) {
664
+ throw asRegistryError(root.path, error);
665
+ }
666
+ aborted(signal);
667
+ let directory;
668
+ try {
669
+ directory = await fs.openPrivateDirectory(root.path, { create: true }, signal);
670
+ await directory.assertIdentity(signal);
671
+ const filename = `${envelope.meta.name}.workflow.json`;
672
+ const path = joinRoot(root, filename);
673
+ // Observe the final lexical entry without resolving/following it. The
674
+ // retained directory performs the guarded publication; resolving this
675
+ // path to a target would reintroduce a final-link substitution race.
676
+ const current = await fs.lstat(path, {}, signal);
677
+ let expected;
678
+ if (current === undefined) {
679
+ expected = { kind: 'createIfAbsent' };
680
+ }
681
+ else {
682
+ if (current.type !== 'file')
683
+ throw new WorkflowRegistryError(`${path}: workflow definition must be a regular file; symbolic-link definitions are not allowed`, 'WORKFLOW_DEFINITION_INVALID');
684
+ if (current.version === undefined)
685
+ throw new WorkflowRegistryError(`${path}: Host filesystem did not return a final-entry version`, 'WORKFLOW_REGISTRY_UNSUPPORTED');
686
+ expected = { kind: 'replaceIfVersion', version: current.version };
687
+ }
688
+ await directory.writeText(filename, utf8Decoder.decode(bytes), expected, signal);
689
+ await directory.assertIdentity(signal);
690
+ const published = await directory.readBytes(filename, signal, this.config.definitionMaxBytes);
691
+ const definition = parseWorkflowDefinition(published, path, root.scope, envelope.meta.name, this.config.definitionMaxBytes);
692
+ this.emitChange();
693
+ return definition;
694
+ }
695
+ catch (error) {
696
+ throw asRegistryError(root.path, error);
697
+ }
698
+ finally {
699
+ await directory?.close().catch(() => undefined);
700
+ }
701
+ }
702
+ async saveLocal(envelope, bytes, root, signal) {
703
+ aborted(signal);
704
+ /* c8 ignore start -- the guarded local algorithm deliberately fails before mutation on Windows */
705
+ if (process.platform === 'win32' || (fsConstants.O_NOFOLLOW ?? 0) === 0 || (fsConstants.O_DIRECTORY ?? 0) === 0) {
706
+ throw new WorkflowRegistryError(`${root.path}: durable no-follow definition publication is unavailable on ${process.platform}`, 'WORKFLOW_REGISTRY_UNSUPPORTED');
707
+ }
708
+ /* c8 ignore stop */
709
+ // Resolve the native CAS primitive before creating a root or staging file.
710
+ const nativePublication = await requireNativePublication(root.path);
711
+ await assertLocalRootSafe(root);
712
+ await localMkdir(root.path, { recursive: true, mode: 0o700 });
713
+ await localChmod(root.path, 0o700).catch(() => undefined);
714
+ const filename = `${envelope.meta.name}.workflow.json`;
715
+ const path = joinRoot(root, filename);
716
+ const lockPath = joinRoot(root, `.${filename}.lock`);
717
+ const current = await localFileVersion(path);
718
+ const observedBytes = current === undefined
719
+ ? undefined
720
+ : await readLocalNoFollow(path, this.config.definitionMaxBytes, signal);
721
+ if (current !== undefined) {
722
+ const afterRead = await localFileVersion(path);
723
+ if (afterRead === undefined || !sameLocalFileVersion(current, afterRead)) {
724
+ throw new WorkflowRegistryError(`${path}: target changed during observation`, 'WORKFLOW_STALE_VERSION');
725
+ }
726
+ }
727
+ const temp = joinRoot(root, `.${filename}.${randomHex(12)}.tmp`);
728
+ let tempHandle;
729
+ let preserveTemp = false;
730
+ try {
731
+ tempHandle = await localOpen(temp, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL, 0o600);
732
+ await tempHandle.writeFile(bytes);
733
+ aborted(signal);
734
+ await tempHandle.sync();
735
+ await tempHandle.close();
736
+ tempHandle = undefined;
737
+ aborted(signal);
738
+ return await withLocalPublicationLock(path, async () => {
739
+ let cleanOwnedTemp = true;
740
+ let publicationLock;
741
+ try {
742
+ publicationLock = await acquireLocalPublicationLock(lockPath, nativePublication);
743
+ // Waiting for a cooperating publisher is not a commit. Honor a late
744
+ // abort now; after the first mutation recovery must run to completion.
745
+ aborted(signal);
746
+ if (current === undefined) {
747
+ await publicationLock.assertIdentity();
748
+ try {
749
+ await localLink(temp, path);
750
+ }
751
+ catch (error) {
752
+ if (errorCode(error) === 'EEXIST') {
753
+ throw new WorkflowRegistryError(`${path}: create publication raced another entry`, 'WORKFLOW_STALE_VERSION', { cause: error });
754
+ }
755
+ throw error;
756
+ }
757
+ // Drop the staging directory entry before the guarded reader checks
758
+ // link count; the published file itself must have exactly one link.
759
+ await localUnlink(temp);
760
+ }
761
+ else {
762
+ let before;
763
+ try {
764
+ before = await localFileVersion(path);
765
+ }
766
+ catch (error) {
767
+ if (error instanceof WorkflowRegistryError && error.code === 'WORKFLOW_DEFINITION_INVALID') {
768
+ throw new WorkflowRegistryError(`${path}: target changed before publication`, 'WORKFLOW_STALE_VERSION', { cause: error });
769
+ }
770
+ throw error;
771
+ }
772
+ if (before === undefined || !sameLocalFileVersion(current, before)) {
773
+ throw new WorkflowRegistryError(`${path}: target changed before publication`, 'WORKFLOW_STALE_VERSION');
774
+ }
775
+ const proposed = await localFileVersion(temp);
776
+ /* c8 ignore next -- the retained staging handle just created this entry */
777
+ if (proposed === undefined)
778
+ throw new WorkflowRegistryError(`${temp}: staging entry disappeared`, 'WORKFLOW_STALE_VERSION');
779
+ await publicationLock.assertIdentity();
780
+ await nativePublication.swap(temp, path);
781
+ cleanOwnedTemp = false;
782
+ let displaced;
783
+ try {
784
+ displaced = await localFileVersion(temp);
785
+ }
786
+ catch {
787
+ // A non-file substitution at the displaced path is equally stale.
788
+ }
789
+ let displacedBytes;
790
+ if (displaced !== undefined) {
791
+ try {
792
+ displacedBytes = await readLocalNoFollow(temp, this.config.definitionMaxBytes);
793
+ }
794
+ catch {
795
+ // Treat an unreadable displaced entry as stale and roll it back.
796
+ }
797
+ }
798
+ if (displaced === undefined
799
+ || displacedBytes === undefined
800
+ || observedBytes === undefined
801
+ || !sameLocalFileAfterExchange(current, displaced)
802
+ || !sameBytes(observedBytes, displacedBytes)) {
803
+ // Roll back only while the destination is still exactly our staged
804
+ // inode. A third writer wins without being overwritten by cleanup.
805
+ let destination;
806
+ try {
807
+ destination = await localFileVersion(path);
808
+ }
809
+ catch {
810
+ // Unsafe substitution: do not exchange through it.
811
+ }
812
+ let destinationBytes;
813
+ if (destination !== undefined) {
814
+ try {
815
+ destinationBytes = await readLocalNoFollow(path, this.config.definitionMaxBytes);
816
+ }
817
+ catch {
818
+ // Unsafe/read-raced destination: do not exchange through it.
819
+ }
820
+ }
821
+ if (destination === undefined
822
+ || destinationBytes === undefined
823
+ || !sameLocalFileAfterExchange(proposed, destination)
824
+ || !sameBytes(bytes, destinationBytes)) {
825
+ preserveTemp = true;
826
+ throw new WorkflowRegistryError(`${path}: target changed during publication; displaced entry retained at ${temp}`, 'WORKFLOW_STALE_VERSION');
827
+ }
828
+ try {
829
+ await nativePublication.swap(temp, path);
830
+ cleanOwnedTemp = true;
831
+ await fsyncLocalDirectory(root.path);
832
+ }
833
+ catch (error) {
834
+ preserveTemp = true;
835
+ throw new WorkflowRegistryError(`${path}: stale publication rollback failed; displaced entry retained at ${temp}`, 'WORKFLOW_STALE_VERSION', { cause: error });
836
+ }
837
+ throw new WorkflowRegistryError(`${path}: target changed before publication`, 'WORKFLOW_STALE_VERSION');
838
+ }
839
+ try {
840
+ await localUnlink(temp);
841
+ }
842
+ catch (error) {
843
+ preserveTemp = true;
844
+ await fsyncLocalDirectory(root.path).catch(() => undefined);
845
+ throw new WorkflowRegistryError(`${path}: publication cleanup failed; displaced entry retained at ${temp}`, 'WORKFLOW_STALE_VERSION', { cause: error });
846
+ }
847
+ cleanOwnedTemp = true;
848
+ }
849
+ await fsyncLocalDirectory(root.path);
850
+ const published = await readLocalNoFollow(path, this.config.definitionMaxBytes);
851
+ if (!sameBytes(published, bytes)) {
852
+ throw new WorkflowRegistryError(`${path}: target changed after publication`, 'WORKFLOW_STALE_VERSION');
853
+ }
854
+ const definition = parseWorkflowDefinition(published, path, root.scope, envelope.meta.name, this.config.definitionMaxBytes);
855
+ this.emitChange();
856
+ return definition;
857
+ }
858
+ finally {
859
+ // When exchange could not be safely rolled back, temp contains the
860
+ // displaced entry and is deliberately not treated as owned staging.
861
+ if (cleanOwnedTemp)
862
+ await localUnlink(temp).catch(() => undefined);
863
+ await publicationLock?.release();
864
+ }
865
+ });
866
+ }
867
+ finally {
868
+ await tempHandle?.close().catch(() => undefined);
869
+ if (!preserveTemp)
870
+ await localUnlink(temp).catch(() => undefined);
871
+ }
872
+ }
873
+ emitChange() {
874
+ if (this.disposed)
875
+ return;
876
+ this.revision += 1;
877
+ this.ctx?.emit?.('workflows/change');
878
+ }
879
+ /** Await watcher teardown; no late callback can publish a change hint. */
880
+ async dispose() {
881
+ if (this.disposed)
882
+ return;
883
+ this.disposed = true;
884
+ await this.watchers?.dispose();
885
+ }
886
+ }
887
+ function randomHex(bytes) {
888
+ // Avoid importing crypto in browser-adjacent builds; cryptographic uniqueness
889
+ // is not an authority for registry temp names (wx is the race guard).
890
+ const values = new Uint8Array(bytes);
891
+ if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function')
892
+ crypto.getRandomValues(values);
893
+ /* c8 ignore next -- Node always exposes crypto.getRandomValues */
894
+ else
895
+ for (let index = 0; index < values.length; index += 1)
896
+ values[index] = Math.floor(Math.random() * 256);
897
+ return [...values].map(value => value.toString(16).padStart(2, '0')).join('');
898
+ }
899
+ //# sourceMappingURL=index.js.map