cloving 1.1.0 → 1.2.0

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 (213) hide show
  1. package/README.md +121 -21
  2. package/assets/watch-fonts/fira-code-OFL.md +93 -0
  3. package/assets/watch-fonts/fira-code-latin-400-normal.woff2 +0 -0
  4. package/assets/watch-fonts/fira-code-latin-500-normal.woff2 +0 -0
  5. package/assets/watch-fonts/heebo-OFL.md +93 -0
  6. package/assets/watch-fonts/heebo-latin-500-normal.woff2 +0 -0
  7. package/assets/watch-fonts/heebo-latin-700-normal.woff2 +0 -0
  8. package/assets/watch-fonts/heebo-latin-800-normal.woff2 +0 -0
  9. package/assets/watch-fonts/manifest.json +112 -0
  10. package/assets/watch-fonts/space-grotesk-OFL.md +93 -0
  11. package/assets/watch-fonts/space-grotesk-latin-400-normal.woff2 +0 -0
  12. package/assets/watch-fonts/space-grotesk-latin-500-normal.woff2 +0 -0
  13. package/assets/watch-fonts/space-grotesk-latin-600-normal.woff2 +0 -0
  14. package/assets/watch-fonts/space-grotesk-latin-700-normal.woff2 +0 -0
  15. package/dist/src/authority/commands.d.ts +1 -0
  16. package/dist/src/authority/commands.js +25 -4
  17. package/dist/src/authority/dispatch-decision.d.ts +1 -0
  18. package/dist/src/authority/dispatch-decision.js +7 -4
  19. package/dist/src/authority/events.js +2 -2
  20. package/dist/src/authority/handoff-chain.d.ts +24 -0
  21. package/dist/src/authority/handoff-chain.js +46 -0
  22. package/dist/src/authority/reconciliation.d.ts +1 -0
  23. package/dist/src/authority/reconciliation.js +18 -11
  24. package/dist/src/authority/transitions.d.ts +1 -1
  25. package/dist/src/authority/transitions.js +3 -0
  26. package/dist/src/broker/actor/authority-queries.d.ts +4 -0
  27. package/dist/src/broker/actor/authority-queries.js +51 -2
  28. package/dist/src/broker/actor/delivery-intents.d.ts +1 -0
  29. package/dist/src/broker/actor/effect-delivery.js +9 -0
  30. package/dist/src/broker/authority-admission.js +13 -5
  31. package/dist/src/chats/analysis-batches.d.ts +18 -0
  32. package/dist/src/chats/analysis-batches.js +15 -1
  33. package/dist/src/chats/analysis-checkpoint.d.ts +29 -0
  34. package/dist/src/chats/analysis-checkpoint.js +31 -0
  35. package/dist/src/chats/analysis-input.d.ts +7 -1
  36. package/dist/src/chats/analysis-input.js +20 -6
  37. package/dist/src/chats/analysis-merge.d.ts +16 -0
  38. package/dist/src/chats/analysis-merge.js +134 -0
  39. package/dist/src/chats/analysis-prompt.d.ts +8 -0
  40. package/dist/src/chats/analysis-prompt.js +27 -1
  41. package/dist/src/chats/analysis-proposal.d.ts +2 -0
  42. package/dist/src/chats/analysis-proposal.js +9 -0
  43. package/dist/src/chats/analysis-shape.d.ts +1 -0
  44. package/dist/src/chats/analysis-shape.js +11 -4
  45. package/dist/src/chats/reference-mapping.d.ts +16 -0
  46. package/dist/src/chats/reference-mapping.js +153 -0
  47. package/dist/src/chats/workflow-generation-projection.d.ts +8 -0
  48. package/dist/src/chats/workflow-generation-projection.js +75 -0
  49. package/dist/src/chats/workflow-generation-source.d.ts +25 -0
  50. package/dist/src/chats/workflow-generation-source.js +124 -0
  51. package/dist/src/chats/workflow-generation-text.d.ts +23 -0
  52. package/dist/src/chats/workflow-generation-text.js +74 -0
  53. package/dist/src/chats/workflow-generation.d.ts +69 -0
  54. package/dist/src/chats/workflow-generation.js +142 -0
  55. package/dist/src/cli/render.d.ts +1 -0
  56. package/dist/src/cli/render.js +60 -13
  57. package/dist/src/cli/usage.js +5 -2
  58. package/dist/src/cli/watch-board-view.d.ts +12 -1
  59. package/dist/src/cli/watch-board-view.js +57 -11
  60. package/dist/src/cli/watch-board.d.ts +7 -1
  61. package/dist/src/cli/watch-board.js +42 -10
  62. package/dist/src/cli/watch-client.d.ts +1 -1
  63. package/dist/src/cli/watch-client.js +7 -3
  64. package/dist/src/cli/watch-model.d.ts +10 -4
  65. package/dist/src/cli/watch-model.js +35 -58
  66. package/dist/src/cli/watch-page.js +21 -4
  67. package/dist/src/cli/watch-phases.js +3 -3
  68. package/dist/src/cli/watch-pr-command.d.ts +5 -2
  69. package/dist/src/cli/watch-pr-command.js +41 -4
  70. package/dist/src/cli/watch-pr-identity.d.ts +14 -2
  71. package/dist/src/cli/watch-pr-identity.js +25 -0
  72. package/dist/src/cli/watch-pr-observer.d.ts +1 -0
  73. package/dist/src/cli/watch-pr-observer.js +48 -6
  74. package/dist/src/cli/watch-pull-request.d.ts +10 -0
  75. package/dist/src/cli/watch-pull-request.js +130 -0
  76. package/dist/src/cli/watch-reads.d.ts +7 -1
  77. package/dist/src/cli/watch-reads.js +42 -20
  78. package/dist/src/cli/watch-render.d.ts +14 -1
  79. package/dist/src/cli/watch-render.js +2 -1
  80. package/dist/src/cli/watch-server.js +33 -2
  81. package/dist/src/cli/watch-turn.js +1 -1
  82. package/dist/src/cli/watch-view.d.ts +7 -0
  83. package/dist/src/cli/watch-view.js +51 -3
  84. package/dist/src/daemon/analysis-worker.d.ts +0 -2
  85. package/dist/src/daemon/analysis-worker.js +71 -19
  86. package/dist/src/daemon/chats-batch-binding.d.ts +23 -0
  87. package/dist/src/daemon/chats-batch-binding.js +99 -0
  88. package/dist/src/daemon/chats-capture.d.ts +18 -0
  89. package/dist/src/daemon/chats-capture.js +194 -0
  90. package/dist/src/daemon/chats-checkpoints.d.ts +41 -0
  91. package/dist/src/daemon/chats-checkpoints.js +186 -0
  92. package/dist/src/daemon/chats-files.d.ts +29 -2
  93. package/dist/src/daemon/chats-files.js +142 -3
  94. package/dist/src/daemon/chats-follow-up.d.ts +38 -0
  95. package/dist/src/daemon/chats-follow-up.js +180 -0
  96. package/dist/src/daemon/chats-question.d.ts +15 -0
  97. package/dist/src/daemon/chats-question.js +93 -0
  98. package/dist/src/daemon/chats-worker.d.ts +1 -0
  99. package/dist/src/daemon/chats-worker.js +52 -51
  100. package/dist/src/daemon/claude-turn.d.ts +2 -1
  101. package/dist/src/daemon/claude-turn.js +1 -1
  102. package/dist/src/daemon/claude-worker.d.ts +0 -2
  103. package/dist/src/daemon/claude-worker.js +6 -11
  104. package/dist/src/daemon/daemon.d.ts +5 -1
  105. package/dist/src/daemon/daemon.js +38 -10
  106. package/dist/src/daemon/delivery-router.js +6 -0
  107. package/dist/src/daemon/github-calls.d.ts +4 -0
  108. package/dist/src/daemon/github-calls.js +16 -7
  109. package/dist/src/daemon/github-ci.d.ts +6 -0
  110. package/dist/src/daemon/github-ci.js +170 -0
  111. package/dist/src/daemon/github-merge-call.d.ts +14 -0
  112. package/dist/src/daemon/github-merge-call.js +73 -0
  113. package/dist/src/daemon/github-merge.d.ts +3 -5
  114. package/dist/src/daemon/github-merge.js +52 -95
  115. package/dist/src/daemon/github-methods.d.ts +28 -0
  116. package/dist/src/daemon/github-methods.js +80 -16
  117. package/dist/src/daemon/github-operation.d.ts +2 -0
  118. package/dist/src/daemon/github-retry.d.ts +6 -0
  119. package/dist/src/daemon/github-retry.js +49 -0
  120. package/dist/src/daemon/github-ticket-status.js +27 -1
  121. package/dist/src/daemon/github-worker.js +2 -1
  122. package/dist/src/daemon/handoff-worker.d.ts +10 -0
  123. package/dist/src/daemon/handoff-worker.js +232 -0
  124. package/dist/src/daemon/issue-queue.d.ts +1 -0
  125. package/dist/src/daemon/issue-queue.js +94 -42
  126. package/dist/src/daemon/notify-journal.d.ts +18 -0
  127. package/dist/src/daemon/notify-journal.js +108 -0
  128. package/dist/src/daemon/notify-message.js +2 -1
  129. package/dist/src/daemon/notify.js +176 -37
  130. package/dist/src/daemon/retrying-turn.d.ts +3 -0
  131. package/dist/src/daemon/retrying-turn.js +55 -9
  132. package/dist/src/daemon/ticket-dependency-waits.d.ts +18 -0
  133. package/dist/src/daemon/ticket-dependency-waits.js +60 -0
  134. package/dist/src/daemon/ticket-registration.d.ts +8 -4
  135. package/dist/src/daemon/ticket-registration.js +76 -22
  136. package/dist/src/daemon/ticket-source.d.ts +4 -1
  137. package/dist/src/daemon/ticket-source.js +8 -2
  138. package/dist/src/decision-visibility.d.ts +2 -0
  139. package/dist/src/decision-visibility.js +10 -0
  140. package/dist/src/execution-pin.d.ts +4 -0
  141. package/dist/src/execution-pin.js +23 -2
  142. package/dist/src/host/start-delivery.d.ts +2 -1
  143. package/dist/src/host/start-delivery.js +1 -1
  144. package/dist/src/host/step-bridge-host.d.ts +1 -0
  145. package/dist/src/host/step-bridge-host.js +16 -0
  146. package/dist/src/host/world-reconciliation.d.ts +1 -0
  147. package/dist/src/host/world-reconciliation.js +3 -0
  148. package/dist/src/host/world-runs.js +5 -0
  149. package/dist/src/issue-identity.d.ts +6 -0
  150. package/dist/src/issue-identity.js +23 -0
  151. package/dist/src/service/queue-runs.d.ts +3 -0
  152. package/dist/src/service/queue-runs.js +39 -0
  153. package/dist/src/service/registry.d.ts +8 -2
  154. package/dist/src/service/registry.js +20 -8
  155. package/dist/src/service/replies.d.ts +2 -0
  156. package/dist/src/service/run-handlers.js +44 -12
  157. package/dist/src/service/service.d.ts +3 -0
  158. package/dist/src/service/service.js +3 -1
  159. package/dist/src/service/submission-handler.d.ts +1 -0
  160. package/dist/src/service/submission-handler.js +92 -13
  161. package/dist/src/service/worker-totals.js +25 -1
  162. package/dist/src/step-bridge.d.ts +9 -0
  163. package/dist/src/steps/handoff.d.ts +2 -0
  164. package/dist/src/steps/handoff.js +13 -0
  165. package/dist/src/ticket-dependencies.d.ts +1 -0
  166. package/dist/src/ticket-dependencies.js +15 -0
  167. package/dist/src/ticket-status.js +7 -4
  168. package/dist/src/token-prices.d.ts +2 -0
  169. package/dist/src/token-prices.js +41 -9
  170. package/dist/src/worker-usage.d.ts +9 -0
  171. package/dist/src/worker-usage.js +1 -1
  172. package/dist/src/workflow-admission.d.ts +1 -0
  173. package/dist/src/workflow-candidate.js +1 -0
  174. package/dist/src/workflow-contracts.d.ts +2 -0
  175. package/dist/src/workflow-contracts.js +8 -3
  176. package/dist/src/workflow-declaration.d.ts +11 -0
  177. package/dist/src/workflow-declaration.js +71 -15
  178. package/dist/src/workflow-discovery.d.ts +18 -0
  179. package/dist/src/workflow-discovery.js +33 -0
  180. package/dist/src/workflow-refusals.d.ts +6 -0
  181. package/dist/src/workflow-refusals.js +39 -0
  182. package/dist/src/workflows.d.ts +2 -1
  183. package/dist/src/workflows.js +1 -0
  184. package/dist/tools/admit-workflow.d.ts +13 -0
  185. package/dist/tools/admit-workflow.js +94 -0
  186. package/dist/tools/self-host-project.d.ts +17 -0
  187. package/dist/tools/self-host-project.js +177 -0
  188. package/dist/tools/self-host.js +27 -81
  189. package/dist/tools/workflow-bundle-dependencies.d.ts +5 -0
  190. package/dist/tools/workflow-bundle-dependencies.js +93 -0
  191. package/dist/tools/workflow-bundles.d.ts +20 -0
  192. package/dist/tools/workflow-bundles.js +173 -0
  193. package/dist/tools/workflow-candidate-counts.d.ts +8 -0
  194. package/dist/tools/workflow-candidate-counts.js +251 -0
  195. package/dist/tools/workflow-candidate-program.d.ts +11 -0
  196. package/dist/tools/workflow-candidate-program.js +78 -0
  197. package/dist/tools/workflow-candidate-source.d.ts +6 -0
  198. package/dist/tools/workflow-candidate-source.js +107 -0
  199. package/dist/tools/workflow-candidate-syntax.d.ts +15 -0
  200. package/dist/tools/workflow-candidate-syntax.js +219 -0
  201. package/dist/tools/workflow-folder-capture.d.ts +17 -0
  202. package/dist/tools/workflow-folder-capture.js +216 -0
  203. package/dist/tools/workflow-folders.d.ts +33 -0
  204. package/dist/tools/workflow-folders.js +150 -0
  205. package/dist/tools/workflow-source-effects.d.ts +10 -0
  206. package/dist/tools/workflow-source-effects.js +213 -0
  207. package/dist/workflows/analyze-chats.d.ts +6 -1
  208. package/dist/workflows/analyze-chats.declaration.json +14 -1
  209. package/dist/workflows/analyze-chats.js +188 -11
  210. package/dist/workflows/develop-prd.js +71 -3
  211. package/dist/workflows/develop-ticket.js +5 -0
  212. package/dist/workflows/ticket-prompts.js +12 -2
  213. package/package.json +21 -3
