@procedures/lib 0.0.0-stage → 0.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 (114) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/LICENSE +21 -0
  3. package/README.md +119 -2
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/cli-flags.d.ts +27 -0
  6. package/dist/cli-flags.d.ts.map +1 -0
  7. package/dist/cli-flags.js +33 -0
  8. package/dist/cli-flags.js.map +1 -0
  9. package/dist/dag/builder.d.ts +18 -0
  10. package/dist/dag/builder.d.ts.map +1 -0
  11. package/dist/dag/builder.js +35 -0
  12. package/dist/dag/builder.js.map +1 -0
  13. package/dist/dag/index.d.ts +12 -0
  14. package/dist/dag/index.d.ts.map +1 -0
  15. package/dist/dag/index.js +12 -0
  16. package/dist/dag/index.js.map +1 -0
  17. package/dist/git/index.d.ts +5 -0
  18. package/dist/git/index.d.ts.map +1 -0
  19. package/dist/git/index.js +5 -0
  20. package/dist/git/index.js.map +1 -0
  21. package/dist/git/ref-parser.d.ts +34 -0
  22. package/dist/git/ref-parser.d.ts.map +1 -0
  23. package/dist/git/ref-parser.js +70 -0
  24. package/dist/git/ref-parser.js.map +1 -0
  25. package/dist/index.d.ts +14 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +19 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/module.d.ts +22 -0
  30. package/dist/module.d.ts.map +1 -0
  31. package/dist/module.js +180 -0
  32. package/dist/module.js.map +1 -0
  33. package/dist/procedures/core/catch.d.ts +33 -0
  34. package/dist/procedures/core/catch.d.ts.map +1 -0
  35. package/dist/procedures/core/catch.js +118 -0
  36. package/dist/procedures/core/catch.js.map +1 -0
  37. package/dist/procedures/core/index.d.ts +9 -0
  38. package/dist/procedures/core/index.d.ts.map +1 -0
  39. package/dist/procedures/core/index.js +8 -0
  40. package/dist/procedures/core/index.js.map +1 -0
  41. package/dist/procedures/dag/index.d.ts +5 -0
  42. package/dist/procedures/dag/index.d.ts.map +1 -0
  43. package/dist/procedures/dag/index.js +5 -0
  44. package/dist/procedures/dag/index.js.map +1 -0
  45. package/dist/procedures/dag/traverse.d.ts +54 -0
  46. package/dist/procedures/dag/traverse.d.ts.map +1 -0
  47. package/dist/procedures/dag/traverse.js +252 -0
  48. package/dist/procedures/dag/traverse.js.map +1 -0
  49. package/dist/procedures/ecosystem/index.d.ts +7 -0
  50. package/dist/procedures/ecosystem/index.d.ts.map +1 -0
  51. package/dist/procedures/ecosystem/index.js +6 -0
  52. package/dist/procedures/ecosystem/index.js.map +1 -0
  53. package/dist/procedures/ecosystem/procedures.d.ts +63 -0
  54. package/dist/procedures/ecosystem/procedures.d.ts.map +1 -0
  55. package/dist/procedures/ecosystem/procedures.js +215 -0
  56. package/dist/procedures/ecosystem/procedures.js.map +1 -0
  57. package/dist/procedures/lib/audit.d.ts +29 -0
  58. package/dist/procedures/lib/audit.d.ts.map +1 -0
  59. package/dist/procedures/lib/audit.js +236 -0
  60. package/dist/procedures/lib/audit.js.map +1 -0
  61. package/dist/procedures/lib/index.d.ts +8 -0
  62. package/dist/procedures/lib/index.d.ts.map +1 -0
  63. package/dist/procedures/lib/index.js +8 -0
  64. package/dist/procedures/lib/index.js.map +1 -0
  65. package/dist/procedures/lib/new.d.ts +20 -0
  66. package/dist/procedures/lib/new.d.ts.map +1 -0
  67. package/dist/procedures/lib/new.js +205 -0
  68. package/dist/procedures/lib/new.js.map +1 -0
  69. package/dist/procedures/lib/rename.d.ts +28 -0
  70. package/dist/procedures/lib/rename.d.ts.map +1 -0
  71. package/dist/procedures/lib/rename.js +267 -0
  72. package/dist/procedures/lib/rename.js.map +1 -0
  73. package/dist/procedures/lib/scan.d.ts +14 -0
  74. package/dist/procedures/lib/scan.d.ts.map +1 -0
  75. package/dist/procedures/lib/scan.js +102 -0
  76. package/dist/procedures/lib/scan.js.map +1 -0
  77. package/dist/register.d.ts +7 -0
  78. package/dist/register.d.ts.map +1 -0
  79. package/dist/register.js +9 -0
  80. package/dist/register.js.map +1 -0
  81. package/dist/scoped-input.d.ts +17 -0
  82. package/dist/scoped-input.d.ts.map +1 -0
  83. package/dist/scoped-input.js +23 -0
  84. package/dist/scoped-input.js.map +1 -0
  85. package/dist/types.d.ts +248 -0
  86. package/dist/types.d.ts.map +1 -0
  87. package/dist/types.js +231 -0
  88. package/dist/types.js.map +1 -0
  89. package/dist/workspace.d.ts +46 -0
  90. package/dist/workspace.d.ts.map +1 -0
  91. package/dist/workspace.js +98 -0
  92. package/dist/workspace.js.map +1 -0
  93. package/package.json +87 -3
  94. package/src/cli-flags.ts +49 -0
  95. package/src/dag/builder.ts +44 -0
  96. package/src/dag/index.ts +23 -0
  97. package/src/git/index.ts +11 -0
  98. package/src/git/ref-parser.ts +83 -0
  99. package/src/index.ts +56 -0
  100. package/src/module.ts +232 -0
  101. package/src/procedures/core/catch.ts +142 -0
  102. package/src/procedures/core/index.ts +9 -0
  103. package/src/procedures/dag/index.ts +5 -0
  104. package/src/procedures/dag/traverse.ts +281 -0
  105. package/src/procedures/ecosystem/index.ts +12 -0
  106. package/src/procedures/ecosystem/procedures.ts +317 -0
  107. package/src/procedures/lib/audit.ts +307 -0
  108. package/src/procedures/lib/index.ts +8 -0
  109. package/src/procedures/lib/new.ts +253 -0
  110. package/src/procedures/lib/rename.ts +355 -0
  111. package/src/procedures/lib/scan.ts +143 -0
  112. package/src/register.ts +11 -0
  113. package/src/types.ts +526 -0
  114. package/src/workspace.ts +110 -0
