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.
- package/README.md +121 -21
- package/assets/watch-fonts/fira-code-OFL.md +93 -0
- package/assets/watch-fonts/fira-code-latin-400-normal.woff2 +0 -0
- package/assets/watch-fonts/fira-code-latin-500-normal.woff2 +0 -0
- package/assets/watch-fonts/heebo-OFL.md +93 -0
- package/assets/watch-fonts/heebo-latin-500-normal.woff2 +0 -0
- package/assets/watch-fonts/heebo-latin-700-normal.woff2 +0 -0
- package/assets/watch-fonts/heebo-latin-800-normal.woff2 +0 -0
- package/assets/watch-fonts/manifest.json +112 -0
- package/assets/watch-fonts/space-grotesk-OFL.md +93 -0
- package/assets/watch-fonts/space-grotesk-latin-400-normal.woff2 +0 -0
- package/assets/watch-fonts/space-grotesk-latin-500-normal.woff2 +0 -0
- package/assets/watch-fonts/space-grotesk-latin-600-normal.woff2 +0 -0
- package/assets/watch-fonts/space-grotesk-latin-700-normal.woff2 +0 -0
- package/dist/src/authority/commands.d.ts +1 -0
- package/dist/src/authority/commands.js +25 -4
- package/dist/src/authority/dispatch-decision.d.ts +1 -0
- package/dist/src/authority/dispatch-decision.js +7 -4
- package/dist/src/authority/events.js +2 -2
- package/dist/src/authority/handoff-chain.d.ts +24 -0
- package/dist/src/authority/handoff-chain.js +46 -0
- package/dist/src/authority/reconciliation.d.ts +1 -0
- package/dist/src/authority/reconciliation.js +18 -11
- package/dist/src/authority/transitions.d.ts +1 -1
- package/dist/src/authority/transitions.js +3 -0
- package/dist/src/broker/actor/authority-queries.d.ts +4 -0
- package/dist/src/broker/actor/authority-queries.js +51 -2
- package/dist/src/broker/actor/delivery-intents.d.ts +1 -0
- package/dist/src/broker/actor/effect-delivery.js +9 -0
- package/dist/src/broker/authority-admission.js +13 -5
- package/dist/src/chats/analysis-batches.d.ts +18 -0
- package/dist/src/chats/analysis-batches.js +15 -1
- package/dist/src/chats/analysis-checkpoint.d.ts +29 -0
- package/dist/src/chats/analysis-checkpoint.js +31 -0
- package/dist/src/chats/analysis-input.d.ts +7 -1
- package/dist/src/chats/analysis-input.js +20 -6
- package/dist/src/chats/analysis-merge.d.ts +16 -0
- package/dist/src/chats/analysis-merge.js +134 -0
- package/dist/src/chats/analysis-prompt.d.ts +8 -0
- package/dist/src/chats/analysis-prompt.js +27 -1
- package/dist/src/chats/analysis-proposal.d.ts +2 -0
- package/dist/src/chats/analysis-proposal.js +9 -0
- package/dist/src/chats/analysis-shape.d.ts +1 -0
- package/dist/src/chats/analysis-shape.js +11 -4
- package/dist/src/chats/reference-mapping.d.ts +16 -0
- package/dist/src/chats/reference-mapping.js +153 -0
- package/dist/src/chats/workflow-generation-projection.d.ts +8 -0
- package/dist/src/chats/workflow-generation-projection.js +75 -0
- package/dist/src/chats/workflow-generation-source.d.ts +25 -0
- package/dist/src/chats/workflow-generation-source.js +124 -0
- package/dist/src/chats/workflow-generation-text.d.ts +23 -0
- package/dist/src/chats/workflow-generation-text.js +74 -0
- package/dist/src/chats/workflow-generation.d.ts +69 -0
- package/dist/src/chats/workflow-generation.js +142 -0
- package/dist/src/cli/render.d.ts +1 -0
- package/dist/src/cli/render.js +60 -13
- package/dist/src/cli/usage.js +5 -2
- package/dist/src/cli/watch-board-view.d.ts +12 -1
- package/dist/src/cli/watch-board-view.js +57 -11
- package/dist/src/cli/watch-board.d.ts +7 -1
- package/dist/src/cli/watch-board.js +42 -10
- package/dist/src/cli/watch-client.d.ts +1 -1
- package/dist/src/cli/watch-client.js +7 -3
- package/dist/src/cli/watch-model.d.ts +10 -4
- package/dist/src/cli/watch-model.js +35 -58
- package/dist/src/cli/watch-page.js +21 -4
- package/dist/src/cli/watch-phases.js +3 -3
- package/dist/src/cli/watch-pr-command.d.ts +5 -2
- package/dist/src/cli/watch-pr-command.js +41 -4
- package/dist/src/cli/watch-pr-identity.d.ts +14 -2
- package/dist/src/cli/watch-pr-identity.js +25 -0
- package/dist/src/cli/watch-pr-observer.d.ts +1 -0
- package/dist/src/cli/watch-pr-observer.js +48 -6
- package/dist/src/cli/watch-pull-request.d.ts +10 -0
- package/dist/src/cli/watch-pull-request.js +130 -0
- package/dist/src/cli/watch-reads.d.ts +7 -1
- package/dist/src/cli/watch-reads.js +42 -20
- package/dist/src/cli/watch-render.d.ts +14 -1
- package/dist/src/cli/watch-render.js +2 -1
- package/dist/src/cli/watch-server.js +33 -2
- package/dist/src/cli/watch-turn.js +1 -1
- package/dist/src/cli/watch-view.d.ts +7 -0
- package/dist/src/cli/watch-view.js +51 -3
- package/dist/src/daemon/analysis-worker.d.ts +0 -2
- package/dist/src/daemon/analysis-worker.js +71 -19
- package/dist/src/daemon/chats-batch-binding.d.ts +23 -0
- package/dist/src/daemon/chats-batch-binding.js +99 -0
- package/dist/src/daemon/chats-capture.d.ts +18 -0
- package/dist/src/daemon/chats-capture.js +194 -0
- package/dist/src/daemon/chats-checkpoints.d.ts +41 -0
- package/dist/src/daemon/chats-checkpoints.js +186 -0
- package/dist/src/daemon/chats-files.d.ts +29 -2
- package/dist/src/daemon/chats-files.js +142 -3
- package/dist/src/daemon/chats-follow-up.d.ts +38 -0
- package/dist/src/daemon/chats-follow-up.js +180 -0
- package/dist/src/daemon/chats-question.d.ts +15 -0
- package/dist/src/daemon/chats-question.js +93 -0
- package/dist/src/daemon/chats-worker.d.ts +1 -0
- package/dist/src/daemon/chats-worker.js +52 -51
- package/dist/src/daemon/claude-turn.d.ts +2 -1
- package/dist/src/daemon/claude-turn.js +1 -1
- package/dist/src/daemon/claude-worker.d.ts +0 -2
- package/dist/src/daemon/claude-worker.js +6 -11
- package/dist/src/daemon/daemon.d.ts +5 -1
- package/dist/src/daemon/daemon.js +38 -10
- package/dist/src/daemon/delivery-router.js +6 -0
- package/dist/src/daemon/github-calls.d.ts +4 -0
- package/dist/src/daemon/github-calls.js +16 -7
- package/dist/src/daemon/github-ci.d.ts +6 -0
- package/dist/src/daemon/github-ci.js +170 -0
- package/dist/src/daemon/github-merge-call.d.ts +14 -0
- package/dist/src/daemon/github-merge-call.js +73 -0
- package/dist/src/daemon/github-merge.d.ts +3 -5
- package/dist/src/daemon/github-merge.js +52 -95
- package/dist/src/daemon/github-methods.d.ts +28 -0
- package/dist/src/daemon/github-methods.js +80 -16
- package/dist/src/daemon/github-operation.d.ts +2 -0
- package/dist/src/daemon/github-retry.d.ts +6 -0
- package/dist/src/daemon/github-retry.js +49 -0
- package/dist/src/daemon/github-ticket-status.js +27 -1
- package/dist/src/daemon/github-worker.js +2 -1
- package/dist/src/daemon/handoff-worker.d.ts +10 -0
- package/dist/src/daemon/handoff-worker.js +232 -0
- package/dist/src/daemon/issue-queue.d.ts +1 -0
- package/dist/src/daemon/issue-queue.js +94 -42
- package/dist/src/daemon/notify-journal.d.ts +18 -0
- package/dist/src/daemon/notify-journal.js +108 -0
- package/dist/src/daemon/notify-message.js +2 -1
- package/dist/src/daemon/notify.js +176 -37
- package/dist/src/daemon/retrying-turn.d.ts +3 -0
- package/dist/src/daemon/retrying-turn.js +55 -9
- package/dist/src/daemon/ticket-dependency-waits.d.ts +18 -0
- package/dist/src/daemon/ticket-dependency-waits.js +60 -0
- package/dist/src/daemon/ticket-registration.d.ts +8 -4
- package/dist/src/daemon/ticket-registration.js +76 -22
- package/dist/src/daemon/ticket-source.d.ts +4 -1
- package/dist/src/daemon/ticket-source.js +8 -2
- package/dist/src/decision-visibility.d.ts +2 -0
- package/dist/src/decision-visibility.js +10 -0
- package/dist/src/execution-pin.d.ts +4 -0
- package/dist/src/execution-pin.js +23 -2
- package/dist/src/host/start-delivery.d.ts +2 -1
- package/dist/src/host/start-delivery.js +1 -1
- package/dist/src/host/step-bridge-host.d.ts +1 -0
- package/dist/src/host/step-bridge-host.js +16 -0
- package/dist/src/host/world-reconciliation.d.ts +1 -0
- package/dist/src/host/world-reconciliation.js +3 -0
- package/dist/src/host/world-runs.js +5 -0
- package/dist/src/issue-identity.d.ts +6 -0
- package/dist/src/issue-identity.js +23 -0
- package/dist/src/service/queue-runs.d.ts +3 -0
- package/dist/src/service/queue-runs.js +39 -0
- package/dist/src/service/registry.d.ts +8 -2
- package/dist/src/service/registry.js +20 -8
- package/dist/src/service/replies.d.ts +2 -0
- package/dist/src/service/run-handlers.js +44 -12
- package/dist/src/service/service.d.ts +3 -0
- package/dist/src/service/service.js +3 -1
- package/dist/src/service/submission-handler.d.ts +1 -0
- package/dist/src/service/submission-handler.js +92 -13
- package/dist/src/service/worker-totals.js +25 -1
- package/dist/src/step-bridge.d.ts +9 -0
- package/dist/src/steps/handoff.d.ts +2 -0
- package/dist/src/steps/handoff.js +13 -0
- package/dist/src/ticket-dependencies.d.ts +1 -0
- package/dist/src/ticket-dependencies.js +15 -0
- package/dist/src/ticket-status.js +7 -4
- package/dist/src/token-prices.d.ts +2 -0
- package/dist/src/token-prices.js +41 -9
- package/dist/src/worker-usage.d.ts +9 -0
- package/dist/src/worker-usage.js +1 -1
- package/dist/src/workflow-admission.d.ts +1 -0
- package/dist/src/workflow-candidate.js +1 -0
- package/dist/src/workflow-contracts.d.ts +2 -0
- package/dist/src/workflow-contracts.js +8 -3
- package/dist/src/workflow-declaration.d.ts +11 -0
- package/dist/src/workflow-declaration.js +71 -15
- package/dist/src/workflow-discovery.d.ts +18 -0
- package/dist/src/workflow-discovery.js +33 -0
- package/dist/src/workflow-refusals.d.ts +6 -0
- package/dist/src/workflow-refusals.js +39 -0
- package/dist/src/workflows.d.ts +2 -1
- package/dist/src/workflows.js +1 -0
- package/dist/tools/admit-workflow.d.ts +13 -0
- package/dist/tools/admit-workflow.js +94 -0
- package/dist/tools/self-host-project.d.ts +17 -0
- package/dist/tools/self-host-project.js +177 -0
- package/dist/tools/self-host.js +27 -81
- package/dist/tools/workflow-bundle-dependencies.d.ts +5 -0
- package/dist/tools/workflow-bundle-dependencies.js +93 -0
- package/dist/tools/workflow-bundles.d.ts +20 -0
- package/dist/tools/workflow-bundles.js +173 -0
- package/dist/tools/workflow-candidate-counts.d.ts +8 -0
- package/dist/tools/workflow-candidate-counts.js +251 -0
- package/dist/tools/workflow-candidate-program.d.ts +11 -0
- package/dist/tools/workflow-candidate-program.js +78 -0
- package/dist/tools/workflow-candidate-source.d.ts +6 -0
- package/dist/tools/workflow-candidate-source.js +107 -0
- package/dist/tools/workflow-candidate-syntax.d.ts +15 -0
- package/dist/tools/workflow-candidate-syntax.js +219 -0
- package/dist/tools/workflow-folder-capture.d.ts +17 -0
- package/dist/tools/workflow-folder-capture.js +216 -0
- package/dist/tools/workflow-folders.d.ts +33 -0
- package/dist/tools/workflow-folders.js +150 -0
- package/dist/tools/workflow-source-effects.d.ts +10 -0
- package/dist/tools/workflow-source-effects.js +213 -0
- package/dist/workflows/analyze-chats.d.ts +6 -1
- package/dist/workflows/analyze-chats.declaration.json +14 -1
- package/dist/workflows/analyze-chats.js +188 -11
- package/dist/workflows/develop-prd.js +71 -3
- package/dist/workflows/develop-ticket.js +5 -0
- package/dist/workflows/ticket-prompts.js +12 -2
- package/package.json +21 -3
package/dist/tools/self-host.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { isProfilesFile } from "../src/turn-profile.js";
|
|
2
|
-
import { parseTokenPrices } from "../src/token-prices.js";
|
|
3
|
-
import { execFileSync
|
|
4
|
-
import {
|
|
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 {
|
|
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
|
|
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,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 {};
|