@@ -1,13 +1,11 @@
1
1
  import { isProfilesFile } from "../src/turn-profile.js";
2
- import { parseTokenPrices } from "../src/token-prices.js";
3
- import { execFileSync, spawn } from "node:child_process";
4
- import { copyFile, mkdir, readFile, rm, symlink, writeFile, } from "node:fs/promises";
2
+ import { parseFallbackModels, parseTokenPrices, } from "../src/token-prices.js";
3
+ import { execFileSync } from "node:child_process";
4
+ import { readFile, realpath, writeFile } from "node:fs/promises";
5
+ import { homedir } from "node:os";
5
6
  import { dirname, extname, join, resolve } from "node:path";
6
- import packageMetadata from "#package" with { type: "json" };
7
- import { findPackageJSON } from "node:module";
8
7
  import { pathToFileURL } from "node:url";
9
- import { workflowDeclarations, workflowFiles, workflowManifests, } from "./self-host-workflows.js";
10
- import { isWorkflowManifest } from "./workflow-manifest.js";
8
+ import { preparedProject, } from "./self-host-project.js";
11
9
  import { selfUpdateMilliseconds } from "./self-update-host.js";
12
10
  import { hostedPrograms } from "./self-host-processes.js";
13
11
  const product = resolve(import.meta.dirname, "..");