package/src/module.ts ADDED
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Module (no side effects): the procedures of this package as data, for `createRegistry()`.
3
+ *
4
+ * Procedure Registration for lib operations
5
+ *
6
+ * This is the canonical home for lib.* procedures.
7
+ * client-cli no longer registers these to avoid duplicates.
8
+ */
9
+
10
+ import { createProcedure, zodAdapter, RUNS_REFS_TAG, defineModule, type ProcedureModule, PROCEDURE_REGISTRY, registerModules, type ProcedureRegistry } from "@procedures/client";
11
+ import gitModule from "@procedures/git/module";
12
+ import fsModule from "@procedures/fs/module";
13
+ import {
14
+ libScan,
15
+ libRename,
16
+ libNew,
17
+ libAudit,
18
+ } from "./procedures/lib/index.js";
19
+ import {
20
+ ecosystemProcedures,
21
+ EcosystemProceduresInputSchema,
22
+ EcosystemProceduresOutputSchema,
23
+ type EcosystemProceduresInput,
24
+ type EcosystemProceduresOutput,
25
+ } from "./procedures/ecosystem/index.js";
26
+ import { dagTraverse } from "./procedures/dag/index.js";
27
+ import { coreCatch } from "./procedures/core/index.js";
28
+ import {
29
+ LibScanInputSchema,
30
+ LibRenameInputSchema,
31
+ LibNewInputSchema,
32
+ LibAuditInputSchema,
33
+ DagTraverseInputSchema,
34
+ CoreCatchInputSchema,
35
+ type LibScanInput,
36
+ type LibScanOutput,
37
+ type LibRenameInput,
38
+ type LibRenameOutput,
39
+ type LibNewInput,
40
+ type LibNewOutput,
41
+ type LibAuditInput,
42
+ type LibAuditOutput,
43
+ type DagTraverseInput,
44
+ type DagTraverseOutput,
45
+ type CoreCatchInput,
46
+ type CoreCatchOutput,
47
+ LibScanOutputSchema,
48
+ LibRenameOutputSchema,
49
+ LibNewOutputSchema,
50
+ LibAuditOutputSchema,
51
+ DagTraverseOutputSchema,
52
+ CoreCatchOutputSchema,
53
+ } from "./types.js";
54
+ import type { ProcedureContext } from "@procedures/client";
55
+
56
+ // =============================================================================
57
+ // lib.* Procedures
58
+ // =============================================================================
59
+
60
+ const libScanProcedure = createProcedure()
61
+ .path(["lib", "scan"])
62
+ .input(zodAdapter<LibScanInput>(LibScanInputSchema))
63
+ .output(zodAdapter<LibScanOutput>(LibScanOutputSchema))
64
+ .meta({
65
+ description: "Scan for packages in the git directory",
66
+ calls: ["fs.readdir", "fs.read.json", "git.status", "git.remote"],
67
+ effects: { reads: ["fs"], writes: [] },
68
+ args: [],
69
+ shorts: {},
70
+ output: "json",
71
+ })
72
+ .handler(async (input: LibScanInput, ctx: ProcedureContext): Promise<LibScanOutput> => {
73
+ return libScan(input, ctx);
74
+ })
75
+ .build();
76
+
77
+ const libRenameProcedure = createProcedure()
78
+ .path(["lib", "rename"])
79
+ .input(zodAdapter<LibRenameInput>(LibRenameInputSchema))
80
+ .output(zodAdapter<LibRenameOutput>(LibRenameOutputSchema))
81
+ .meta({
82
+ description: "Rename a package across the ecosystem",
83
+ calls: ["fs.glob", "fs.read.json", "fs.write", "fs.exists"],
84
+ // It rewrites package.json files and TypeScript imports of the whole workspace
85
+ effects: { reads: ["fs"], writes: ["fs"] },
86
+ args: ["oldName", "newName"],
87
+ shorts: { dryRun: "n" },
88
+ output: "json",
89
+ })
90
+ .handler(async (input: LibRenameInput, ctx: ProcedureContext): Promise<LibRenameOutput> => {
91
+ return libRename(input, ctx);
92
+ })
93
+ .build();
94
+
95
+ const libNewProcedure = createProcedure()
96
+ .path(["lib", "new"])
97
+ .input(zodAdapter<LibNewInput>(LibNewInputSchema))
98
+ .output(zodAdapter<LibNewOutput>(LibNewOutputSchema))
99
+ .meta({
100
+ description: "Create a new package in the ecosystem",
101
+ calls: ["shell.run", "fs.exists", "fs.mkdir", "fs.write", "fs.read.json"],
102
+ // It starts the cue-config CLI from the node_modules of the workspace: code of the project,
103
+ // so it writes process. That CLI writes only files of the new package folder. An existing
104
+ // folder is an error, so the procedure overwrites nothing.
105
+ effects: { reads: ["fs"], writes: ["fs", "process"], destructive: false },
106
+ args: ["name"],
107
+ shorts: { dryRun: "n" },
108
+ output: "json",
109
+ })
110
+ .handler(async (input: LibNewInput, ctx: ProcedureContext): Promise<LibNewOutput> => {
111
+ return libNew(input, ctx);
112
+ })
113
+ .build();
114
+
115
+ const libAuditProcedure = createProcedure()
116
+ .path(["lib", "audit"])
117
+ .input(zodAdapter<LibAuditInput>(LibAuditInputSchema))
118
+ .output(zodAdapter<LibAuditOutput>(LibAuditOutputSchema))
119
+ .meta({
120
+ description: "Audit ecosystem packages for issues",
121
+ calls: ["fs.readdir", "fs.read.json", "git.status", "git.remote", "fs.exists", "fs.mkdir", "fs.write"],
122
+ // It reads the registry for the flag notices. With `fix`, it makes only the missing folders
123
+ // and files, so a second call changes nothing more.
124
+ effects: { reads: ["fs", "registry"], writes: ["fs"], destructive: false, idempotent: true },
125
+ args: [],
126
+ shorts: { fix: "f" },
127
+ output: "json",
128
+ })
129
+ .handler(async (input: LibAuditInput, ctx: ProcedureContext): Promise<LibAuditOutput> => {
130
+ return libAudit(input, ctx);
131
+ })
132
+ .build();
133
+
134
+ // =============================================================================
135
+ // ecosystem.* Procedures
136
+ // =============================================================================
137
+
138
+ const ecosystemProceduresProcedure = createProcedure()
139
+ .path(["ecosystem", "procedures"])
140
+ .input(zodAdapter<EcosystemProceduresInput>(EcosystemProceduresInputSchema))
141
+ .output(zodAdapter<EcosystemProceduresOutput>(EcosystemProceduresOutputSchema))
142
+ .meta({
143
+ description: "List all procedures across the ecosystem",
144
+ // It imports the register file of each package: that code runs, and it registers procedures
145
+ effects: { reads: ["fs", "registry"], writes: ["process", "registry"] },
146
+ args: [],
147
+ shorts: { namespace: "n" },
148
+ output: "json",
149
+ })
150
+ .handler(async (input: EcosystemProceduresInput, ctx: ProcedureContext): Promise<EcosystemProceduresOutput> => {
151
+ return ecosystemProcedures(input, ctx);
152
+ })
153
+ .build();
154
+
155
+ // =============================================================================
156
+ // dag.* Procedures
157
+ // =============================================================================
158
+
159
+ const dagTraverseProcedure = createProcedure()
160
+ .path(["dag", "traverse"])
161
+ .input(zodAdapter<DagTraverseInput>(DagTraverseInputSchema))
162
+ .output(zodAdapter<DagTraverseOutput>(DagTraverseOutputSchema))
163
+ .meta({
164
+ description: "Traverse ecosystem packages in dependency order, executing visit procedure for each",
165
+ // It runs the procedure refs of its input: a server with an expose rule limits what they call
166
+ tags: [RUNS_REFS_TAG],
167
+ // Its own effect is the scan of the workspace. The admission examines each visit call.
168
+ effects: { reads: ["fs"], writes: [] },
169
+ args: [],
170
+ shorts: { root: "r", concurrency: "j", continueOnError: "c", dryRun: "d" },
171
+ output: "streaming",
172
+ })
173
+ .handler(async (input: DagTraverseInput, ctx: ProcedureContext): Promise<DagTraverseOutput> => {
174
+ return dagTraverse(input, ctx);
175
+ })
176
+ .build();
177
+
178
+ // =============================================================================
179
+ // core.* Procedures
180
+ // =============================================================================
181
+
182
+ const coreCatchProcedure = createProcedure()
183
+ .path(["core", "catch"])
184
+ .input(zodAdapter<CoreCatchInput>(CoreCatchInputSchema))
185
+ .output(zodAdapter<CoreCatchOutput>(CoreCatchOutputSchema))
186
+ .meta({
187
+ description: "Execute a procedure with error handling",
188
+ // It runs the procedure refs of its input: a server with an expose rule limits what they call
189
+ tags: [RUNS_REFS_TAG],
190
+ // Control flow: no effects of its own. The admission examines each nested call.
191
+ effects: { reads: [], writes: [] },
192
+ args: [],
193
+ shorts: {},
194
+ output: "json",
195
+ })
196
+ .handler(async (input: CoreCatchInput, ctx: ProcedureContext): Promise<CoreCatchOutput> => {
197
+ return coreCatch(input, ctx);
198
+ })
199
+ .build();
200
+
201
+ // =============================================================================
202
+ // Registration
203
+ // =============================================================================
204
+
205
+ /**
206
+ * The module of this package, for `createRegistry()`: its procedures as data. Importing this
207
+ * file registers nothing. (`register.ts` registers the same procedures in the global registry.)
208
+ */
209
+ export const libModule: ProcedureModule = defineModule("@procedures/lib", [
210
+ // lib.* procedures (canonical home)
211
+ libScanProcedure,
212
+ libRenameProcedure,
213
+ libNewProcedure,
214
+ libAuditProcedure,
215
+ // ecosystem.* procedures
216
+ ecosystemProceduresProcedure,
217
+ // dag.* procedures (canonical home)
218
+ dagTraverseProcedure,
219
+ // core.* procedures
220
+ coreCatchProcedure,
221
+ ], [fsModule, gitModule]);
222
+
223
+ export default libModule;
224
+
225
+ /**
226
+ * This function registers the procedures of @procedures/lib, and of the modules that it requires, in
227
+ * a registry. The default is the global registry. Importing this file registers nothing: the
228
+ * `./register` entry starts this function (ARCHITECTURE-PROPOSALS-2026-10, P9).
229
+ */
230
+ export function registerLibProcedures(registry: ProcedureRegistry = PROCEDURE_REGISTRY): void {
231
+ registerModules(registry, [libModule]);
232
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * core.catch procedure
3
+ *
4
+ * Wraps a procedure execution with error handling.
5
+ * The `try` procedure ref should use `$when: "catch"` so it's not
6
+ * executed during hydration but instead by this procedure.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * {
11
+ * $proc: ["core", "catch"],
12
+ * input: {
13
+ * try: {
14
+ * $proc: ["git", "commit"],
15
+ * input: { message: "auto" },
16
+ * $when: "catch" // Defer execution to catch procedure
17
+ * },
18
+ * handler: {
19
+ * $proc: ["core", "identity"],
20
+ * input: { value: { continue: true } },
21
+ * $when: "catch"
22
+ * }
23
+ * }
24
+ * }
25
+ * ```
26
+ */
27
+
28
+ import type { ProcedureContext, ProcedurePath } from "@procedures/client";
29
+ import {
30
+ isAnyProcedureRef,
31
+ normalizeRef,
32
+ hydrateInput,
33
+ scopeOf,
34
+ type StepResultInfo,
35
+ type ContinueDecision,
36
+ } from "@procedures/client";
37
+ import type { CoreCatchInput, CoreCatchOutput } from "../../types.js";
38
+
39
+ /**
40
+ * Execute a procedure with error handling
41
+ */
42
+ export async function coreCatch(
43
+ input: CoreCatchInput,
44
+ ctx: ProcedureContext
45
+ ): Promise<CoreCatchOutput> {
46
+ const { try: tryRef, handler: handlerRef, cwd } = input;
47
+ // The names of the chain around this procedure: a `$ref` in an operand can read them
48
+ const scope = scopeOf(input);
49
+
50
+ // Validate try is a procedure ref
51
+ if (!isAnyProcedureRef(tryRef)) {
52
+ throw new Error("catch: 'try' must be a procedure reference with $when: \"catch\"");
53
+ }
54
+
55
+ // Create executor that uses client.call
56
+ const executor = async <TIn, TOut>(path: ProcedurePath, inp: TIn): Promise<TOut> => {
57
+ // Inject cwd if provided
58
+ const inputWithCwd = cwd && typeof inp === "object" && inp !== null
59
+ ? { ...inp, cwd }
60
+ : inp;
61
+ return ctx.client.call(path, inputWithCwd) as Promise<TOut>;
62
+ };
63
+
64
+ // Normalize the try ref
65
+ const normalizedTry = normalizeRef(tryRef);
66
+
67
+ // Build input for try, merging with cwd
68
+ const tryInput = {
69
+ ...(typeof normalizedTry.input === "object" ? normalizedTry.input : {}),
70
+ ...(cwd ? { cwd } : {}),
71
+ };
72
+
73
+ try {
74
+ // Hydrate the try input (execute any nested $immediate refs). The hydration is inside the
75
+ // `try`: a nested ref that throws is an error of the `try` step (deep dive DATA-22).
76
+ const hydratedTryInput = await hydrateInput(tryInput, executor, {
77
+ contextStack: ["catch"],
78
+ onRef: ctx.onRef,
79
+ scope,
80
+ });
81
+
82
+ // Execute the try procedure
83
+ const result = await ctx.client.call(normalizedTry.path, hydratedTryInput);
84
+
85
+ return {
86
+ success: true,
87
+ result,
88
+ continue: true,
89
+ };
90
+ } catch (error) {
91
+ const errorMessage = error instanceof Error ? error.message : String(error);
92
+
93
+ // If no handler, return error result with continue: false
94
+ if (!handlerRef || !isAnyProcedureRef(handlerRef)) {
95
+ return {
96
+ success: false,
97
+ error: errorMessage,
98
+ continue: false,
99
+ };
100
+ }
101
+
102
+ // Build StepResultInfo for handler
103
+ const resultInfo: StepResultInfo = {
104
+ success: false,
105
+ error: errorMessage,
106
+ proc: normalizedTry.path,
107
+ };
108
+
109
+ // Execute handler with result info merged into its input
110
+ const normalizedHandler = normalizeRef(handlerRef);
111
+ const handlerInput = {
112
+ ...(typeof normalizedHandler.input === "object" ? normalizedHandler.input : {}),
113
+ ...resultInfo,
114
+ ...(cwd ? { cwd } : {}),
115
+ };
116
+
117
+ try {
118
+ // Hydrate handler input (a failure here is a failure of the handler)
119
+ const hydratedHandlerInput = await hydrateInput(handlerInput, executor, {
120
+ contextStack: ["catch"],
121
+ onRef: ctx.onRef,
122
+ scope,
123
+ });
124
+
125
+ const decision = await ctx.client.call(normalizedHandler.path, hydratedHandlerInput) as ContinueDecision;
126
+
127
+ return {
128
+ success: false,
129
+ error: errorMessage,
130
+ result: decision,
131
+ continue: decision.continue ?? false,
132
+ };
133
+ } catch (handlerError) {
134
+ // Handler itself failed
135
+ return {
136
+ success: false,
137
+ error: `Handler failed: ${handlerError instanceof Error ? handlerError.message : String(handlerError)}`,
138
+ continue: false,
139
+ };
140
+ }
141
+ }
142
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Core control-flow procedures that require ProcedureContext
3
+ *
4
+ * These procedures need access to the client for executing procedure refs
5
+ * with proper error handling and control flow.
6
+ */
7
+
8
+ export { coreCatch } from "./catch.js";
9
+ export type { CoreCatchInput, CoreCatchOutput } from "../../types.js";
@@ -0,0 +1,5 @@
1
+ /**
2
+ * DAG Procedures
3
+ */
4
+
5
+ export { dagTraverse } from "./traverse.js";
@@ -0,0 +1,281 @@
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
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * ecosystem procedures
3
+ */
4
+
5
+ export { ecosystemProcedures } from "./procedures.js";
6
+ export type {
7
+ EcosystemProceduresInput,
8
+ EcosystemProceduresOutput,
9
+ ProcedureInfo,
10
+ PackageProcedures,
11
+ } from "./procedures.js";
12
+ export { EcosystemProceduresInputSchema, EcosystemProceduresOutputSchema } from "./procedures.js";