@herdctl/core 0.0.1 → 0.0.2

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 (275) hide show
  1. package/dist/config/__tests__/agent.test.js +31 -13
  2. package/dist/config/__tests__/agent.test.js.map +1 -1
  3. package/dist/config/__tests__/merge.test.js +9 -2
  4. package/dist/config/__tests__/merge.test.js.map +1 -1
  5. package/dist/config/__tests__/schema.test.js +350 -1
  6. package/dist/config/__tests__/schema.test.js.map +1 -1
  7. package/dist/config/index.d.ts +1 -1
  8. package/dist/config/index.d.ts.map +1 -1
  9. package/dist/config/index.js +3 -1
  10. package/dist/config/index.js.map +1 -1
  11. package/dist/config/schema.d.ts +828 -24
  12. package/dist/config/schema.d.ts.map +1 -1
  13. package/dist/config/schema.js +118 -6
  14. package/dist/config/schema.js.map +1 -1
  15. package/dist/fleet-manager/__tests__/coverage.test.js +11 -332
  16. package/dist/fleet-manager/__tests__/coverage.test.js.map +1 -1
  17. package/dist/fleet-manager/__tests__/errors.test.js +1 -49
  18. package/dist/fleet-manager/__tests__/errors.test.js.map +1 -1
  19. package/dist/fleet-manager/__tests__/integration.test.js +109 -0
  20. package/dist/fleet-manager/__tests__/integration.test.js.map +1 -1
  21. package/dist/fleet-manager/__tests__/reload.test.js +1 -1
  22. package/dist/fleet-manager/__tests__/reload.test.js.map +1 -1
  23. package/dist/fleet-manager/config-reload.d.ts +164 -0
  24. package/dist/fleet-manager/config-reload.d.ts.map +1 -0
  25. package/dist/fleet-manager/config-reload.js +445 -0
  26. package/dist/fleet-manager/config-reload.js.map +1 -0
  27. package/dist/fleet-manager/context.d.ts +76 -0
  28. package/dist/fleet-manager/context.d.ts.map +1 -0
  29. package/dist/fleet-manager/context.js +11 -0
  30. package/dist/fleet-manager/context.js.map +1 -0
  31. package/dist/fleet-manager/errors.d.ts +0 -25
  32. package/dist/fleet-manager/errors.d.ts.map +1 -1
  33. package/dist/fleet-manager/errors.js +0 -38
  34. package/dist/fleet-manager/errors.js.map +1 -1
  35. package/dist/fleet-manager/event-emitters.d.ts +123 -0
  36. package/dist/fleet-manager/event-emitters.d.ts.map +1 -0
  37. package/dist/fleet-manager/event-emitters.js +136 -0
  38. package/dist/fleet-manager/event-emitters.js.map +1 -0
  39. package/dist/fleet-manager/event-types.d.ts +0 -15
  40. package/dist/fleet-manager/event-types.d.ts.map +1 -1
  41. package/dist/fleet-manager/fleet-manager.d.ts +40 -653
  42. package/dist/fleet-manager/fleet-manager.d.ts.map +1 -1
  43. package/dist/fleet-manager/fleet-manager.js +95 -1720
  44. package/dist/fleet-manager/fleet-manager.js.map +1 -1
  45. package/dist/fleet-manager/index.d.ts +13 -2
  46. package/dist/fleet-manager/index.d.ts.map +1 -1
  47. package/dist/fleet-manager/index.js +19 -6
  48. package/dist/fleet-manager/index.js.map +1 -1
  49. package/dist/fleet-manager/job-control.d.ts +64 -0
  50. package/dist/fleet-manager/job-control.d.ts.map +1 -0
  51. package/dist/fleet-manager/job-control.js +296 -0
  52. package/dist/fleet-manager/job-control.js.map +1 -0
  53. package/dist/fleet-manager/log-streaming.d.ts +171 -0
  54. package/dist/fleet-manager/log-streaming.d.ts.map +1 -0
  55. package/dist/fleet-manager/log-streaming.js +503 -0
  56. package/dist/fleet-manager/log-streaming.js.map +1 -0
  57. package/dist/fleet-manager/schedule-executor.d.ts +63 -0
  58. package/dist/fleet-manager/schedule-executor.d.ts.map +1 -0
  59. package/dist/fleet-manager/schedule-executor.js +209 -0
  60. package/dist/fleet-manager/schedule-executor.js.map +1 -0
  61. package/dist/fleet-manager/schedule-management.d.ts +71 -0
  62. package/dist/fleet-manager/schedule-management.d.ts.map +1 -0
  63. package/dist/fleet-manager/schedule-management.js +171 -0
  64. package/dist/fleet-manager/schedule-management.js.map +1 -0
  65. package/dist/fleet-manager/status-queries.d.ts +105 -0
  66. package/dist/fleet-manager/status-queries.d.ts.map +1 -0
  67. package/dist/fleet-manager/status-queries.js +247 -0
  68. package/dist/fleet-manager/status-queries.js.map +1 -0
  69. package/dist/fleet-manager/types.d.ts +0 -39
  70. package/dist/fleet-manager/types.d.ts.map +1 -1
  71. package/dist/runner/__tests__/job-executor.test.js +206 -1
  72. package/dist/runner/__tests__/job-executor.test.js.map +1 -1
  73. package/dist/runner/job-executor.d.ts +9 -0
  74. package/dist/runner/job-executor.d.ts.map +1 -1
  75. package/dist/runner/job-executor.js +78 -4
  76. package/dist/runner/job-executor.js.map +1 -1
  77. package/dist/runner/types.d.ts +2 -0
  78. package/dist/runner/types.d.ts.map +1 -1
  79. package/dist/scheduler/__tests__/cron.test.d.ts +2 -0
  80. package/dist/scheduler/__tests__/cron.test.d.ts.map +1 -0
  81. package/dist/scheduler/__tests__/cron.test.js +867 -0
  82. package/dist/scheduler/__tests__/cron.test.js.map +1 -0
  83. package/dist/scheduler/__tests__/scheduler.test.js +164 -5
  84. package/dist/scheduler/__tests__/scheduler.test.js.map +1 -1
  85. package/dist/scheduler/cron.d.ts +126 -0
  86. package/dist/scheduler/cron.d.ts.map +1 -0
  87. package/dist/scheduler/cron.js +390 -0
  88. package/dist/scheduler/cron.js.map +1 -0
  89. package/dist/scheduler/errors.d.ts +81 -1
  90. package/dist/scheduler/errors.d.ts.map +1 -1
  91. package/dist/scheduler/errors.js +81 -6
  92. package/dist/scheduler/errors.js.map +1 -1
  93. package/dist/scheduler/index.d.ts +1 -0
  94. package/dist/scheduler/index.d.ts.map +1 -1
  95. package/dist/scheduler/index.js +2 -0
  96. package/dist/scheduler/index.js.map +1 -1
  97. package/dist/scheduler/schedule-runner.d.ts +2 -2
  98. package/dist/scheduler/schedule-runner.d.ts.map +1 -1
  99. package/dist/scheduler/schedule-runner.js +20 -8
  100. package/dist/scheduler/schedule-runner.js.map +1 -1
  101. package/dist/scheduler/scheduler.d.ts +4 -4
  102. package/dist/scheduler/scheduler.d.ts.map +1 -1
  103. package/dist/scheduler/scheduler.js +86 -20
  104. package/dist/scheduler/scheduler.js.map +1 -1
  105. package/dist/scheduler/types.d.ts +1 -1
  106. package/dist/scheduler/types.d.ts.map +1 -1
  107. package/dist/state/schemas/job-metadata.d.ts +2 -2
  108. package/package.json +33 -8
  109. package/.turbo/turbo-build.log +0 -4
  110. package/.turbo/turbo-test.log +0 -219
  111. package/.turbo/turbo-typecheck.log +0 -4
  112. package/coverage/base.css +0 -224
  113. package/coverage/block-navigation.js +0 -87
  114. package/coverage/coverage-final.json +0 -51
  115. package/coverage/favicon.png +0 -0
  116. package/coverage/index.html +0 -251
  117. package/coverage/prettify.css +0 -1
  118. package/coverage/prettify.js +0 -2
  119. package/coverage/sort-arrow-sprite.png +0 -0
  120. package/coverage/sorter.js +0 -210
  121. package/coverage/src/config/index.html +0 -191
  122. package/coverage/src/config/index.ts.html +0 -442
  123. package/coverage/src/config/interpolate.ts.html +0 -652
  124. package/coverage/src/config/loader.ts.html +0 -1501
  125. package/coverage/src/config/merge.ts.html +0 -823
  126. package/coverage/src/config/parser.ts.html +0 -1213
  127. package/coverage/src/config/schema.ts.html +0 -1123
  128. package/coverage/src/fleet-manager/errors.ts.html +0 -2326
  129. package/coverage/src/fleet-manager/event-types.ts.html +0 -1219
  130. package/coverage/src/fleet-manager/fleet-manager.ts.html +0 -7030
  131. package/coverage/src/fleet-manager/index.html +0 -206
  132. package/coverage/src/fleet-manager/index.ts.html +0 -469
  133. package/coverage/src/fleet-manager/job-manager.ts.html +0 -2074
  134. package/coverage/src/fleet-manager/job-queue.ts.html +0 -2479
  135. package/coverage/src/fleet-manager/types.ts.html +0 -2602
  136. package/coverage/src/index.html +0 -116
  137. package/coverage/src/index.ts.html +0 -181
  138. package/coverage/src/runner/errors.ts.html +0 -1006
  139. package/coverage/src/runner/index.html +0 -191
  140. package/coverage/src/runner/index.ts.html +0 -256
  141. package/coverage/src/runner/job-executor.ts.html +0 -1429
  142. package/coverage/src/runner/message-processor.ts.html +0 -1150
  143. package/coverage/src/runner/sdk-adapter.ts.html +0 -658
  144. package/coverage/src/runner/types.ts.html +0 -559
  145. package/coverage/src/scheduler/errors.ts.html +0 -388
  146. package/coverage/src/scheduler/index.html +0 -206
  147. package/coverage/src/scheduler/index.ts.html +0 -244
  148. package/coverage/src/scheduler/interval.ts.html +0 -652
  149. package/coverage/src/scheduler/schedule-runner.ts.html +0 -1411
  150. package/coverage/src/scheduler/schedule-state.ts.html +0 -718
  151. package/coverage/src/scheduler/scheduler.ts.html +0 -1795
  152. package/coverage/src/scheduler/types.ts.html +0 -733
  153. package/coverage/src/state/directory.ts.html +0 -736
  154. package/coverage/src/state/errors.ts.html +0 -376
  155. package/coverage/src/state/fleet-state.ts.html +0 -937
  156. package/coverage/src/state/index.html +0 -221
  157. package/coverage/src/state/index.ts.html +0 -322
  158. package/coverage/src/state/job-metadata.ts.html +0 -1420
  159. package/coverage/src/state/job-output.ts.html +0 -1033
  160. package/coverage/src/state/schemas/fleet-state.ts.html +0 -445
  161. package/coverage/src/state/schemas/index.html +0 -176
  162. package/coverage/src/state/schemas/index.ts.html +0 -286
  163. package/coverage/src/state/schemas/job-metadata.ts.html +0 -628
  164. package/coverage/src/state/schemas/job-output.ts.html +0 -616
  165. package/coverage/src/state/schemas/session-info.ts.html +0 -361
  166. package/coverage/src/state/session.ts.html +0 -844
  167. package/coverage/src/state/types.ts.html +0 -262
  168. package/coverage/src/state/utils/atomic.ts.html +0 -748
  169. package/coverage/src/state/utils/index.html +0 -146
  170. package/coverage/src/state/utils/index.ts.html +0 -103
  171. package/coverage/src/state/utils/reads.ts.html +0 -1621
  172. package/coverage/src/work-sources/adapters/github.ts.html +0 -3583
  173. package/coverage/src/work-sources/adapters/index.html +0 -131
  174. package/coverage/src/work-sources/adapters/index.ts.html +0 -277
  175. package/coverage/src/work-sources/errors.ts.html +0 -298
  176. package/coverage/src/work-sources/index.html +0 -176
  177. package/coverage/src/work-sources/index.ts.html +0 -529
  178. package/coverage/src/work-sources/manager.ts.html +0 -1324
  179. package/coverage/src/work-sources/registry.ts.html +0 -619
  180. package/coverage/src/work-sources/types.ts.html +0 -568
  181. package/dist/fleet-manager/__tests__/event-helpers.test.d.ts +0 -7
  182. package/dist/fleet-manager/__tests__/event-helpers.test.d.ts.map +0 -1
  183. package/dist/fleet-manager/__tests__/event-helpers.test.js +0 -368
  184. package/dist/fleet-manager/__tests__/event-helpers.test.js.map +0 -1
  185. package/src/config/__tests__/agent.test.ts +0 -864
  186. package/src/config/__tests__/interpolate.test.ts +0 -644
  187. package/src/config/__tests__/loader.test.ts +0 -784
  188. package/src/config/__tests__/merge.test.ts +0 -751
  189. package/src/config/__tests__/parser.test.ts +0 -533
  190. package/src/config/__tests__/schema.test.ts +0 -873
  191. package/src/config/index.ts +0 -119
  192. package/src/config/interpolate.ts +0 -189
  193. package/src/config/loader.ts +0 -472
  194. package/src/config/merge.ts +0 -246
  195. package/src/config/parser.ts +0 -376
  196. package/src/config/schema.ts +0 -346
  197. package/src/fleet-manager/__tests__/coverage.test.ts +0 -2869
  198. package/src/fleet-manager/__tests__/errors.test.ts +0 -660
  199. package/src/fleet-manager/__tests__/event-helpers.test.ts +0 -448
  200. package/src/fleet-manager/__tests__/integration.test.ts +0 -1209
  201. package/src/fleet-manager/__tests__/job-control.test.ts +0 -283
  202. package/src/fleet-manager/__tests__/job-manager.test.ts +0 -869
  203. package/src/fleet-manager/__tests__/job-queue.test.ts +0 -401
  204. package/src/fleet-manager/__tests__/reload.test.ts +0 -751
  205. package/src/fleet-manager/__tests__/status-queries.test.ts +0 -595
  206. package/src/fleet-manager/__tests__/trigger.test.ts +0 -601
  207. package/src/fleet-manager/errors.ts +0 -747
  208. package/src/fleet-manager/event-types.ts +0 -378
  209. package/src/fleet-manager/fleet-manager.ts +0 -2315
  210. package/src/fleet-manager/index.ts +0 -128
  211. package/src/fleet-manager/job-manager.ts +0 -663
  212. package/src/fleet-manager/job-queue.ts +0 -798
  213. package/src/fleet-manager/types.ts +0 -839
  214. package/src/index.ts +0 -32
  215. package/src/runner/__tests__/errors.test.ts +0 -382
  216. package/src/runner/__tests__/job-executor.test.ts +0 -1708
  217. package/src/runner/__tests__/message-processor.test.ts +0 -960
  218. package/src/runner/__tests__/sdk-adapter.test.ts +0 -626
  219. package/src/runner/errors.ts +0 -307
  220. package/src/runner/index.ts +0 -57
  221. package/src/runner/job-executor.ts +0 -448
  222. package/src/runner/message-processor.ts +0 -355
  223. package/src/runner/sdk-adapter.ts +0 -191
  224. package/src/runner/types.ts +0 -158
  225. package/src/scheduler/__tests__/errors.test.ts +0 -159
  226. package/src/scheduler/__tests__/interval.test.ts +0 -515
  227. package/src/scheduler/__tests__/schedule-runner.test.ts +0 -798
  228. package/src/scheduler/__tests__/schedule-state.test.ts +0 -671
  229. package/src/scheduler/__tests__/scheduler.test.ts +0 -1280
  230. package/src/scheduler/errors.ts +0 -101
  231. package/src/scheduler/index.ts +0 -53
  232. package/src/scheduler/interval.ts +0 -189
  233. package/src/scheduler/schedule-runner.ts +0 -442
  234. package/src/scheduler/schedule-state.ts +0 -211
  235. package/src/scheduler/scheduler.ts +0 -570
  236. package/src/scheduler/types.ts +0 -216
  237. package/src/state/__tests__/directory.test.ts +0 -595
  238. package/src/state/__tests__/fleet-state.test.ts +0 -868
  239. package/src/state/__tests__/job-metadata-schema.test.ts +0 -414
  240. package/src/state/__tests__/job-metadata.test.ts +0 -831
  241. package/src/state/__tests__/job-output.test.ts +0 -856
  242. package/src/state/__tests__/session-schema.test.ts +0 -378
  243. package/src/state/__tests__/session.test.ts +0 -604
  244. package/src/state/directory.ts +0 -217
  245. package/src/state/errors.ts +0 -97
  246. package/src/state/fleet-state.ts +0 -284
  247. package/src/state/index.ts +0 -79
  248. package/src/state/job-metadata.ts +0 -445
  249. package/src/state/job-output.ts +0 -316
  250. package/src/state/schemas/__tests__/job-output.test.ts +0 -338
  251. package/src/state/schemas/fleet-state.ts +0 -120
  252. package/src/state/schemas/index.ts +0 -67
  253. package/src/state/schemas/job-metadata.ts +0 -181
  254. package/src/state/schemas/job-output.ts +0 -177
  255. package/src/state/schemas/session-info.ts +0 -92
  256. package/src/state/session.ts +0 -253
  257. package/src/state/types.ts +0 -59
  258. package/src/state/utils/__tests__/atomic.test.ts +0 -723
  259. package/src/state/utils/__tests__/reads.test.ts +0 -1071
  260. package/src/state/utils/atomic.ts +0 -221
  261. package/src/state/utils/index.ts +0 -6
  262. package/src/state/utils/reads.ts +0 -512
  263. package/src/work-sources/__tests__/github.test.ts +0 -1800
  264. package/src/work-sources/__tests__/manager.test.ts +0 -529
  265. package/src/work-sources/__tests__/registry.test.ts +0 -477
  266. package/src/work-sources/__tests__/types.test.ts +0 -479
  267. package/src/work-sources/adapters/github.ts +0 -1166
  268. package/src/work-sources/adapters/index.ts +0 -64
  269. package/src/work-sources/errors.ts +0 -71
  270. package/src/work-sources/index.ts +0 -148
  271. package/src/work-sources/manager.ts +0 -413
  272. package/src/work-sources/registry.ts +0 -178
  273. package/src/work-sources/types.ts +0 -161
  274. package/tsconfig.json +0 -9
  275. package/vitest.config.ts +0 -19