@@ -32,7 +30,7 @@ if (runtimeArgument === undefined)
32
30
  throw new Error("The start program needs --runtime. Use: cloving start");
33
31
  const runtime = resolve(runtimeArgument);
34
32
  const directory = dirname(runtime);
35
- const repository = topLevel(resolve(flag("--repository") ?? process.cwd()));
33
+ const repository = await realpath(topLevel(resolve(flag("--repository") ?? process.cwd())));
36
34
  const updateMilliseconds = await selfUpdateMilliseconds({
37
35
  seconds: process.env["CLOVING_SELF_UPDATE_SECONDS"],
38
36
  repository,
@@ -84,6 +82,16 @@ async function profilesFile() {
84
82
  }
85
83
  throw new Error("The daemon did not start: invalid-profiles");
86
84
  }
85
+ function queueLanes() {
86
+ const value = process.env["CLOVING_QUEUE_LANES"];
87
+ if (value === undefined)
88
+ return 1;
89
+ if (!/^[1-4]$/.test(value))
90
+ throw new Error("The daemon did not start: invalid-queue-lanes");
91
+ if (process.env["CLOVING_QUEUE_SECONDS"] === undefined)
92
+ throw new Error("The daemon did not start: queue-lanes-needs-queue");
93
+ return Number(value);
94
+ }
87
95
  function queueInterval(commands) {
88
96
  const value = process.env["CLOVING_QUEUE_SECONDS"];
89
97
  if (value === undefined)
@@ -109,61 +117,6 @@ function listed(name) {
109
117
  throw new Error(`${name} is not a JSON list of texts.`);
110
118
  return list.map(String);
111
119
  }
112
- function record(value) {
113
- return typeof value === "object" && value !== null ? { ...value } : {};
114
- }
115
- function ended(argumentsList, environment) {
116
- return new Promise((settle, reject) => {
117
- const child = spawn(process.execPath, argumentsList, {
118
- cwd: project,
119
- env: environment,
120
- stdio: "inherit",
121
- });
122
- for (const signal of ["SIGINT", "SIGTERM"])
123
- process.on(signal, () => child.kill(signal));
124
- child.once("error", reject);
125
- child.once("close", (status) => {
126
- settle(status ?? 1);
127
- });
128
- });
129
- }
130
- async function linkedPackages() {
131
- const modules = join(project, "node_modules");
132
- await mkdir(join(modules, "cloving"), { recursive: true });
133
- for (const name of Object.keys(packageMetadata.dependencies)) {
134
- const location = findPackageJSON(name, join(product, "package.json"));
135
- if (location === undefined)
136
- throw new Error(`missing-runtime-dependency: ${name}`);
137
- await mkdir(dirname(join(modules, name)), { recursive: true });
138
- await symlink(dirname(location), join(modules, name));
139
- }
140
- const manifest = record(packageMetadata);
141
- const exported = Object.keys(record(manifest["exports"])).filter((subpath) => subpath.startsWith("./workflows"));
142
- await symlink(join(product, "src"), join(modules, "cloving", "src"));
143
- await writeFile(join(modules, "cloving", "package.json"), JSON.stringify({
144
- name: manifest["name"],
145
- version: manifest["version"],
146
- type: "module",
147
- exports: Object.fromEntries(exported.map((subpath) => [
148
- subpath,
149
- `./src/${subpath.slice("./workflows/".length) || "workflows"}${extname(import.meta.filename)}`,
150
- ])),
151
- }));
152
- }
153
- async function registered() {
154
- const compiled = JSON.parse(await readFile(join(project, "artifacts/manifest.json"), "utf8"));
155
- if (!isWorkflowManifest(compiled))
156
- throw new Error("The compiled workflow manifest is invalid.");
157
- const manifests = workflowManifests(compiled);
158
- const declarations = await workflowDeclarations(join(product, "workflows"), manifests);
159
- const paths = new Map();
160
- for (const [functionName, manifest] of Object.entries(workflowManifests(compiled, declarations))) {
161
- const path = join(project, `${manifest.name}.workflow.json`);
162
- await writeFile(path, JSON.stringify(manifest));
163
- paths.set(functionName, path);
164
- }
165
- return { manifests: Object.fromEntries(paths), declarations };
166
- }
167
120
  function source(file) {
168
121
  return JSON.stringify(pathToFileURL(join(product, file.replace(/\.ts$/, extname(import.meta.filename)))).href);
169
122
  }
@@ -171,7 +124,6 @@ async function launchers(commands, workflows) {
171
124
  const settings = {
172
125
  ...commands,
173
126
  directory,
174
- repository,
175
127
  concurrency,
176
128
  runtimeDirectory: runtime,
177
129
  ...workflows,
@@ -210,23 +162,8 @@ function guide() {
210
162
  "",
211
163
  ].join("\n");
212
164
  }
213
- async function prepared() {
214
- await rm(project, { recursive: true, force: true });
215
- await mkdir(join(project, "workflows"), { recursive: true });
216
- await writeFile(join(project, "package.json"), '{"name":"cloving-self-host","private":true,"type":"module"}');
217
- for (const file of await workflowFiles(join(product, "workflows")))
218
- await copyFile(join(product, "workflows", file), join(project, "workflows", file));
219
- await linkedPackages();
220
- const compiled = await ended([
221
- join(product, `tools/compile-workflows${extname(import.meta.filename)}`),
222
- "workflows",
223
- "artifacts",
224
- ], process.env);
225
- if (compiled !== 0)
226
- throw new Error("The workflows did not compile.");
227
- return registered();
228
- }
229
165
  const tokenPrices = parseTokenPrices(process.env["CLOVING_TOKEN_PRICES"]);
166
+ const fallbackModels = parseFallbackModels(process.env["CLOVING_FALLBACK_MODELS"]);
230
167
  const profiles = await profilesFile();
231
168
  const worker = selectedWorker();
232
169
  const command = worker === "codex"
@@ -234,16 +171,25 @@ const command = worker === "codex"
234
171
  : (listed("CLOVING_CLAUDE_COMMAND") ?? defaultCommand);
235
172
  const nextCommand = listed("CLOVING_NEXT_COMMAND");
236
173
  const ticketCommand = listed("CLOVING_TICKET_COMMAND");
174
+ const lanes = queueLanes();
237
175
  const queueMilliseconds = queueInterval({ nextCommand, ticketCommand });
238
- const manifests = await prepared();
176
+ const manifests = await preparedProject({
177
+ product,
178
+ project,
179
+ repository,
180
+ home: homedir(),
181
+ xdgConfigHome: process.env["XDG_CONFIG_HOME"],
182
+ });
239
183
  await launchers({
240
184
  tokenPrices,
185
+ fallbackModels,
241
186
  worker,
242
187
  profiles,
243
188
  command,
244
189
  ticketCommand,
245
190
  nextCommand,
246
191
  queueMilliseconds,
192
+ queueLanes: lanes,
247
193
  resetQueue: flags.includes("--reset-queue"),
248
194
  notifyCommand: listed("CLOVING_NOTIFY_COMMAND"),
249
195
  githubCommand: listed("CLOVING_GH_COMMAND"),
@@ -0,0 +1,5 @@
1
+ export declare function workflowPackageRoot(directory: string): Promise<string>;
2
+ export declare function workflowBundleDependencies(input: {
3
+ readonly product: string;
4
+ readonly packages: readonly string[];
5
+ }): Promise<string>;
@@ -0,0 +1,93 @@
1
+ import { createHash } from "node:crypto";
2
+ import { access, readFile, realpath } from "node:fs/promises";
3
+ import { findPackageJSON } from "node:module";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { record } from "../src/protocol.js";
6
+ export async function workflowPackageRoot(directory) {
7
+ const root = resolve(directory);
8
+ try {
9
+ await access(join(root, "package.json"));
10
+ return root;
11
+ }
12
+ catch (error) {
13
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
14
+ throw error;
15
+ }
16
+ const parent = dirname(root);
17
+ if (parent === root)
18
+ throw new Error("workflow-package-unavailable");
19
+ return workflowPackageRoot(parent);
20
+ }
21
+ function references(manifest) {
22
+ const dependencies = record(manifest["dependencies"]) ?? {};
23
+ const optional = record(manifest["optionalDependencies"]) ?? {};
24
+ const peers = record(manifest["peerDependencies"]) ?? {};
25
+ const peerMetadata = record(manifest["peerDependenciesMeta"]) ?? {};
26
+ return Object.keys({ ...dependencies, ...optional, ...peers })
27
+ .sort()
28
+ .map((name) => ({
29
+ name,
30
+ optional: Object.hasOwn(optional, name) ||
31
+ (!Object.hasOwn(dependencies, name) &&
32
+ record(peerMetadata[name])?.["optional"] === true),
33
+ }));
34
+ }
35
+ async function manifestFile(from, reference) {
36
+ try {
37
+ const file = findPackageJSON(reference.name, from);
38
+ if (file !== undefined)
39
+ return await realpath(file);
40
+ }
41
+ catch (error) {
42
+ if (!(error instanceof Error &&
43
+ "code" in error &&
44
+ error.code === "ERR_MODULE_NOT_FOUND"))
45
+ throw error;
46
+ }
47
+ if (reference.optional)
48
+ return undefined;
49
+ throw new Error("workflow-dependency-unavailable");
50
+ }
51
+ async function capturePackage(capture, input) {
52
+ const file = await manifestFile(input.from, input);
53
+ if (file === undefined)
54
+ return null;
55
+ const previous = capture.seen.get(file);
56
+ if (previous !== undefined)
57
+ return previous;
58
+ const manifest = await readFile(file, "utf8");
59
+ const parsed = JSON.parse(manifest);
60
+ const fields = record(parsed);
61
+ if (typeof fields?.["name"] !== "string" ||
62
+ typeof fields["version"] !== "string")
63
+ throw new Error("invalid-workflow-dependency");
64
+ const captured = { manifest, dependencies: {} };
65
+ const index = capture.packages.length;
66
+ capture.seen.set(file, index);
67
+ capture.packages.push(captured);
68
+ for (const reference of references(fields))
69
+ captured.dependencies[reference.name] = await capturePackage(capture, {
70
+ ...reference,
71
+ from: file,
72
+ });
73
+ return index;
74
+ }
75
+ export async function workflowBundleDependencies(input) {
76
+ const capture = { seen: new Map(), packages: [] };
77
+ const roots = {};
78
+ const manifest = join(await workflowPackageRoot(input.product), "package.json");
79
+ for (const name of [...input.packages].sort())
80
+ roots[name] = await capturePackage(capture, {
81
+ name,
82
+ from: manifest,
83
+ optional: false,
84
+ });
85
+ return JSON.stringify({
86
+ node: process.version,
87
+ product: createHash("sha256")
88
+ .update(await readFile(manifest))
89
+ .digest("hex"),
90
+ roots,
91
+ packages: capture.packages,
92
+ });
93
+ }
@@ -0,0 +1,20 @@
1
+ interface BundleInput {
2
+ readonly directory: string;
3
+ readonly repository: string;
4
+ readonly project: string;
5
+ }
6
+ interface BundleLookup {
7
+ readonly directory: string;
8
+ readonly repository: string;
9
+ readonly id: string;
10
+ }
11
+ type BundleResult = {
12
+ readonly kind: "available";
13
+ readonly directory: string;
14
+ } | {
15
+ readonly kind: "unavailable";
16
+ readonly code: "pinned-workflow-unavailable";
17
+ };
18
+ export declare function readRetainedWorkflowBundle(input: BundleLookup): Promise<BundleResult>;
19
+ export declare function retainWorkflowBundle(input: BundleInput): Promise<string>;
20
+ export {};
@@ -0,0 +1,173 @@
1
+ import { createHash } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { lstat, mkdir, mkdtemp, open, readdir, realpath, rename, rm, writeFile, } from "node:fs/promises";
4
+ import { dirname, join } from "node:path";
5
+ const unavailable = {
6
+ kind: "unavailable",
7
+ code: "pinned-workflow-unavailable",
8
+ };
9
+ export async function readRetainedWorkflowBundle(input) {
10
+ if (!/^[a-f0-9]{64}$/.test(input.id))
11
+ return unavailable;
12
+ try {
13
+ const directory = join(input.directory, input.id);
14
+ if (!(await lstat(directory)).isDirectory())
15
+ return unavailable;
16
+ const repository = await realpath(input.repository);
17
+ const contents = bundleContents(repository, await projectFiles(directory));
18
+ const stored = await regularFile(join(directory, "bundle.json"));
19
+ if (digest(contents) !== input.id || stored.toString("utf8") !== contents)
20
+ return unavailable;
21
+ return { kind: "available", directory };
22
+ }
23
+ catch {
24
+ return unavailable;
25
+ }
26
+ }
27
+ function digest(bytes) {
28
+ return createHash("sha256").update(bytes).digest("hex");
29
+ }
30
+ async function regularFile(path) {
31
+ const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
32
+ try {
33
+ if (!(await file.stat()).isFile())
34
+ throw new Error("invalid-bundle-file");
35
+ return await file.readFile();
36
+ }
37
+ finally {
38
+ await file.close();
39
+ }
40
+ }
41
+ async function treeFiles(root, name) {
42
+ const path = join(root, name);
43
+ const entry = await lstat(path);
44
+ if (entry.isFile())
45
+ return [{ name, bytes: await regularFile(path) }];
46
+ if (!entry.isDirectory())
47
+ throw new Error("invalid-bundle-file");
48
+ const files = [];
49
+ for (const child of (await readdir(path)).sort())
50
+ files.push(...(await treeFiles(root, `${name}/${child}`)));
51
+ return files;
52
+ }
53
+ async function projectFiles(project) {
54
+ const files = [];
55
+ const names = (await readdir(project)).filter((name) => name.endsWith(".json") && name !== "bundle.json");
56
+ for (const name of [...names, "artifacts", "workflows"].sort())
57
+ files.push(...(await treeFiles(project, name)));
58
+ const required = [
59
+ "package.json",
60
+ "declarations.json",
61
+ "dependencies.json",
62
+ ...[
63
+ "client.mjs",
64
+ "flow.mjs",
65
+ "steps.mjs",
66
+ "manifest.json",
67
+ "flow.mjs.__wf_tmp.js.debug.json",
68
+ "steps.mjs.debug.json",
69
+ ].map((name) => `artifacts/${name}`),
70
+ ];
71
+ if (required.some((name) => !files.some((file) => file.name === name)))
72
+ throw new Error("incomplete-workflow-bundle");
73
+ return files;
74
+ }
75
+ function bundleContents(repository, files) {
76
+ return JSON.stringify({
77
+ schemaVersion: 1,
78
+ repository,
79
+ files: Object.fromEntries(files.map(({ name, bytes }) => [name, digest(bytes)])),
80
+ });
81
+ }
82
+ async function validatedId(input) {
83
+ if ((await readRetainedWorkflowBundle(input)).kind !== "available")
84
+ throw new Error("pinned-workflow-unavailable");
85
+ return input.id;
86
+ }
87
+ async function existingEntry(path) {
88
+ return lstat(path).catch((error) => {
89
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
90
+ return undefined;
91
+ throw error;
92
+ });
93
+ }
94
+ async function retainedBefore(input, contents) {
95
+ try {
96
+ const history = join(input.directory, ".history");
97
+ await mkdir(history, { recursive: true, mode: 0o700 });
98
+ if (!(await lstat(history)).isDirectory())
99
+ throw new Error("pinned-workflow-unavailable");
100
+ const directory = join(history, input.id);
101
+ const entry = await existingEntry(directory);
102
+ if (entry === undefined)
103
+ return false;
104
+ if (!entry.isDirectory())
105
+ throw new Error("pinned-workflow-unavailable");
106
+ const receipt = await regularFile(join(directory, "bundle.json"));
107
+ if (receipt.toString("utf8") !== contents)
108
+ throw new Error("pinned-workflow-unavailable");
109
+ return true;
110
+ }
111
+ catch {
112
+ throw new Error("pinned-workflow-unavailable");
113
+ }
114
+ }
115
+ async function rememberBundle(input, contents) {
116
+ const history = join(input.directory, ".history");
117
+ const temporary = await mkdtemp(join(history, ".pending-"));
118
+ try {
119
+ await writeFile(join(temporary, "bundle.json"), contents, {
120
+ flag: "wx",
121
+ mode: 0o600,
122
+ });
123
+ await publishDirectory(temporary, join(history, input.id));
124
+ await retainedBefore(input, contents);
125
+ }
126
+ finally {
127
+ await rm(temporary, { recursive: true, force: true });
128
+ }
129
+ }
130
+ async function publishDirectory(from, to) {
131
+ await rename(from, to).catch((error) => {
132
+ if (error instanceof Error &&
133
+ "code" in error &&
134
+ ["ENOTEMPTY", "EEXIST"].includes(String(error.code)))
135
+ return;
136
+ throw error;
137
+ });
138
+ }
139
+ export async function retainWorkflowBundle(input) {
140
+ const repository = await realpath(input.repository);
141
+ const files = await projectFiles(input.project);
142
+ const contents = bundleContents(repository, files);
143
+ const id = digest(contents);
144
+ await mkdir(input.directory, { recursive: true, mode: 0o700 });
145
+ const destination = join(input.directory, id);
146
+ const lookup = { ...input, id };
147
+ if (await retainedBefore(lookup, contents))
148
+ return validatedId(lookup);
149
+ if (await existingEntry(destination)) {
150
+ await validatedId(lookup);
151
+ await rememberBundle(lookup, contents);
152
+ return id;
153
+ }
154
+ const temporary = await mkdtemp(join(input.directory, ".pending-"));
155
+ try {
156
+ for (const { name, bytes } of files) {
157
+ const path = join(temporary, name);
158
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
159
+ await writeFile(path, bytes, { flag: "wx", mode: 0o600 });
160
+ }
161
+ await writeFile(join(temporary, "bundle.json"), contents, {
162
+ flag: "wx",
163
+ mode: 0o600,
164
+ });
165
+ await publishDirectory(temporary, destination);
166
+ }
167
+ finally {
168
+ await rm(temporary, { recursive: true, force: true });
169
+ }
170
+ await validatedId(lookup);
171
+ await rememberBundle(lookup, contents);
172
+ return id;
173
+ }
@@ -0,0 +1,8 @@
1
+ import ts from "typescript";
2
+ import { type CandidateSymbols } from "./workflow-candidate-syntax.ts";
3
+ interface Totals {
4
+ readonly knownCallCount: number;
5
+ readonly knownUnitTotal: number;
6
+ }
7
+ export declare function candidateEffectTotals(source: ts.SourceFile, symbols: CandidateSymbols): Totals;
8
+ export {};