@procedures/lib 0.3.0 → 0.5.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/CHANGELOG.md +74 -0
- package/README.md +13 -43
- package/dist/.tsbuildinfo +1 -1
- package/dist/cli-flags.d.ts +2 -0
- package/dist/cli-flags.d.ts.map +1 -1
- package/dist/cli-flags.js +12 -0
- package/dist/cli-flags.js.map +1 -1
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +6 -51
- package/dist/module.js.map +1 -1
- package/dist/procedures/lib/audit.d.ts +15 -2
- package/dist/procedures/lib/audit.d.ts.map +1 -1
- package/dist/procedures/lib/audit.js +32 -4
- package/dist/procedures/lib/audit.js.map +1 -1
- package/dist/types.d.ts +17 -58
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +8 -92
- package/dist/types.js.map +1 -1
- package/package.json +8 -8
- package/src/cli-flags.ts +14 -0
- package/src/module.ts +5 -61
- package/src/procedures/lib/audit.ts +34 -4
- package/src/types.ts +30 -150
- package/dist/procedures/core/catch.d.ts +0 -33
- package/dist/procedures/core/catch.d.ts.map +0 -1
- package/dist/procedures/core/catch.js +0 -118
- package/dist/procedures/core/catch.js.map +0 -1
- package/dist/procedures/core/index.d.ts +0 -9
- package/dist/procedures/core/index.d.ts.map +0 -1
- package/dist/procedures/core/index.js +0 -8
- package/dist/procedures/core/index.js.map +0 -1
- package/dist/procedures/dag/index.d.ts +0 -5
- package/dist/procedures/dag/index.d.ts.map +0 -1
- package/dist/procedures/dag/index.js +0 -5
- package/dist/procedures/dag/index.js.map +0 -1
- package/dist/procedures/dag/traverse.d.ts +0 -54
- package/dist/procedures/dag/traverse.d.ts.map +0 -1
- package/dist/procedures/dag/traverse.js +0 -252
- package/dist/procedures/dag/traverse.js.map +0 -1
- package/src/procedures/core/catch.ts +0 -142
- package/src/procedures/core/index.ts +0 -9
- package/src/procedures/dag/index.ts +0 -5
- package/src/procedures/dag/traverse.ts +0 -281
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dag.traverse procedure
|
|
3
|
-
*
|
|
4
|
-
* General-purpose DAG traversal that executes a visit procedure
|
|
5
|
-
* for each node in dependency order.
|
|
6
|
-
*
|
|
7
|
-
* This enables declarative composition of traversals:
|
|
8
|
-
* ```typescript
|
|
9
|
-
* // Execute git add on all packages (using $when: "$parent")
|
|
10
|
-
* await client.exec({
|
|
11
|
-
* $proc: ["dag", "traverse"],
|
|
12
|
-
* input: {
|
|
13
|
-
* visit: {
|
|
14
|
-
* $proc: ["git", "add"],
|
|
15
|
-
* input: { all: true },
|
|
16
|
-
* $when: "$parent" // Defer to dag.traverse
|
|
17
|
-
* }
|
|
18
|
-
* }
|
|
19
|
-
* });
|
|
20
|
-
*
|
|
21
|
-
* // Simple form with just procedure path
|
|
22
|
-
* await client.exec({
|
|
23
|
-
* $proc: ["dag", "traverse"],
|
|
24
|
-
* input: {
|
|
25
|
-
* visit: ["git", "add"]
|
|
26
|
-
* }
|
|
27
|
-
* });
|
|
28
|
-
* ```
|
|
29
|
-
*
|
|
30
|
-
* The `$when` field controls when nested $proc refs are executed:
|
|
31
|
-
* - "$immediate" (default): Execute during hydration
|
|
32
|
-
* - "$parent": Defer to parent procedure (dag.traverse executes per-node)
|
|
33
|
-
* - "$never": Never auto-execute, pass as pure data
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
import { existsSync } from "node:fs";
|
|
37
|
-
import { dirname, join, resolve } from "node:path";
|
|
38
|
-
import type { ProcedureContext, ProcedurePath } from "@procedures/client";
|
|
39
|
-
import { isAnyProcedureRef, hydrateInput, scopeOf } from "@procedures/client";
|
|
40
|
-
import type {
|
|
41
|
-
DAGNode,
|
|
42
|
-
DagTraverseInput,
|
|
43
|
-
DagTraverseOutput,
|
|
44
|
-
TraverseNodeResult,
|
|
45
|
-
} from "../../types.js";
|
|
46
|
-
import { libScan } from "../lib/scan.js";
|
|
47
|
-
import {
|
|
48
|
-
buildDAGNodes,
|
|
49
|
-
buildLeveledDAG,
|
|
50
|
-
executeDAG,
|
|
51
|
-
createProcessor,
|
|
52
|
-
filterDAGFromRoot,
|
|
53
|
-
} from "../../dag/index.js";
|
|
54
|
-
|
|
55
|
-
/** The folder of the git repository that holds `path` (it has `.git`), or `path` itself. */
|
|
56
|
-
export function repositoryRoot(path: string): string {
|
|
57
|
-
let current = resolve(path);
|
|
58
|
-
for (;;) {
|
|
59
|
-
if (existsSync(join(current, ".git"))) return current;
|
|
60
|
-
const parent = dirname(current);
|
|
61
|
-
if (parent === current) return resolve(path);
|
|
62
|
-
current = parent;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** True when the value names a `git` procedure: a path, or a `$proc` ref at any depth. */
|
|
67
|
-
export function callsGit(value: unknown): boolean {
|
|
68
|
-
if (Array.isArray(value)) {
|
|
69
|
-
if (value[0] === "git" && value.every((part) => typeof part === "string")) return true;
|
|
70
|
-
return value.some(callsGit);
|
|
71
|
-
}
|
|
72
|
-
if (typeof value === "object" && value !== null) {
|
|
73
|
-
return Object.values(value).some(callsGit);
|
|
74
|
-
}
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Run the tasks of one key one at a time, in the order of the calls. Tasks of other keys run
|
|
80
|
-
* in parallel.
|
|
81
|
-
*/
|
|
82
|
-
export function keyedLock(): <T>(key: string, task: () => Promise<T>) => Promise<T> {
|
|
83
|
-
const tails = new Map<string, Promise<void>>();
|
|
84
|
-
return async <T>(key: string, task: () => Promise<T>): Promise<T> => {
|
|
85
|
-
const previous = tails.get(key) ?? Promise.resolve();
|
|
86
|
-
let release!: () => void;
|
|
87
|
-
const done = new Promise<void>((resolveDone) => {
|
|
88
|
-
release = resolveDone;
|
|
89
|
-
});
|
|
90
|
-
const tail = previous.then(() => done);
|
|
91
|
-
tails.set(key, tail);
|
|
92
|
-
await previous;
|
|
93
|
-
try {
|
|
94
|
-
return await task();
|
|
95
|
-
} finally {
|
|
96
|
-
release();
|
|
97
|
-
if (tails.get(key) === tail) tails.delete(key);
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Execute a DAG traversal with a custom visit procedure
|
|
104
|
-
*
|
|
105
|
-
* The visits of nodes in one git repository run one at a time when the visit calls git (or
|
|
106
|
-
* with `serialize: "repository"`). All the packages of a workspace share one repository, and
|
|
107
|
-
* concurrent `git add` calls failed on `index.lock` (deep dive DATA-17).
|
|
108
|
-
*/
|
|
109
|
-
export async function dagTraverse(
|
|
110
|
-
input: DagTraverseInput,
|
|
111
|
-
ctx: ProcedureContext
|
|
112
|
-
): Promise<DagTraverseOutput> {
|
|
113
|
-
const startTime = Date.now();
|
|
114
|
-
const results: TraverseNodeResult[] = [];
|
|
115
|
-
|
|
116
|
-
// Scan for all packages
|
|
117
|
-
const scanResult = await libScan(input.rootPath !== undefined ? { rootPath: input.rootPath } : {}, ctx);
|
|
118
|
-
let allNodes = buildDAGNodes(scanResult.packages);
|
|
119
|
-
|
|
120
|
-
// Filter to specific packages if requested
|
|
121
|
-
if (input.filter && input.filter.length > 0) {
|
|
122
|
-
const filtered = new Map<string, DAGNode>();
|
|
123
|
-
for (const name of input.filter) {
|
|
124
|
-
const node = allNodes.get(name);
|
|
125
|
-
if (node) {
|
|
126
|
-
filtered.set(name, node);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
allNodes = filtered;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Filter to root and its dependencies if requested
|
|
133
|
-
if (input.root) {
|
|
134
|
-
const rootNode = allNodes.get(input.root);
|
|
135
|
-
if (!rootNode) {
|
|
136
|
-
return {
|
|
137
|
-
success: false,
|
|
138
|
-
results: [],
|
|
139
|
-
totalDuration: Date.now() - startTime,
|
|
140
|
-
visited: 0,
|
|
141
|
-
failed: 0,
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
allNodes = filterDAGFromRoot(allNodes, input.root);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Build DAG for dependency-ordered execution
|
|
148
|
-
const dag = buildLeveledDAG(allNodes);
|
|
149
|
-
|
|
150
|
-
// Determine how to execute the visit procedure
|
|
151
|
-
// visit can be:
|
|
152
|
-
// - Array: procedure path like ["git", "add"]
|
|
153
|
-
// - $proc ref: { $proc: [...], input: {...}, $when: "$parent" }
|
|
154
|
-
const visitIsRef = isAnyProcedureRef(input.visit);
|
|
155
|
-
let visitPath: ProcedurePath;
|
|
156
|
-
let baseInput: unknown = {};
|
|
157
|
-
|
|
158
|
-
if (Array.isArray(input.visit)) {
|
|
159
|
-
visitPath = input.visit;
|
|
160
|
-
} else if (visitIsRef && typeof input.visit === "object" && "$proc" in input.visit) {
|
|
161
|
-
visitPath = input.visit.$proc;
|
|
162
|
-
baseInput = input.visit.input ?? {};
|
|
163
|
-
} else {
|
|
164
|
-
throw new Error("visit must be a procedure path or $proc reference with $when");
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const serialize = input.serialize ?? "auto";
|
|
168
|
-
const oneAtATime = serialize === "repository" || (serialize === "auto" && callsGit(input.visit));
|
|
169
|
-
const lock = keyedLock();
|
|
170
|
-
|
|
171
|
-
// Create processor that executes visit procedure per node
|
|
172
|
-
const processor = createProcessor(async (node: DAGNode) => {
|
|
173
|
-
if (oneAtATime) {
|
|
174
|
-
await lock(repositoryRoot(node.repoPath), () => visit(node));
|
|
175
|
-
} else {
|
|
176
|
-
await visit(node);
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
async function visit(node: DAGNode): Promise<void> {
|
|
181
|
-
if (input.dryRun) {
|
|
182
|
-
results.push({
|
|
183
|
-
name: node.name,
|
|
184
|
-
path: node.repoPath,
|
|
185
|
-
success: true,
|
|
186
|
-
duration: 0,
|
|
187
|
-
output: { dryRun: true, wouldExecute: visitPath },
|
|
188
|
-
});
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const nodeStartTime = Date.now();
|
|
193
|
-
|
|
194
|
-
try {
|
|
195
|
-
// Merge base input with node context
|
|
196
|
-
const visitInput = {
|
|
197
|
-
...(typeof baseInput === "object" ? baseInput : {}),
|
|
198
|
-
cwd: node.repoPath,
|
|
199
|
-
node: {
|
|
200
|
-
name: node.name,
|
|
201
|
-
path: node.repoPath,
|
|
202
|
-
dependencies: node.dependencies,
|
|
203
|
-
},
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
// Create executor for hydration that injects cwd into every call
|
|
207
|
-
const executor = async <TIn, TOut>(path: ProcedurePath, inp: TIn): Promise<TOut> => {
|
|
208
|
-
// Inject cwd into the input so nested procedures run in correct directory
|
|
209
|
-
const inputWithCwd = typeof inp === "object" && inp !== null
|
|
210
|
-
? { ...inp, cwd: node.repoPath }
|
|
211
|
-
: inp;
|
|
212
|
-
return ctx.client.call(path, inputWithCwd) as Promise<TOut>;
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
// Hydrate the input (execute any nested procedure refs like chain steps)
|
|
216
|
-
// Push "dag.traverse" onto context stack so refs with $when: "dag.traverse" execute
|
|
217
|
-
const hydratedInput = await hydrateInput(visitInput, executor, {
|
|
218
|
-
contextStack: ["dag.traverse"],
|
|
219
|
-
onRef: ctx.onRef,
|
|
220
|
-
// The names of the chain around this procedure: a `$ref` in the visit can read them
|
|
221
|
-
scope: scopeOf(input),
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
// Execute visit procedure with hydrated input
|
|
225
|
-
const output = await ctx.client.call(visitPath, hydratedInput);
|
|
226
|
-
|
|
227
|
-
results.push({
|
|
228
|
-
name: node.name,
|
|
229
|
-
path: node.repoPath,
|
|
230
|
-
success: true,
|
|
231
|
-
duration: Date.now() - nodeStartTime,
|
|
232
|
-
output,
|
|
233
|
-
});
|
|
234
|
-
} catch (error) {
|
|
235
|
-
const result: TraverseNodeResult = {
|
|
236
|
-
name: node.name,
|
|
237
|
-
path: node.repoPath,
|
|
238
|
-
success: false,
|
|
239
|
-
duration: Date.now() - nodeStartTime,
|
|
240
|
-
error: error instanceof Error ? error.message : String(error),
|
|
241
|
-
};
|
|
242
|
-
results.push(result);
|
|
243
|
-
throw error; // Re-throw for DAG executor to handle
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Execute DAG traversal
|
|
248
|
-
const dagResult = await executeDAG(dag, processor, {
|
|
249
|
-
concurrency: input.concurrency ?? 4,
|
|
250
|
-
failFast: !input.continueOnError,
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
// If dry run, we already populated results in the processor
|
|
254
|
-
if (!input.dryRun) {
|
|
255
|
-
// Ensure all nodes have results (some may have been skipped due to errors)
|
|
256
|
-
for (const [name, nodeResult] of dagResult.results) {
|
|
257
|
-
const existing = results.find((r) => r.name === name);
|
|
258
|
-
if (!existing) {
|
|
259
|
-
const node = allNodes.get(name);
|
|
260
|
-
const result: TraverseNodeResult = {
|
|
261
|
-
name,
|
|
262
|
-
path: node?.repoPath ?? "unknown",
|
|
263
|
-
success: nodeResult.success,
|
|
264
|
-
duration: nodeResult.duration,
|
|
265
|
-
};
|
|
266
|
-
if (nodeResult.error?.message) {
|
|
267
|
-
result.error = nodeResult.error.message;
|
|
268
|
-
}
|
|
269
|
-
results.push(result);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return {
|
|
275
|
-
success: dagResult.success,
|
|
276
|
-
results,
|
|
277
|
-
totalDuration: Date.now() - startTime,
|
|
278
|
-
visited: results.filter((r) => r.success).length,
|
|
279
|
-
failed: results.filter((r) => !r.success).length,
|
|
280
|
-
};
|
|
281
|
-
}
|