@@ -1,472 +0,0 @@
1
- /**
2
- * Configuration loader for herdctl
3
- *
4
- * Provides a single entry point to load and resolve all configuration:
5
- * - Auto-discovers herdctl.yaml by walking up the directory tree
6
- * - Loads fleet config and all referenced agent configs
7
- * - Merges fleet defaults into agent configs
8
- * - Interpolates environment variables
9
- * - Validates the entire configuration tree
10
- */
11
-
12
- import { readFile, access } from "node:fs/promises";
13
- import { dirname, join, resolve } from "node:path";
14
- import { parse as parseYaml, YAMLParseError } from "yaml";
15
- import { ZodError } from "zod";
16
- import {
17
- FleetConfigSchema,
18
- AgentConfigSchema,
19
- type FleetConfig,
20
- type AgentConfig,
21
- } from "./schema.js";
22
- import { ConfigError, FileReadError, SchemaValidationError } from "./parser.js";
23
- import { mergeAgentConfig, type ExtendedDefaults } from "./merge.js";
24
- import { interpolateConfig, type InterpolateOptions } from "./interpolate.js";
25
-
26
- // =============================================================================
27
- // Constants
28
- // =============================================================================
29
-
30
- /**
31
- * Default config file names to search for
32
- */
33
- export const CONFIG_FILE_NAMES = ["herdctl.yaml", "herdctl.yml"] as const;
34
-
35
- // =============================================================================
36
- // Error Classes
37
- // =============================================================================
38
-
39
- /**
40
- * Error thrown when no configuration file is found
41
- */
42
- export class ConfigNotFoundError extends ConfigError {
43
- public readonly searchedPaths: string[];
44
- public readonly startDirectory: string;
45
-
46
- constructor(startDirectory: string, searchedPaths: string[]) {
47
- super(
48
- `No herdctl configuration file found. ` +
49
- `Searched from '${startDirectory}' up to filesystem root. ` +
50
- `Create a herdctl.yaml file to get started.`
51
- );
52
- this.name = "ConfigNotFoundError";
53
- this.searchedPaths = searchedPaths;
54
- this.startDirectory = startDirectory;
55
- }
56
- }
57
-
58
- /**
59
- * Error thrown when agent loading fails
60
- */
61
- export class AgentLoadError extends ConfigError {
62
- public readonly agentPath: string;
63
- public readonly agentName?: string;
64
-
65
- constructor(agentPath: string, cause: Error, agentName?: string) {
66
- const nameInfo = agentName ? ` (${agentName})` : "";
67
- super(`Failed to load agent '${agentPath}'${nameInfo}: ${cause.message}`);
68
- this.name = "AgentLoadError";
69
- this.agentPath = agentPath;
70
- this.agentName = agentName;
71
- this.cause = cause;
72
- }
73
- }
74
-
75
- // =============================================================================
76
- // Types
77
- // =============================================================================
78
-
79
- /**
80
- * A fully resolved agent configuration with computed properties
81
- */
82
- export interface ResolvedAgent extends AgentConfig {
83
- /**
84
- * The absolute path to the agent configuration file
85
- */
86
- configPath: string;
87
- }
88
-
89
- /**
90
- * A fully resolved configuration with all agents loaded and merged
91
- */
92
- export interface ResolvedConfig {
93
- /**
94
- * The parsed and validated fleet configuration
95
- */
96
- fleet: FleetConfig;
97
-
98
- /**
99
- * All agent configurations, fully resolved with defaults merged
100
- */
101
- agents: ResolvedAgent[];
102
-
103
- /**
104
- * The absolute path to the fleet configuration file
105
- */
106
- configPath: string;
107
-
108
- /**
109
- * The directory containing the fleet configuration
110
- */
111
- configDir: string;
112
- }
113
-
114
- /**
115
- * Options for the loadConfig function
116
- */
117
- export interface LoadConfigOptions {
118
- /**
119
- * Custom environment variables for interpolation
120
- * Defaults to process.env
121
- */
122
- env?: Record<string, string | undefined>;
123
-
124
- /**
125
- * Whether to interpolate environment variables
126
- * Defaults to true
127
- */
128
- interpolate?: boolean;
129
-
130
- /**
131
- * Whether to merge fleet defaults into agent configs
132
- * Defaults to true
133
- */
134
- mergeDefaults?: boolean;
135
- }
136
-
137
- // =============================================================================
138
- // File Discovery
139
- // =============================================================================
140
-
141
- /**
142
- * Check if a file exists and is accessible
143
- */
144
- async function fileExists(filePath: string): Promise<boolean> {
145
- try {
146
- await access(filePath);
147
- return true;
148
- } catch {
149
- return false;
150
- }
151
- }
152
-
153
- /**
154
- * Find a configuration file by walking up the directory tree
155
- *
156
- * Searches for herdctl.yaml or herdctl.yml starting from the given directory
157
- * and walking up to the filesystem root (similar to how git finds .git).
158
- *
159
- * @param startDir - The directory to start searching from
160
- * @returns The absolute path to the config file, or null if not found
161
- */
162
- export async function findConfigFile(
163
- startDir: string
164
- ): Promise<{ path: string; searchedPaths: string[] } | null> {
165
- const searchedPaths: string[] = [];
166
- let currentDir = resolve(startDir);
167
-
168
- while (true) {
169
- // Check for each possible config file name
170
- for (const fileName of CONFIG_FILE_NAMES) {
171
- const configPath = join(currentDir, fileName);
172
- searchedPaths.push(configPath);
173
-
174
- if (await fileExists(configPath)) {
175
- return { path: configPath, searchedPaths };
176
- }
177
- }
178
-
179
- // Move up to parent directory
180
- const parentDir = dirname(currentDir);
181
-
182
- // Stop if we've reached the root
183
- if (parentDir === currentDir) {
184
- return null;
185
- }
186
-
187
- currentDir = parentDir;
188
- }
189
- }
190
-
191
- // =============================================================================
192
- // Internal Parsing Functions
193
- // =============================================================================
194
-
195
- /**
196
- * Parse and validate fleet config from YAML content
197
- */
198
- function parseFleetYaml(content: string, filePath: string): FleetConfig {
199
- let rawConfig: unknown;
200
- try {
201
- rawConfig = parseYaml(content);
202
- } catch (error) {
203
- if (error instanceof YAMLParseError) {
204
- const position = error.linePos?.[0];
205
- const locationInfo = position
206
- ? ` at line ${position.line}, column ${position.col}`
207
- : "";
208
- throw new ConfigError(
209
- `Invalid YAML syntax in '${filePath}'${locationInfo}: ${error.message}`
210
- );
211
- }
212
- throw error;
213
- }
214
-
215
- // Handle empty files
216
- if (rawConfig === null || rawConfig === undefined) {
217
- rawConfig = {};
218
- }
219
-
220
- try {
221
- return FleetConfigSchema.parse(rawConfig);
222
- } catch (error) {
223
- if (error instanceof ZodError) {
224
- throw new SchemaValidationError(error);
225
- }
226
- throw error;
227
- }
228
- }
229
-
230
- /**
231
- * Parse and validate agent config from YAML content
232
- */
233
- function parseAgentYaml(content: string, filePath: string): AgentConfig {
234
- let rawConfig: unknown;
235
- try {
236
- rawConfig = parseYaml(content);
237
- } catch (error) {
238
- if (error instanceof YAMLParseError) {
239
- const position = error.linePos?.[0];
240
- const locationInfo = position
241
- ? ` at line ${position.line}, column ${position.col}`
242
- : "";
243
- throw new ConfigError(
244
- `Invalid YAML syntax in '${filePath}'${locationInfo}: ${error.message}`
245
- );
246
- }
247
- throw error;
248
- }
249
-
250
- // Handle empty files
251
- if (rawConfig === null || rawConfig === undefined) {
252
- rawConfig = {};
253
- }
254
-
255
- try {
256
- return AgentConfigSchema.parse(rawConfig);
257
- } catch (error) {
258
- if (error instanceof ZodError) {
259
- const issues = error.issues.map((issue) => ({
260
- path: issue.path.join(".") || "(root)",
261
- message: issue.message,
262
- }));
263
- const issueMessages = issues
264
- .map((i) => ` - ${i.path}: ${i.message}`)
265
- .join("\n");
266
- throw new ConfigError(
267
- `Agent configuration validation failed in '${filePath}':\n${issueMessages}`
268
- );
269
- }
270
- throw error;
271
- }
272
- }
273
-
274
- /**
275
- * Resolve an agent path relative to the fleet config directory
276
- */
277
- function resolveAgentPath(agentPath: string, fleetConfigDir: string): string {
278
- if (agentPath.startsWith("/")) {
279
- return agentPath;
280
- }
281
- return resolve(fleetConfigDir, agentPath);
282
- }
283
-
284
- // =============================================================================
285
- // Main Loading Function
286
- // =============================================================================
287
-
288
- /**
289
- * Load complete configuration from a file path or by auto-discovery
290
- *
291
- * This function:
292
- * 1. Finds the config file (if not provided, searches up directory tree)
293
- * 2. Parses and validates the fleet configuration
294
- * 3. Loads and validates all referenced agent configurations
295
- * 4. Interpolates environment variables (optional)
296
- * 5. Merges fleet defaults into agent configs (optional)
297
- * 6. Returns a fully resolved configuration object
298
- *
299
- * @param configPath - Path to herdctl.yaml, or directory to search from.
300
- * If not provided, searches from current working directory.
301
- * @param options - Loading options
302
- * @returns A fully resolved configuration
303
- * @throws {ConfigNotFoundError} If no config file is found
304
- * @throws {FileReadError} If a config file cannot be read
305
- * @throws {ConfigError} If YAML syntax is invalid
306
- * @throws {SchemaValidationError} If configuration fails validation
307
- * @throws {AgentLoadError} If an agent configuration fails to load
308
- *
309
- * @example
310
- * ```typescript
311
- * // Auto-discover config file
312
- * const config = await loadConfig();
313
- *
314
- * // Load from specific path
315
- * const config = await loadConfig("./my-project/herdctl.yaml");
316
- *
317
- * // Load from specific directory
318
- * const config = await loadConfig("./my-project");
319
- *
320
- * // Load without environment interpolation
321
- * const config = await loadConfig(undefined, { interpolate: false });
322
- * ```
323
- */
324
- export async function loadConfig(
325
- configPath?: string,
326
- options: LoadConfigOptions = {}
327
- ): Promise<ResolvedConfig> {
328
- const {
329
- env = process.env,
330
- interpolate = true,
331
- mergeDefaults = true,
332
- } = options;
333
-
334
- // Determine the config file path
335
- let resolvedConfigPath: string;
336
- let searchedPaths: string[] = [];
337
-
338
- if (configPath) {
339
- // Check if it's a file or directory
340
- const isYamlFile =
341
- configPath.endsWith(".yaml") || configPath.endsWith(".yml");
342
-
343
- if (isYamlFile) {
344
- // Treat as direct file path
345
- resolvedConfigPath = resolve(configPath);
346
- } else {
347
- // Treat as directory - search from there
348
- const found = await findConfigFile(configPath);
349
- if (!found) {
350
- throw new ConfigNotFoundError(configPath, searchedPaths);
351
- }
352
- resolvedConfigPath = found.path;
353
- searchedPaths = found.searchedPaths;
354
- }
355
- } else {
356
- // Auto-discover from current working directory
357
- const found = await findConfigFile(process.cwd());
358
- if (!found) {
359
- throw new ConfigNotFoundError(process.cwd(), searchedPaths);
360
- }
361
- resolvedConfigPath = found.path;
362
- searchedPaths = found.searchedPaths;
363
- }
364
-
365
- // Read the fleet config file
366
- let fleetContent: string;
367
- try {
368
- fleetContent = await readFile(resolvedConfigPath, "utf-8");
369
- } catch (error) {
370
- throw new FileReadError(
371
- resolvedConfigPath,
372
- error instanceof Error ? error : undefined
373
- );
374
- }
375
-
376
- // Parse the fleet config
377
- let fleetConfig = parseFleetYaml(fleetContent, resolvedConfigPath);
378
-
379
- // Interpolate environment variables in fleet config
380
- if (interpolate) {
381
- fleetConfig = interpolateConfig(fleetConfig, { env });
382
- }
383
-
384
- const configDir = dirname(resolvedConfigPath);
385
-
386
- // Load all agent configs
387
- const agents: ResolvedAgent[] = [];
388
-
389
- // FleetConfigSchema has default of [], so agents is always defined
390
- const agentRefs = fleetConfig.agents;
391
-
392
- for (const agentRef of agentRefs) {
393
- const agentPath = resolveAgentPath(agentRef.path, configDir);
394
-
395
- // Read agent config file
396
- let agentContent: string;
397
- try {
398
- agentContent = await readFile(agentPath, "utf-8");
399
- } catch (error) {
400
- throw new AgentLoadError(
401
- agentRef.path,
402
- new FileReadError(agentPath, error instanceof Error ? error : undefined)
403
- );
404
- }
405
-
406
- // Parse and validate agent config
407
- let agentConfig: AgentConfig;
408
- try {
409
- agentConfig = parseAgentYaml(agentContent, agentPath);
410
- } catch (error) {
411
- throw new AgentLoadError(
412
- agentRef.path,
413
- error instanceof Error ? error : new Error(String(error))
414
- );
415
- }
416
-
417
- // Interpolate environment variables in agent config
418
- if (interpolate) {
419
- agentConfig = interpolateConfig(agentConfig, { env });
420
- }
421
-
422
- // Merge fleet defaults into agent config
423
- if (mergeDefaults && fleetConfig.defaults) {
424
- agentConfig = mergeAgentConfig(
425
- fleetConfig.defaults as ExtendedDefaults,
426
- agentConfig
427
- );
428
- }
429
-
430
- agents.push({
431
- ...agentConfig,
432
- configPath: agentPath,
433
- });
434
- }
435
-
436
- return {
437
- fleet: fleetConfig,
438
- agents,
439
- configPath: resolvedConfigPath,
440
- configDir,
441
- };
442
- }
443
-
444
- /**
445
- * Load configuration without throwing on errors
446
- *
447
- * @param configPath - Path to herdctl.yaml or directory to search from
448
- * @param options - Loading options
449
- * @returns Success result with config, or failure result with error
450
- */
451
- export async function safeLoadConfig(
452
- configPath?: string,
453
- options: LoadConfigOptions = {}
454
- ): Promise<
455
- | { success: true; data: ResolvedConfig }
456
- | { success: false; error: ConfigError }
457
- > {
458
- try {
459
- const config = await loadConfig(configPath, options);
460
- return { success: true, data: config };
461
- } catch (error) {
462
- if (error instanceof ConfigError) {
463
- return { success: false, error };
464
- }
465
- return {
466
- success: false,
467
- error: new ConfigError(
468
- error instanceof Error ? error.message : String(error)
469
- ),
470
- };
471
- }
472
- }
@@ -1,246 +0,0 @@
1
- /**
2
- * Deep merge utilities for herdctl configuration
3
- *
4
- * Provides functions to merge fleet-level defaults with agent-specific overrides.
5
- * - Nested objects merge recursively
6
- * - Arrays are replaced, not merged (agent's array replaces defaults)
7
- * - Agent-specific values override fleet defaults
8
- */
9
-
10
- import type {
11
- AgentConfig,
12
- Defaults,
13
- WorkSource,
14
- Session,
15
- Docker,
16
- PermissionMode,
17
- BashPermissions,
18
- } from "./schema.js";
19
-
20
- // =============================================================================
21
- // Input Types (for merging - fields are optional before Zod applies defaults)
22
- // =============================================================================
23
-
24
- /**
25
- * Permissions input type - all fields optional before Zod applies defaults
26
- */
27
- export interface PermissionsInput {
28
- mode?: PermissionMode;
29
- allowed_tools?: string[];
30
- denied_tools?: string[];
31
- bash?: BashPermissions;
32
- }
33
-
34
- // =============================================================================
35
- // Type Guards
36
- // =============================================================================
37
-
38
- /**
39
- * Check if a value is a plain object (not an array, null, or other type)
40
- */
41
- function isPlainObject(value: unknown): value is Record<string, unknown> {
42
- return (
43
- typeof value === "object" &&
44
- value !== null &&
45
- !Array.isArray(value) &&
46
- Object.prototype.toString.call(value) === "[object Object]"
47
- );
48
- }
49
-
50
- // =============================================================================
51
- // Deep Merge
52
- // =============================================================================
53
-
54
- /**
55
- * Deep merge two objects. The override object's values take precedence.
56
- * Arrays are replaced entirely (not merged).
57
- * Objects are merged recursively.
58
- *
59
- * @param base - The base object (defaults)
60
- * @param override - The override object (agent-specific)
61
- * @returns A new merged object
62
- */
63
- export function deepMerge<T extends Record<string, unknown>>(
64
- base: T | undefined,
65
- override: T | undefined
66
- ): T | undefined {
67
- // If base is undefined, return override
68
- if (base === undefined) {
69
- return override;
70
- }
71
-
72
- // If override is undefined, return base
73
- if (override === undefined) {
74
- return base;
75
- }
76
-
77
- // Create a new object to hold the result
78
- const result = { ...base } as Record<string, unknown>;
79
-
80
- // Iterate over the override object
81
- for (const key of Object.keys(override)) {
82
- const baseValue = base[key];
83
- const overrideValue = override[key];
84
-
85
- // If the override value is undefined, skip it (keep base value)
86
- if (overrideValue === undefined) {
87
- continue;
88
- }
89
-
90
- // If the override value is an array, replace entirely
91
- if (Array.isArray(overrideValue)) {
92
- result[key] = overrideValue;
93
- continue;
94
- }
95
-
96
- // If both values are plain objects, merge recursively
97
- if (isPlainObject(baseValue) && isPlainObject(overrideValue)) {
98
- result[key] = deepMerge(baseValue, overrideValue);
99
- continue;
100
- }
101
-
102
- // Otherwise, override value takes precedence
103
- result[key] = overrideValue;
104
- }
105
-
106
- return result as T;
107
- }
108
-
109
- // =============================================================================
110
- // Agent Config Merge Types
111
- // =============================================================================
112
-
113
- /**
114
- * The fields from fleet defaults that can be merged into agent config
115
- */
116
- export interface MergeableDefaults {
117
- permissions?: PermissionsInput;
118
- work_source?: WorkSource;
119
- session?: Session;
120
- docker?: Docker;
121
- model?: string;
122
- max_turns?: number;
123
- permission_mode?: PermissionMode;
124
- }
125
-
126
- /**
127
- * Extended defaults schema that includes all mergeable fields.
128
- * Uses input types (with optional fields) since these are pre-validation values.
129
- */
130
- export interface ExtendedDefaults {
131
- docker?: Docker;
132
- permissions?: PermissionsInput;
133
- work_source?: WorkSource;
134
- instances?: { max_concurrent?: number };
135
- session?: Session;
136
- model?: string;
137
- max_turns?: number;
138
- permission_mode?: PermissionMode;
139
- }
140
-
141
- // =============================================================================
142
- // Agent Config Merge
143
- // =============================================================================
144
-
145
- /**
146
- * Merge fleet defaults into an agent configuration.
147
- *
148
- * The merge applies to the following fields:
149
- * - permissions: Deep merged (agent overrides fleet defaults)
150
- * - work_source: Deep merged
151
- * - session: Deep merged
152
- * - docker: Deep merged
153
- * - model: Agent value overrides default
154
- * - max_turns: Agent value overrides default
155
- * - permission_mode: Agent value overrides default
156
- *
157
- * Arrays within these objects (e.g., allowed_tools) are replaced, not merged.
158
- *
159
- * @param defaults - The fleet-level defaults
160
- * @param agent - The agent-specific configuration
161
- * @returns A new agent configuration with defaults merged in
162
- */
163
- export function mergeAgentConfig(
164
- defaults: ExtendedDefaults | undefined,
165
- agent: AgentConfig
166
- ): AgentConfig {
167
- // If no defaults, return agent as-is
168
- if (!defaults) {
169
- return agent;
170
- }
171
-
172
- // Start with the agent config
173
- const result: AgentConfig = { ...agent };
174
-
175
- // Merge permissions (deep merge)
176
- if (defaults.permissions || agent.permissions) {
177
- result.permissions = deepMerge(
178
- defaults.permissions as Record<string, unknown> | undefined,
179
- agent.permissions as Record<string, unknown> | undefined
180
- ) as AgentConfig["permissions"];
181
- }
182
-
183
- // Merge work_source (deep merge)
184
- if (defaults.work_source || agent.work_source) {
185
- result.work_source = deepMerge(
186
- defaults.work_source as Record<string, unknown> | undefined,
187
- agent.work_source as Record<string, unknown> | undefined
188
- ) as AgentConfig["work_source"];
189
- }
190
-
191
- // Merge session (deep merge)
192
- if (defaults.session || agent.session) {
193
- result.session = deepMerge(
194
- defaults.session as Record<string, unknown> | undefined,
195
- agent.session as Record<string, unknown> | undefined
196
- ) as AgentConfig["session"];
197
- }
198
-
199
- // Merge docker (deep merge)
200
- if (defaults.docker || agent.docker) {
201
- result.docker = deepMerge(
202
- defaults.docker as Record<string, unknown> | undefined,
203
- agent.docker as Record<string, unknown> | undefined
204
- ) as AgentConfig["docker"];
205
- }
206
-
207
- // Merge instances (deep merge)
208
- if (defaults.instances || agent.instances) {
209
- result.instances = deepMerge(
210
- defaults.instances as Record<string, unknown> | undefined,
211
- agent.instances as Record<string, unknown> | undefined
212
- ) as AgentConfig["instances"];
213
- }
214
-
215
- // Merge scalar values (agent takes precedence if defined)
216
- if (defaults.model !== undefined && result.model === undefined) {
217
- result.model = defaults.model;
218
- }
219
-
220
- if (defaults.max_turns !== undefined && result.max_turns === undefined) {
221
- result.max_turns = defaults.max_turns;
222
- }
223
-
224
- if (
225
- defaults.permission_mode !== undefined &&
226
- result.permission_mode === undefined
227
- ) {
228
- result.permission_mode = defaults.permission_mode;
229
- }
230
-
231
- return result;
232
- }
233
-
234
- /**
235
- * Merge fleet defaults into multiple agent configurations.
236
- *
237
- * @param defaults - The fleet-level defaults
238
- * @param agents - Array of agent configurations
239
- * @returns Array of merged agent configurations
240
- */
241
- export function mergeAllAgentConfigs(
242
- defaults: ExtendedDefaults | undefined,
243
- agents: AgentConfig[]
244
- ): AgentConfig[] {
245
- return agents.map((agent) => mergeAgentConfig(defaults, agent));
246
- }