@herdctl/core 5.5.0 → 5.6.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 (119) hide show
  1. package/dist/config/index.d.ts +1 -1
  2. package/dist/config/index.d.ts.map +1 -1
  3. package/dist/config/index.js +3 -1
  4. package/dist/config/index.js.map +1 -1
  5. package/dist/distribution/__tests__/agent-discovery.test.d.ts +7 -0
  6. package/dist/distribution/__tests__/agent-discovery.test.d.ts.map +1 -0
  7. package/dist/distribution/__tests__/agent-discovery.test.js +443 -0
  8. package/dist/distribution/__tests__/agent-discovery.test.js.map +1 -0
  9. package/dist/distribution/__tests__/agent-info.test.d.ts +7 -0
  10. package/dist/distribution/__tests__/agent-info.test.d.ts.map +1 -0
  11. package/dist/distribution/__tests__/agent-info.test.js +568 -0
  12. package/dist/distribution/__tests__/agent-info.test.js.map +1 -0
  13. package/dist/distribution/__tests__/agent-remover.test.d.ts +7 -0
  14. package/dist/distribution/__tests__/agent-remover.test.d.ts.map +1 -0
  15. package/dist/distribution/__tests__/agent-remover.test.js +498 -0
  16. package/dist/distribution/__tests__/agent-remover.test.js.map +1 -0
  17. package/dist/distribution/__tests__/agent-repo-metadata.test.d.ts +5 -0
  18. package/dist/distribution/__tests__/agent-repo-metadata.test.d.ts.map +1 -0
  19. package/dist/distribution/__tests__/agent-repo-metadata.test.js +500 -0
  20. package/dist/distribution/__tests__/agent-repo-metadata.test.js.map +1 -0
  21. package/dist/distribution/__tests__/env-scanner.test.d.ts +5 -0
  22. package/dist/distribution/__tests__/env-scanner.test.d.ts.map +1 -0
  23. package/dist/distribution/__tests__/env-scanner.test.js +576 -0
  24. package/dist/distribution/__tests__/env-scanner.test.js.map +1 -0
  25. package/dist/distribution/__tests__/file-installer.test.d.ts +7 -0
  26. package/dist/distribution/__tests__/file-installer.test.d.ts.map +1 -0
  27. package/dist/distribution/__tests__/file-installer.test.js +714 -0
  28. package/dist/distribution/__tests__/file-installer.test.js.map +1 -0
  29. package/dist/distribution/__tests__/fleet-config-updater.test.d.ts +7 -0
  30. package/dist/distribution/__tests__/fleet-config-updater.test.d.ts.map +1 -0
  31. package/dist/distribution/__tests__/fleet-config-updater.test.js +531 -0
  32. package/dist/distribution/__tests__/fleet-config-updater.test.js.map +1 -0
  33. package/dist/distribution/__tests__/installation-metadata.test.d.ts +2 -0
  34. package/dist/distribution/__tests__/installation-metadata.test.d.ts.map +1 -0
  35. package/dist/distribution/__tests__/installation-metadata.test.js +292 -0
  36. package/dist/distribution/__tests__/installation-metadata.test.js.map +1 -0
  37. package/dist/distribution/__tests__/integration.test.d.ts +10 -0
  38. package/dist/distribution/__tests__/integration.test.d.ts.map +1 -0
  39. package/dist/distribution/__tests__/integration.test.js +522 -0
  40. package/dist/distribution/__tests__/integration.test.js.map +1 -0
  41. package/dist/distribution/__tests__/repository-fetcher.test.d.ts +5 -0
  42. package/dist/distribution/__tests__/repository-fetcher.test.d.ts.map +1 -0
  43. package/dist/distribution/__tests__/repository-fetcher.test.js +386 -0
  44. package/dist/distribution/__tests__/repository-fetcher.test.js.map +1 -0
  45. package/dist/distribution/__tests__/repository-validator.test.d.ts +7 -0
  46. package/dist/distribution/__tests__/repository-validator.test.d.ts.map +1 -0
  47. package/dist/distribution/__tests__/repository-validator.test.js +447 -0
  48. package/dist/distribution/__tests__/repository-validator.test.js.map +1 -0
  49. package/dist/distribution/__tests__/source-specifier.test.d.ts +5 -0
  50. package/dist/distribution/__tests__/source-specifier.test.d.ts.map +1 -0
  51. package/dist/distribution/__tests__/source-specifier.test.js +533 -0
  52. package/dist/distribution/__tests__/source-specifier.test.js.map +1 -0
  53. package/dist/distribution/agent-discovery.d.ts +81 -0
  54. package/dist/distribution/agent-discovery.d.ts.map +1 -0
  55. package/dist/distribution/agent-discovery.js +264 -0
  56. package/dist/distribution/agent-discovery.js.map +1 -0
  57. package/dist/distribution/agent-info.d.ts +86 -0
  58. package/dist/distribution/agent-info.d.ts.map +1 -0
  59. package/dist/distribution/agent-info.js +225 -0
  60. package/dist/distribution/agent-info.js.map +1 -0
  61. package/dist/distribution/agent-remover.d.ts +83 -0
  62. package/dist/distribution/agent-remover.d.ts.map +1 -0
  63. package/dist/distribution/agent-remover.js +222 -0
  64. package/dist/distribution/agent-remover.js.map +1 -0
  65. package/dist/distribution/agent-repo-metadata.d.ts +181 -0
  66. package/dist/distribution/agent-repo-metadata.d.ts.map +1 -0
  67. package/dist/distribution/agent-repo-metadata.js +143 -0
  68. package/dist/distribution/agent-repo-metadata.js.map +1 -0
  69. package/dist/distribution/env-scanner.d.ts +78 -0
  70. package/dist/distribution/env-scanner.d.ts.map +1 -0
  71. package/dist/distribution/env-scanner.js +144 -0
  72. package/dist/distribution/env-scanner.js.map +1 -0
  73. package/dist/distribution/file-installer.d.ts +80 -0
  74. package/dist/distribution/file-installer.d.ts.map +1 -0
  75. package/dist/distribution/file-installer.js +268 -0
  76. package/dist/distribution/file-installer.js.map +1 -0
  77. package/dist/distribution/fleet-config-updater.d.ts +96 -0
  78. package/dist/distribution/fleet-config-updater.d.ts.map +1 -0
  79. package/dist/distribution/fleet-config-updater.js +266 -0
  80. package/dist/distribution/fleet-config-updater.js.map +1 -0
  81. package/dist/distribution/index.d.ts +23 -0
  82. package/dist/distribution/index.d.ts.map +1 -0
  83. package/dist/distribution/index.js +42 -0
  84. package/dist/distribution/index.js.map +1 -0
  85. package/dist/distribution/installation-metadata.d.ts +191 -0
  86. package/dist/distribution/installation-metadata.d.ts.map +1 -0
  87. package/dist/distribution/installation-metadata.js +100 -0
  88. package/dist/distribution/installation-metadata.js.map +1 -0
  89. package/dist/distribution/repository-fetcher.d.ts +104 -0
  90. package/dist/distribution/repository-fetcher.d.ts.map +1 -0
  91. package/dist/distribution/repository-fetcher.js +246 -0
  92. package/dist/distribution/repository-fetcher.js.map +1 -0
  93. package/dist/distribution/repository-validator.d.ts +86 -0
  94. package/dist/distribution/repository-validator.d.ts.map +1 -0
  95. package/dist/distribution/repository-validator.js +296 -0
  96. package/dist/distribution/repository-validator.js.map +1 -0
  97. package/dist/distribution/source-specifier.d.ts +106 -0
  98. package/dist/distribution/source-specifier.d.ts.map +1 -0
  99. package/dist/distribution/source-specifier.js +247 -0
  100. package/dist/distribution/source-specifier.js.map +1 -0
  101. package/dist/fleet-manager/errors.d.ts +15 -0
  102. package/dist/fleet-manager/errors.d.ts.map +1 -1
  103. package/dist/fleet-manager/errors.js +16 -0
  104. package/dist/fleet-manager/errors.js.map +1 -1
  105. package/dist/fleet-manager/fleet-manager.d.ts.map +1 -1
  106. package/dist/fleet-manager/fleet-manager.js +31 -9
  107. package/dist/fleet-manager/fleet-manager.js.map +1 -1
  108. package/dist/index.d.ts +1 -0
  109. package/dist/index.d.ts.map +1 -1
  110. package/dist/index.js +2 -0
  111. package/dist/index.js.map +1 -1
  112. package/dist/runner/message-processor.d.ts.map +1 -1
  113. package/dist/runner/message-processor.js +7 -2
  114. package/dist/runner/message-processor.js.map +1 -1
  115. package/dist/runner/runtime/container-manager.js +1 -1
  116. package/dist/runner/runtime/container-manager.js.map +1 -1
  117. package/dist/scheduler/errors.d.ts +15 -0
  118. package/dist/scheduler/errors.d.ts.map +1 -1
  119. package/package.json +1 -1
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Repository Validation for Agent Distribution
3
+ *
4
+ * Validates that a directory is a valid agent repository by checking:
5
+ * - agent.yaml exists and is valid YAML conforming to AgentConfigSchema
6
+ * - docker.network is not "none" (agents need network access for Anthropic API)
7
+ * - herdctl.json is valid if present
8
+ * - Optional files like CLAUDE.md and README.md
9
+ */
10
+ import { access, readFile } from "node:fs/promises";
11
+ import { join } from "node:path";
12
+ import { parse as parseYaml, YAMLParseError } from "yaml";
13
+ import { AgentConfigSchema } from "../config/schema.js";
14
+ import { createLogger } from "../utils/logger.js";
15
+ import { AgentRepoMetadataSchema } from "./agent-repo-metadata.js";
16
+ const logger = createLogger("distribution:validator");
17
+ // =============================================================================
18
+ // Error Codes
19
+ // =============================================================================
20
+ /** Error: agent.yaml not found */
21
+ export const MISSING_AGENT_YAML = "MISSING_AGENT_YAML";
22
+ /** Error: agent.yaml is not valid YAML */
23
+ export const YAML_PARSE_ERROR = "YAML_PARSE_ERROR";
24
+ /** Error: agent.yaml fails schema validation */
25
+ export const INVALID_AGENT_YAML = "INVALID_AGENT_YAML";
26
+ /** Error: docker.network is set to "none" */
27
+ export const DOCKER_NETWORK_NONE = "DOCKER_NETWORK_NONE";
28
+ /** Error: herdctl.json is not valid JSON */
29
+ export const JSON_PARSE_ERROR = "JSON_PARSE_ERROR";
30
+ /** Error: herdctl.json fails schema validation */
31
+ export const INVALID_HERDCTL_JSON = "INVALID_HERDCTL_JSON";
32
+ /** Warning: no herdctl.json (optional but recommended) */
33
+ export const MISSING_HERDCTL_JSON = "MISSING_HERDCTL_JSON";
34
+ /** Warning: name in agent.yaml differs from name in herdctl.json */
35
+ export const NAME_MISMATCH = "NAME_MISMATCH";
36
+ /** Warning: no CLAUDE.md file (optional but recommended) */
37
+ export const MISSING_CLAUDE_MD = "MISSING_CLAUDE_MD";
38
+ /** Warning: no README.md (optional but recommended) */
39
+ export const MISSING_README = "MISSING_README";
40
+ // =============================================================================
41
+ // Helper Functions
42
+ // =============================================================================
43
+ /**
44
+ * Check if a file exists
45
+ */
46
+ async function fileExists(filePath) {
47
+ try {
48
+ await access(filePath);
49
+ return true;
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ }
55
+ /**
56
+ * Read and parse YAML file
57
+ */
58
+ async function readYamlFile(filePath) {
59
+ try {
60
+ const content = await readFile(filePath, "utf-8");
61
+ const data = parseYaml(content);
62
+ return { success: true, data };
63
+ }
64
+ catch (err) {
65
+ if (err instanceof YAMLParseError) {
66
+ const position = err.linePos?.[0];
67
+ const locationInfo = position ? ` at line ${position.line}, column ${position.col}` : "";
68
+ return {
69
+ success: false,
70
+ error: `Invalid YAML syntax${locationInfo}: ${err.message}`,
71
+ isYamlError: true,
72
+ };
73
+ }
74
+ // File read error
75
+ const error = err;
76
+ return { success: false, error: error.message, isYamlError: false };
77
+ }
78
+ }
79
+ /**
80
+ * Read and parse JSON file
81
+ */
82
+ async function readJsonFile(filePath) {
83
+ try {
84
+ const content = await readFile(filePath, "utf-8");
85
+ const data = JSON.parse(content);
86
+ return { success: true, data };
87
+ }
88
+ catch (err) {
89
+ if (err instanceof SyntaxError) {
90
+ return { success: false, error: `Invalid JSON: ${err.message}`, isJsonError: true };
91
+ }
92
+ // File read error
93
+ const error = err;
94
+ return { success: false, error: error.message, isJsonError: false };
95
+ }
96
+ }
97
+ // =============================================================================
98
+ // Main Validator
99
+ // =============================================================================
100
+ /**
101
+ * Validate a directory as an agent repository
102
+ *
103
+ * Checks that the directory contains valid agent repository files:
104
+ * - agent.yaml (required) - must be valid YAML conforming to AgentConfigSchema
105
+ * - herdctl.json (optional) - if present, must be valid JSON conforming to AgentRepoMetadataSchema
106
+ * - CLAUDE.md (optional) - recommended for agent identity
107
+ * - README.md (optional) - recommended for documentation
108
+ *
109
+ * Also performs safety checks:
110
+ * - docker.network cannot be "none" (agents need network for Anthropic API)
111
+ *
112
+ * @param dirPath - Absolute path to the directory to validate
113
+ * @returns Validation result with errors, warnings, and parsed configurations
114
+ *
115
+ * @example
116
+ * ```typescript
117
+ * const result = await validateRepository("/tmp/my-agent");
118
+ * if (result.valid) {
119
+ * console.log(`Agent ${result.agentName} is valid`);
120
+ * } else {
121
+ * console.error("Validation errors:", result.errors);
122
+ * }
123
+ * ```
124
+ */
125
+ export async function validateRepository(dirPath) {
126
+ logger.debug("Validating repository", { path: dirPath });
127
+ const errors = [];
128
+ const warnings = [];
129
+ let agentName = null;
130
+ let agentConfig = null;
131
+ let repoMetadata = null;
132
+ // ==========================================================================
133
+ // 1. Check and validate agent.yaml (required)
134
+ // ==========================================================================
135
+ const agentYamlPath = join(dirPath, "agent.yaml");
136
+ const agentYamlExists = await fileExists(agentYamlPath);
137
+ if (!agentYamlExists) {
138
+ logger.debug("agent.yaml not found", { path: agentYamlPath });
139
+ errors.push({
140
+ code: MISSING_AGENT_YAML,
141
+ message: "Required file agent.yaml not found. Every agent repository must have an agent.yaml file.",
142
+ path: "agent.yaml",
143
+ });
144
+ }
145
+ else {
146
+ // Read and parse YAML
147
+ const yamlResult = await readYamlFile(agentYamlPath);
148
+ if (!yamlResult.success) {
149
+ logger.debug("Failed to parse agent.yaml", { error: yamlResult.error });
150
+ errors.push({
151
+ code: yamlResult.isYamlError ? YAML_PARSE_ERROR : MISSING_AGENT_YAML,
152
+ message: yamlResult.error,
153
+ path: "agent.yaml",
154
+ });
155
+ }
156
+ else {
157
+ // ==========================================================================
158
+ // 2. Check docker.network BEFORE schema validation (provides clearer error)
159
+ // Note: AgentDockerSchema doesn't include 'network' field (it's fleet-level only),
160
+ // but we want to give a specific error message if someone sets network: none
161
+ // ==========================================================================
162
+ const rawData = yamlResult.data;
163
+ const dockerConfig = rawData?.docker;
164
+ if (dockerConfig?.network === "none") {
165
+ logger.debug("docker.network is set to none", { docker: dockerConfig });
166
+ errors.push({
167
+ code: DOCKER_NETWORK_NONE,
168
+ message: 'docker.network is set to "none", which would prevent the agent from accessing Anthropic APIs. ' +
169
+ "Agents require network access. Remove the network field (bridge is the default at fleet level) " +
170
+ 'or set it at the fleet level to "bridge" or "host".',
171
+ path: "agent.yaml",
172
+ });
173
+ }
174
+ // Validate against AgentConfigSchema
175
+ const schemaResult = AgentConfigSchema.safeParse(yamlResult.data);
176
+ if (!schemaResult.success) {
177
+ const issues = schemaResult.error.issues
178
+ .map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`)
179
+ .join("; ");
180
+ logger.debug("agent.yaml schema validation failed", { issues });
181
+ errors.push({
182
+ code: INVALID_AGENT_YAML,
183
+ message: `agent.yaml validation failed: ${issues}`,
184
+ path: "agent.yaml",
185
+ });
186
+ }
187
+ else {
188
+ agentConfig = schemaResult.data;
189
+ agentName = schemaResult.data.name;
190
+ logger.debug("agent.yaml is valid", { name: agentName });
191
+ }
192
+ }
193
+ }
194
+ // ==========================================================================
195
+ // 3. Check and validate herdctl.json (optional)
196
+ // ==========================================================================
197
+ const herdctlJsonPath = join(dirPath, "herdctl.json");
198
+ const herdctlJsonExists = await fileExists(herdctlJsonPath);
199
+ if (!herdctlJsonExists) {
200
+ logger.debug("herdctl.json not found (optional)", { path: herdctlJsonPath });
201
+ warnings.push({
202
+ code: MISSING_HERDCTL_JSON,
203
+ message: "No herdctl.json found. This file is optional but recommended for registry listing and installation metadata.",
204
+ path: "herdctl.json",
205
+ });
206
+ }
207
+ else {
208
+ // Read and parse JSON
209
+ const jsonResult = await readJsonFile(herdctlJsonPath);
210
+ if (!jsonResult.success) {
211
+ logger.debug("Failed to parse herdctl.json", { error: jsonResult.error });
212
+ errors.push({
213
+ code: jsonResult.isJsonError ? JSON_PARSE_ERROR : INVALID_HERDCTL_JSON,
214
+ message: jsonResult.error,
215
+ path: "herdctl.json",
216
+ });
217
+ }
218
+ else {
219
+ // Validate against AgentRepoMetadataSchema
220
+ const schemaResult = AgentRepoMetadataSchema.safeParse(jsonResult.data);
221
+ if (!schemaResult.success) {
222
+ const issues = schemaResult.error.issues
223
+ .map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`)
224
+ .join("; ");
225
+ logger.debug("herdctl.json schema validation failed", { issues });
226
+ errors.push({
227
+ code: INVALID_HERDCTL_JSON,
228
+ message: `herdctl.json validation failed: ${issues}`,
229
+ path: "herdctl.json",
230
+ });
231
+ }
232
+ else {
233
+ repoMetadata = schemaResult.data;
234
+ logger.debug("herdctl.json is valid", { name: repoMetadata.name });
235
+ // ==========================================================================
236
+ // 4. Check name consistency between agent.yaml and herdctl.json
237
+ // ==========================================================================
238
+ if (agentName && repoMetadata.name !== agentName) {
239
+ logger.debug("Name mismatch between agent.yaml and herdctl.json", {
240
+ agentYaml: agentName,
241
+ herdctlJson: repoMetadata.name,
242
+ });
243
+ warnings.push({
244
+ code: NAME_MISMATCH,
245
+ message: `Name mismatch: agent.yaml has name "${agentName}" but herdctl.json has name "${repoMetadata.name}". ` +
246
+ `The name from agent.yaml will be used for installation.`,
247
+ path: "herdctl.json",
248
+ });
249
+ }
250
+ }
251
+ }
252
+ }
253
+ // ==========================================================================
254
+ // 5. Check for optional recommended files
255
+ // ==========================================================================
256
+ const claudeMdPath = join(dirPath, "CLAUDE.md");
257
+ const claudeMdExists = await fileExists(claudeMdPath);
258
+ if (!claudeMdExists) {
259
+ logger.debug("CLAUDE.md not found (optional)", { path: claudeMdPath });
260
+ warnings.push({
261
+ code: MISSING_CLAUDE_MD,
262
+ message: "No CLAUDE.md found. This file is optional but recommended for defining agent identity and behavior.",
263
+ path: "CLAUDE.md",
264
+ });
265
+ }
266
+ const readmePath = join(dirPath, "README.md");
267
+ const readmeExists = await fileExists(readmePath);
268
+ if (!readmeExists) {
269
+ logger.debug("README.md not found (optional)", { path: readmePath });
270
+ warnings.push({
271
+ code: MISSING_README,
272
+ message: "No README.md found. This file is optional but recommended for documentation.",
273
+ path: "README.md",
274
+ });
275
+ }
276
+ // ==========================================================================
277
+ // Build and return result
278
+ // ==========================================================================
279
+ const valid = errors.length === 0;
280
+ logger.info("Repository validation complete", {
281
+ path: dirPath,
282
+ valid,
283
+ errorCount: errors.length,
284
+ warningCount: warnings.length,
285
+ agentName,
286
+ });
287
+ return {
288
+ valid,
289
+ agentName,
290
+ agentConfig,
291
+ repoMetadata,
292
+ errors,
293
+ warnings,
294
+ };
295
+ }
296
+ //# sourceMappingURL=repository-validator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repository-validator.js","sourceRoot":"","sources":["../../src/distribution/repository-validator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Source Specifier Parser
3
+ *
4
+ * Parses source strings into structured specifier objects for agent distribution.
5
+ *
6
+ * Supported formats:
7
+ * - `github:user/repo` → GitHub source (explicit prefix)
8
+ * - `github:user/repo@v1.0.0` → GitHub source with tag/branch/commit ref
9
+ * - `user/repo` → GitHub source (shorthand, equivalent to `github:user/repo`)
10
+ * - `user/repo@v1.0.0` → GitHub source shorthand with ref
11
+ * - `./local/path` or `../path` or `/absolute/path` → Local source
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const specifier = parseSourceSpecifier("github:user/repo@v1.0.0");
16
+ * // { type: 'github', owner: 'user', repo: 'repo', ref: 'v1.0.0' }
17
+ *
18
+ * const shorthand = parseSourceSpecifier("user/repo");
19
+ * // { type: 'github', owner: 'user', repo: 'repo' }
20
+ *
21
+ * const local = parseSourceSpecifier("./my-agents/custom");
22
+ * // { type: 'local', path: '/resolved/absolute/path/my-agents/custom' }
23
+ * ```
24
+ */
25
+ import { FleetManagerError } from "../fleet-manager/errors.js";
26
+ /**
27
+ * GitHub source specifier.
28
+ * Points to a GitHub repository, optionally at a specific ref (tag, branch, or commit).
29
+ */
30
+ export interface GitHubSource {
31
+ type: "github";
32
+ /** GitHub username or organization */
33
+ owner: string;
34
+ /** Repository name */
35
+ repo: string;
36
+ /** Optional ref: tag, branch name, or commit SHA */
37
+ ref?: string;
38
+ }
39
+ /**
40
+ * Local source specifier.
41
+ * Points to a local directory path (resolved to absolute).
42
+ */
43
+ export interface LocalSource {
44
+ type: "local";
45
+ /** Absolute path to the local directory */
46
+ path: string;
47
+ }
48
+ /**
49
+ * Union type for all source specifier variants.
50
+ */
51
+ export type SourceSpecifier = GitHubSource | LocalSource;
52
+ /**
53
+ * Error thrown when a source specifier cannot be parsed.
54
+ */
55
+ export declare class SourceParseError extends FleetManagerError {
56
+ readonly source: string;
57
+ constructor(message: string, source: string);
58
+ }
59
+ /**
60
+ * Parses a source specifier string into a structured SourceSpecifier object.
61
+ *
62
+ * @param source - The source string to parse
63
+ * @returns Parsed SourceSpecifier
64
+ * @throws SourceParseError if the source format is invalid
65
+ *
66
+ * @example
67
+ * ```typescript
68
+ * // GitHub sources
69
+ * parseSourceSpecifier("github:user/repo")
70
+ * // { type: 'github', owner: 'user', repo: 'repo' }
71
+ *
72
+ * parseSourceSpecifier("github:user/repo@v1.0.0")
73
+ * // { type: 'github', owner: 'user', repo: 'repo', ref: 'v1.0.0' }
74
+ *
75
+ * // Local sources
76
+ * parseSourceSpecifier("./local/path")
77
+ * // { type: 'local', path: '/resolved/absolute/path' }
78
+ *
79
+ * // GitHub shorthand
80
+ * parseSourceSpecifier("user/repo")
81
+ * // { type: 'github', owner: 'user', repo: 'repo' }
82
+ * ```
83
+ */
84
+ export declare function parseSourceSpecifier(source: string): SourceSpecifier;
85
+ /**
86
+ * Type guard for GitHubSource.
87
+ */
88
+ export declare function isGitHubSource(specifier: SourceSpecifier): specifier is GitHubSource;
89
+ /**
90
+ * Type guard for LocalSource.
91
+ */
92
+ export declare function isLocalSource(specifier: SourceSpecifier): specifier is LocalSource;
93
+ /**
94
+ * Converts a SourceSpecifier back to its string representation.
95
+ *
96
+ * @param specifier - The specifier to stringify
97
+ * @returns String representation of the specifier
98
+ *
99
+ * @example
100
+ * ```typescript
101
+ * stringifySourceSpecifier({ type: 'github', owner: 'user', repo: 'repo', ref: 'v1.0.0' })
102
+ * // "github:user/repo@v1.0.0"
103
+ * ```
104
+ */
105
+ export declare function stringifySourceSpecifier(specifier: SourceSpecifier): string;
106
+ //# sourceMappingURL=source-specifier.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source-specifier.d.ts","sourceRoot":"","sources":["../../src/distribution/source-specifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAM/D;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,QAAQ,CAAC;IACf,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,sBAAsB;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,oDAAoD;IACpD,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,OAAO,CAAC;IACd,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG,YAAY,GAAG,WAAW,CAAC;AAMzD;;GAEG;AACH,qBAAa,gBAAiB,SAAQ,iBAAiB;aAGnC,MAAM,EAAE,MAAM;gBAD9B,OAAO,EAAE,MAAM,EACC,MAAM,EAAE,MAAM;CAKjC;AA2JD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,CAiCpE;AAMD;;GAEG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,eAAe,GAAG,SAAS,IAAI,YAAY,CAEpF;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,eAAe,GAAG,SAAS,IAAI,WAAW,CAElF;AAMD;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,eAAe,GAAG,MAAM,CAY3E"}
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Source Specifier Parser
3
+ *
4
+ * Parses source strings into structured specifier objects for agent distribution.
5
+ *
6
+ * Supported formats:
7
+ * - `github:user/repo` → GitHub source (explicit prefix)
8
+ * - `github:user/repo@v1.0.0` → GitHub source with tag/branch/commit ref
9
+ * - `user/repo` → GitHub source (shorthand, equivalent to `github:user/repo`)
10
+ * - `user/repo@v1.0.0` → GitHub source shorthand with ref
11
+ * - `./local/path` or `../path` or `/absolute/path` → Local source
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const specifier = parseSourceSpecifier("github:user/repo@v1.0.0");
16
+ * // { type: 'github', owner: 'user', repo: 'repo', ref: 'v1.0.0' }
17
+ *
18
+ * const shorthand = parseSourceSpecifier("user/repo");
19
+ * // { type: 'github', owner: 'user', repo: 'repo' }
20
+ *
21
+ * const local = parseSourceSpecifier("./my-agents/custom");
22
+ * // { type: 'local', path: '/resolved/absolute/path/my-agents/custom' }
23
+ * ```
24
+ */
25
+ import * as path from "path";
26
+ import { FleetManagerError } from "../fleet-manager/errors.js";
27
+ // =============================================================================
28
+ // Error Class
29
+ // =============================================================================
30
+ /**
31
+ * Error thrown when a source specifier cannot be parsed.
32
+ */
33
+ export class SourceParseError extends FleetManagerError {
34
+ source;
35
+ constructor(message, source) {
36
+ super(message, { code: "SOURCE_PARSE_ERROR" });
37
+ this.source = source;
38
+ this.name = "SourceParseError";
39
+ }
40
+ }
41
+ // =============================================================================
42
+ // Constants
43
+ // =============================================================================
44
+ /**
45
+ * Pattern for validating GitHub owner/repo names.
46
+ * GitHub allows: alphanumeric, hyphens, underscores, and dots.
47
+ * Cannot start with a dot or end with .git.
48
+ * Maximum 100 characters for owner, 100 for repo.
49
+ *
50
+ * @see https://docs.github.com/en/repositories/creating-and-managing-repositories/about-repositories
51
+ */
52
+ const GITHUB_NAME_PATTERN = /^[a-zA-Z0-9](?:[a-zA-Z0-9._-]*[a-zA-Z0-9])?$|^[a-zA-Z0-9]$/;
53
+ /**
54
+ * Maximum length for GitHub owner/repo names.
55
+ */
56
+ const MAX_GITHUB_NAME_LENGTH = 100;
57
+ // =============================================================================
58
+ // Parser Implementation
59
+ // =============================================================================
60
+ /**
61
+ * Validates a GitHub owner or repository name.
62
+ *
63
+ * @param name - The name to validate
64
+ * @param field - Field name for error messages ("owner" or "repository")
65
+ * @param source - Original source string for error context
66
+ * @throws SourceParseError if the name is invalid
67
+ */
68
+ function validateGitHubName(name, field, source) {
69
+ if (!name) {
70
+ throw new SourceParseError(`GitHub ${field} cannot be empty`, source);
71
+ }
72
+ if (name.length > MAX_GITHUB_NAME_LENGTH) {
73
+ throw new SourceParseError(`GitHub ${field} "${name}" exceeds maximum length of ${MAX_GITHUB_NAME_LENGTH} characters`, source);
74
+ }
75
+ if (!GITHUB_NAME_PATTERN.test(name)) {
76
+ throw new SourceParseError(`Invalid GitHub ${field} "${name}". ${field === "owner" ? "Owner" : "Repository"} names may only contain alphanumeric characters, hyphens, underscores, and dots, and cannot start with a dot.`, source);
77
+ }
78
+ // Additional check: cannot end with .git (GitHub convention)
79
+ if (name.endsWith(".git")) {
80
+ throw new SourceParseError(`GitHub ${field} "${name}" cannot end with ".git"`, source);
81
+ }
82
+ }
83
+ /**
84
+ * Parses a GitHub source specifier.
85
+ *
86
+ * @param value - The value after "github:" prefix (e.g., "user/repo" or "user/repo@v1.0.0")
87
+ * @param source - Original source string for error context
88
+ * @returns Parsed GitHubSource
89
+ * @throws SourceParseError if the format is invalid
90
+ */
91
+ function parseGitHubSource(value, source) {
92
+ if (!value) {
93
+ throw new SourceParseError('GitHub source requires owner/repo format (e.g., "github:user/repo")', source);
94
+ }
95
+ // Split ref if present: user/repo@ref
96
+ let repoPath = value;
97
+ let ref;
98
+ const atIndex = value.indexOf("@");
99
+ if (atIndex !== -1) {
100
+ repoPath = value.slice(0, atIndex);
101
+ ref = value.slice(atIndex + 1);
102
+ if (!ref) {
103
+ throw new SourceParseError('GitHub ref cannot be empty after "@" (e.g., "github:user/repo@v1.0.0")', source);
104
+ }
105
+ }
106
+ // Split owner/repo
107
+ const slashIndex = repoPath.indexOf("/");
108
+ if (slashIndex === -1) {
109
+ throw new SourceParseError(`Invalid GitHub source format "${source}". Expected "github:owner/repo" format.`, source);
110
+ }
111
+ const owner = repoPath.slice(0, slashIndex);
112
+ const repo = repoPath.slice(slashIndex + 1);
113
+ // Check for extra slashes (nested paths not supported)
114
+ if (repo.includes("/")) {
115
+ throw new SourceParseError(`Invalid GitHub source format "${source}". Nested paths are not supported; use "github:owner/repo" format.`, source);
116
+ }
117
+ // Validate owner and repo
118
+ validateGitHubName(owner, "owner", source);
119
+ validateGitHubName(repo, "repository", source);
120
+ const result = { type: "github", owner, repo };
121
+ if (ref) {
122
+ result.ref = ref;
123
+ }
124
+ return result;
125
+ }
126
+ /**
127
+ * Parses a local path source specifier.
128
+ *
129
+ * @param pathStr - The path string (relative or absolute)
130
+ * @returns Parsed LocalSource with absolute path
131
+ */
132
+ function parseLocalSource(pathStr) {
133
+ // Resolve to absolute path
134
+ const absolutePath = path.resolve(pathStr);
135
+ return {
136
+ type: "local",
137
+ path: absolutePath,
138
+ };
139
+ }
140
+ /**
141
+ * Determines if a source string represents a local path.
142
+ *
143
+ * @param source - The source string to check
144
+ * @returns true if the source is a local path
145
+ */
146
+ function isLocalPath(source) {
147
+ return (source.startsWith("./") ||
148
+ source.startsWith("../") ||
149
+ source.startsWith("/") ||
150
+ // Windows absolute paths (e.g., C:\)
151
+ /^[a-zA-Z]:[/\\]/.test(source));
152
+ }
153
+ /**
154
+ * Parses a source specifier string into a structured SourceSpecifier object.
155
+ *
156
+ * @param source - The source string to parse
157
+ * @returns Parsed SourceSpecifier
158
+ * @throws SourceParseError if the source format is invalid
159
+ *
160
+ * @example
161
+ * ```typescript
162
+ * // GitHub sources
163
+ * parseSourceSpecifier("github:user/repo")
164
+ * // { type: 'github', owner: 'user', repo: 'repo' }
165
+ *
166
+ * parseSourceSpecifier("github:user/repo@v1.0.0")
167
+ * // { type: 'github', owner: 'user', repo: 'repo', ref: 'v1.0.0' }
168
+ *
169
+ * // Local sources
170
+ * parseSourceSpecifier("./local/path")
171
+ * // { type: 'local', path: '/resolved/absolute/path' }
172
+ *
173
+ * // GitHub shorthand
174
+ * parseSourceSpecifier("user/repo")
175
+ * // { type: 'github', owner: 'user', repo: 'repo' }
176
+ * ```
177
+ */
178
+ export function parseSourceSpecifier(source) {
179
+ // Validate input
180
+ if (!source || typeof source !== "string") {
181
+ throw new SourceParseError("Source specifier cannot be empty", source || "");
182
+ }
183
+ const trimmed = source.trim();
184
+ if (!trimmed) {
185
+ throw new SourceParseError("Source specifier cannot be empty or whitespace only", source);
186
+ }
187
+ // Check for GitHub prefix
188
+ if (trimmed.startsWith("github:")) {
189
+ const value = trimmed.slice(7); // Remove "github:" prefix
190
+ return parseGitHubSource(value, source);
191
+ }
192
+ // Check for local path
193
+ if (isLocalPath(trimmed)) {
194
+ return parseLocalSource(trimmed);
195
+ }
196
+ // Check for GitHub shorthand (owner/repo or owner/repo@ref)
197
+ // A bare string containing "/" that isn't a local path is treated as GitHub shorthand
198
+ const slashCount = (trimmed.match(/\//g) || []).length;
199
+ if (slashCount === 1) {
200
+ return parseGitHubSource(trimmed, source);
201
+ }
202
+ throw new SourceParseError(`Unrecognized source format "${source}". Use "owner/repo", "github:owner/repo", or a local path (./path, ../path, /path).`, source);
203
+ }
204
+ // =============================================================================
205
+ // Type Guards
206
+ // =============================================================================
207
+ /**
208
+ * Type guard for GitHubSource.
209
+ */
210
+ export function isGitHubSource(specifier) {
211
+ return specifier.type === "github";
212
+ }
213
+ /**
214
+ * Type guard for LocalSource.
215
+ */
216
+ export function isLocalSource(specifier) {
217
+ return specifier.type === "local";
218
+ }
219
+ // =============================================================================
220
+ // Utility Functions
221
+ // =============================================================================
222
+ /**
223
+ * Converts a SourceSpecifier back to its string representation.
224
+ *
225
+ * @param specifier - The specifier to stringify
226
+ * @returns String representation of the specifier
227
+ *
228
+ * @example
229
+ * ```typescript
230
+ * stringifySourceSpecifier({ type: 'github', owner: 'user', repo: 'repo', ref: 'v1.0.0' })
231
+ * // "github:user/repo@v1.0.0"
232
+ * ```
233
+ */
234
+ export function stringifySourceSpecifier(specifier) {
235
+ switch (specifier.type) {
236
+ case "github": {
237
+ let str = `github:${specifier.owner}/${specifier.repo}`;
238
+ if (specifier.ref) {
239
+ str += `@${specifier.ref}`;
240
+ }
241
+ return str;
242
+ }
243
+ case "local":
244
+ return specifier.path;
245
+ }
246
+ }
247
+ //# sourceMappingURL=source-specifier.js.map