@openforge-ai/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.cjs +627 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +604 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Forge AI Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
#!/usr/bin/env node
|
|
3
|
+
"use strict";
|
|
4
|
+
var __create = Object.create;
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
7
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
8
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
9
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
19
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
20
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
21
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
22
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
23
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
24
|
+
mod
|
|
25
|
+
));
|
|
26
|
+
|
|
27
|
+
// src/index.ts
|
|
28
|
+
var import_commander = require("commander");
|
|
29
|
+
|
|
30
|
+
// src/commands/deploy.ts
|
|
31
|
+
var import_chalk2 = __toESM(require("chalk"), 1);
|
|
32
|
+
|
|
33
|
+
// src/parser/validate.ts
|
|
34
|
+
var import_yaml = require("yaml");
|
|
35
|
+
|
|
36
|
+
// src/parser/schema.ts
|
|
37
|
+
var import_zod = require("zod");
|
|
38
|
+
var modelProviderSchema = import_zod.z.enum(["anthropic", "openai", "google", "ollama", "bedrock"]);
|
|
39
|
+
var modelConfigSchema = import_zod.z.object({
|
|
40
|
+
provider: modelProviderSchema,
|
|
41
|
+
name: import_zod.z.string().min(1),
|
|
42
|
+
temperature: import_zod.z.number().min(0).max(2).optional(),
|
|
43
|
+
max_tokens: import_zod.z.number().int().positive().optional()
|
|
44
|
+
});
|
|
45
|
+
var systemPromptSchema = import_zod.z.object({
|
|
46
|
+
file: import_zod.z.string().optional(),
|
|
47
|
+
inline: import_zod.z.string().optional()
|
|
48
|
+
}).refine(
|
|
49
|
+
(data) => data.file || data.inline,
|
|
50
|
+
{ message: "system_prompt must specify either 'file' or 'inline'" }
|
|
51
|
+
);
|
|
52
|
+
var mcpServerSchema = import_zod.z.object({
|
|
53
|
+
name: import_zod.z.string().min(1),
|
|
54
|
+
command: import_zod.z.string().min(1),
|
|
55
|
+
args: import_zod.z.array(import_zod.z.string()).optional(),
|
|
56
|
+
env: import_zod.z.record(import_zod.z.string()).optional()
|
|
57
|
+
});
|
|
58
|
+
var toolsConfigSchema = import_zod.z.object({
|
|
59
|
+
mcp_servers: import_zod.z.array(mcpServerSchema).optional()
|
|
60
|
+
});
|
|
61
|
+
var memoryTypeSchema = import_zod.z.enum(["none", "in-context", "vector"]);
|
|
62
|
+
var memoryProviderSchema = import_zod.z.enum(["chroma", "pinecone", "weaviate"]);
|
|
63
|
+
var memoryConfigSchema = import_zod.z.object({
|
|
64
|
+
type: memoryTypeSchema,
|
|
65
|
+
provider: memoryProviderSchema.optional(),
|
|
66
|
+
collection: import_zod.z.string().optional()
|
|
67
|
+
}).refine(
|
|
68
|
+
(data) => data.type !== "vector" || data.provider,
|
|
69
|
+
{ message: "vector memory type requires a provider" }
|
|
70
|
+
);
|
|
71
|
+
var hookStepSchema = import_zod.z.object({
|
|
72
|
+
run: import_zod.z.string().min(1)
|
|
73
|
+
});
|
|
74
|
+
var hooksConfigSchema = import_zod.z.object({
|
|
75
|
+
pre_deploy: import_zod.z.array(hookStepSchema).optional(),
|
|
76
|
+
post_deploy: import_zod.z.array(hookStepSchema).optional()
|
|
77
|
+
});
|
|
78
|
+
var environmentOverrideSchema = import_zod.z.object({
|
|
79
|
+
model: modelConfigSchema.partial().optional(),
|
|
80
|
+
tools: toolsConfigSchema.optional(),
|
|
81
|
+
memory: memoryConfigSchema.optional()
|
|
82
|
+
});
|
|
83
|
+
var agentConfigSchema = import_zod.z.object({
|
|
84
|
+
name: import_zod.z.string().min(1).regex(/^[a-z0-9-]+$/, "Agent name must be lowercase alphanumeric with hyphens"),
|
|
85
|
+
description: import_zod.z.string().optional()
|
|
86
|
+
});
|
|
87
|
+
var forgeConfigSchema = import_zod.z.object({
|
|
88
|
+
version: import_zod.z.literal("1"),
|
|
89
|
+
agent: agentConfigSchema,
|
|
90
|
+
model: modelConfigSchema,
|
|
91
|
+
system_prompt: systemPromptSchema.optional(),
|
|
92
|
+
tools: toolsConfigSchema.optional(),
|
|
93
|
+
memory: memoryConfigSchema.optional(),
|
|
94
|
+
environments: import_zod.z.record(environmentOverrideSchema).optional(),
|
|
95
|
+
hooks: hooksConfigSchema.optional()
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// src/parser/validate.ts
|
|
99
|
+
function parseForgeYaml(raw) {
|
|
100
|
+
let parsed;
|
|
101
|
+
try {
|
|
102
|
+
parsed = (0, import_yaml.parse)(raw);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
return {
|
|
105
|
+
success: false,
|
|
106
|
+
errors: [`YAML parse error: ${err instanceof Error ? err.message : String(err)}`]
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const result = forgeConfigSchema.safeParse(parsed);
|
|
110
|
+
if (!result.success) {
|
|
111
|
+
return {
|
|
112
|
+
success: false,
|
|
113
|
+
errors: result.error.issues.map(
|
|
114
|
+
(issue) => `${issue.path.join(".")}: ${issue.message}`
|
|
115
|
+
)
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
success: true,
|
|
120
|
+
config: result.data
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function resolveEnvironment(config, env) {
|
|
124
|
+
if (!config.environments?.[env]) {
|
|
125
|
+
return config;
|
|
126
|
+
}
|
|
127
|
+
const override = config.environments[env];
|
|
128
|
+
const resolved = { ...config };
|
|
129
|
+
if (override.model) {
|
|
130
|
+
resolved.model = { ...config.model, ...override.model };
|
|
131
|
+
}
|
|
132
|
+
if (override.tools) {
|
|
133
|
+
resolved.tools = override.tools;
|
|
134
|
+
}
|
|
135
|
+
if (override.memory) {
|
|
136
|
+
resolved.memory = override.memory;
|
|
137
|
+
}
|
|
138
|
+
return resolved;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/parser/load.ts
|
|
142
|
+
var import_promises = require("fs/promises");
|
|
143
|
+
var import_node_path = require("path");
|
|
144
|
+
var import_chalk = __toESM(require("chalk"), 1);
|
|
145
|
+
async function loadConfig(configPath) {
|
|
146
|
+
const resolved = (0, import_node_path.resolve)(configPath);
|
|
147
|
+
console.log(import_chalk.default.blue("\u2192 Reading configuration from"), resolved);
|
|
148
|
+
let raw;
|
|
149
|
+
try {
|
|
150
|
+
raw = await (0, import_promises.readFile)(resolved, "utf-8");
|
|
151
|
+
} catch {
|
|
152
|
+
console.error(import_chalk.default.red("\u2717 Could not read config file:"), resolved);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
const result = parseForgeYaml(raw);
|
|
156
|
+
if (!result.success || !result.config) {
|
|
157
|
+
console.error(import_chalk.default.red("\u2717 Validation errors:"));
|
|
158
|
+
for (const err of result.errors ?? []) {
|
|
159
|
+
console.error(import_chalk.default.red(` \u2022 ${err}`));
|
|
160
|
+
}
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
return result.config;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/engine/state.ts
|
|
167
|
+
var import_promises2 = require("fs/promises");
|
|
168
|
+
var import_node_crypto = require("crypto");
|
|
169
|
+
var import_node_path2 = require("path");
|
|
170
|
+
var STATE_FILE = "state.json";
|
|
171
|
+
function sortDeep(obj) {
|
|
172
|
+
if (Array.isArray(obj)) return obj.map(sortDeep);
|
|
173
|
+
if (obj !== null && typeof obj === "object") {
|
|
174
|
+
const record = obj;
|
|
175
|
+
return Object.fromEntries(
|
|
176
|
+
Object.keys(record).sort().map((key) => [key, sortDeep(record[key])])
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
return obj;
|
|
180
|
+
}
|
|
181
|
+
function hashConfig(config) {
|
|
182
|
+
const normalized = JSON.stringify(sortDeep(config));
|
|
183
|
+
return (0, import_node_crypto.createHash)("sha256").update(normalized).digest("hex");
|
|
184
|
+
}
|
|
185
|
+
async function readState(stateDir) {
|
|
186
|
+
const statePath = (0, import_node_path2.join)(stateDir, STATE_FILE);
|
|
187
|
+
try {
|
|
188
|
+
const raw = await (0, import_promises2.readFile)(statePath, "utf-8");
|
|
189
|
+
const parsed = JSON.parse(raw);
|
|
190
|
+
if (typeof parsed !== "object" || parsed === null || typeof parsed.configHash !== "string" || typeof parsed.agentName !== "string") {
|
|
191
|
+
console.warn("Warning: State file has invalid structure. Treating as no prior state.");
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
return parsed;
|
|
195
|
+
} catch (err) {
|
|
196
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
console.warn("Warning: Failed to read state file. Treating as no prior state.");
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async function writeState(stateDir, state) {
|
|
204
|
+
const statePath = (0, import_node_path2.join)(stateDir, STATE_FILE);
|
|
205
|
+
await (0, import_promises2.mkdir)(stateDir, { recursive: true, mode: 448 });
|
|
206
|
+
await (0, import_promises2.writeFile)(statePath, JSON.stringify(state, null, 2), {
|
|
207
|
+
encoding: "utf-8",
|
|
208
|
+
mode: 384
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
function redactConfig(config) {
|
|
212
|
+
const cloned = JSON.parse(JSON.stringify(config));
|
|
213
|
+
if (cloned.tools?.mcp_servers) {
|
|
214
|
+
for (const server of cloned.tools.mcp_servers) {
|
|
215
|
+
if (server.env) {
|
|
216
|
+
for (const [key, value] of Object.entries(server.env)) {
|
|
217
|
+
if (!/^\$\{.+\}$/.test(value)) {
|
|
218
|
+
server.env[key] = "[REDACTED]";
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return cloned;
|
|
225
|
+
}
|
|
226
|
+
function createState(config, environment) {
|
|
227
|
+
return {
|
|
228
|
+
configHash: hashConfig(config),
|
|
229
|
+
lastDeployed: (/* @__PURE__ */ new Date()).toISOString(),
|
|
230
|
+
environment,
|
|
231
|
+
agentName: config.agent.name,
|
|
232
|
+
config: redactConfig(config)
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/engine/planner.ts
|
|
237
|
+
function plan(desired, actual) {
|
|
238
|
+
const result = {
|
|
239
|
+
toCreate: [],
|
|
240
|
+
toUpdate: [],
|
|
241
|
+
toDelete: [],
|
|
242
|
+
noChange: [],
|
|
243
|
+
hasChanges: false
|
|
244
|
+
};
|
|
245
|
+
if (!actual) {
|
|
246
|
+
result.toCreate.push({
|
|
247
|
+
resource: "agent",
|
|
248
|
+
newValue: desired.agent,
|
|
249
|
+
summary: `Create agent "${desired.agent.name}"`
|
|
250
|
+
});
|
|
251
|
+
result.toCreate.push({
|
|
252
|
+
resource: "model",
|
|
253
|
+
newValue: desired.model,
|
|
254
|
+
summary: `Configure model ${desired.model.provider}/${desired.model.name}`
|
|
255
|
+
});
|
|
256
|
+
if (desired.system_prompt) {
|
|
257
|
+
result.toCreate.push({
|
|
258
|
+
resource: "system_prompt",
|
|
259
|
+
newValue: desired.system_prompt,
|
|
260
|
+
summary: `Set system prompt from ${desired.system_prompt.file ?? "inline"}`
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
if (desired.tools?.mcp_servers) {
|
|
264
|
+
for (const server of desired.tools.mcp_servers) {
|
|
265
|
+
result.toCreate.push({
|
|
266
|
+
resource: "mcp_server",
|
|
267
|
+
field: server.name,
|
|
268
|
+
newValue: server,
|
|
269
|
+
summary: `Add MCP server "${server.name}"`
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (desired.memory && desired.memory.type !== "none") {
|
|
274
|
+
result.toCreate.push({
|
|
275
|
+
resource: "memory",
|
|
276
|
+
newValue: desired.memory,
|
|
277
|
+
summary: `Configure ${desired.memory.type} memory`
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
result.hasChanges = result.toCreate.length > 0;
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
const desiredHash = hashConfig(desired);
|
|
284
|
+
if (desiredHash === actual.configHash) {
|
|
285
|
+
result.noChange.push({
|
|
286
|
+
resource: "agent",
|
|
287
|
+
summary: `Agent "${desired.agent.name}" is up to date (hash: ${desiredHash.slice(0, 8)})`
|
|
288
|
+
});
|
|
289
|
+
return result;
|
|
290
|
+
}
|
|
291
|
+
const actualConfig = actual.config;
|
|
292
|
+
if (desired.agent.name !== actualConfig.agent.name) {
|
|
293
|
+
result.toUpdate.push({
|
|
294
|
+
resource: "agent",
|
|
295
|
+
field: "name",
|
|
296
|
+
oldValue: actualConfig.agent.name,
|
|
297
|
+
newValue: desired.agent.name,
|
|
298
|
+
summary: `Rename agent "${actualConfig.agent.name}" \u2192 "${desired.agent.name}"`
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
if (desired.agent.description !== actualConfig.agent.description) {
|
|
302
|
+
result.toUpdate.push({
|
|
303
|
+
resource: "agent",
|
|
304
|
+
field: "description",
|
|
305
|
+
oldValue: actualConfig.agent.description,
|
|
306
|
+
newValue: desired.agent.description,
|
|
307
|
+
summary: `Update agent description`
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
if (desired.model.provider !== actualConfig.model.provider) {
|
|
311
|
+
result.toUpdate.push({
|
|
312
|
+
resource: "model",
|
|
313
|
+
field: "provider",
|
|
314
|
+
oldValue: actualConfig.model.provider,
|
|
315
|
+
newValue: desired.model.provider,
|
|
316
|
+
summary: `Change model provider: ${actualConfig.model.provider} \u2192 ${desired.model.provider}`
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
if (desired.model.name !== actualConfig.model.name) {
|
|
320
|
+
result.toUpdate.push({
|
|
321
|
+
resource: "model",
|
|
322
|
+
field: "name",
|
|
323
|
+
oldValue: actualConfig.model.name,
|
|
324
|
+
newValue: desired.model.name,
|
|
325
|
+
summary: `Change model: ${actualConfig.model.name} \u2192 ${desired.model.name}`
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
if (desired.model.temperature !== actualConfig.model.temperature) {
|
|
329
|
+
result.toUpdate.push({
|
|
330
|
+
resource: "model",
|
|
331
|
+
field: "temperature",
|
|
332
|
+
oldValue: actualConfig.model.temperature,
|
|
333
|
+
newValue: desired.model.temperature,
|
|
334
|
+
summary: `Change temperature: ${actualConfig.model.temperature} \u2192 ${desired.model.temperature}`
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
if (desired.model.max_tokens !== actualConfig.model.max_tokens) {
|
|
338
|
+
result.toUpdate.push({
|
|
339
|
+
resource: "model",
|
|
340
|
+
field: "max_tokens",
|
|
341
|
+
oldValue: actualConfig.model.max_tokens,
|
|
342
|
+
newValue: desired.model.max_tokens,
|
|
343
|
+
summary: `Change max_tokens: ${actualConfig.model.max_tokens} \u2192 ${desired.model.max_tokens}`
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
const desiredServers = desired.tools?.mcp_servers ?? [];
|
|
347
|
+
const actualServers = actualConfig.tools?.mcp_servers ?? [];
|
|
348
|
+
const actualServerMap = new Map(actualServers.map((s) => [s.name, s]));
|
|
349
|
+
const desiredServerMap = new Map(desiredServers.map((s) => [s.name, s]));
|
|
350
|
+
for (const server of desiredServers) {
|
|
351
|
+
if (!actualServerMap.has(server.name)) {
|
|
352
|
+
result.toCreate.push({
|
|
353
|
+
resource: "mcp_server",
|
|
354
|
+
field: server.name,
|
|
355
|
+
newValue: server,
|
|
356
|
+
summary: `Add MCP server "${server.name}"`
|
|
357
|
+
});
|
|
358
|
+
} else {
|
|
359
|
+
const existing = actualServerMap.get(server.name);
|
|
360
|
+
if (JSON.stringify(server) !== JSON.stringify(existing)) {
|
|
361
|
+
result.toUpdate.push({
|
|
362
|
+
resource: "mcp_server",
|
|
363
|
+
field: server.name,
|
|
364
|
+
oldValue: existing,
|
|
365
|
+
newValue: server,
|
|
366
|
+
summary: `Update MCP server "${server.name}"`
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
for (const server of actualServers) {
|
|
372
|
+
if (!desiredServerMap.has(server.name)) {
|
|
373
|
+
result.toDelete.push({
|
|
374
|
+
resource: "mcp_server",
|
|
375
|
+
field: server.name,
|
|
376
|
+
oldValue: server,
|
|
377
|
+
summary: `Remove MCP server "${server.name}"`
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
result.hasChanges = result.toCreate.length > 0 || result.toUpdate.length > 0 || result.toDelete.length > 0;
|
|
382
|
+
return result;
|
|
383
|
+
}
|
|
384
|
+
function formatPlan(planResult) {
|
|
385
|
+
const lines = [];
|
|
386
|
+
if (!planResult.hasChanges) {
|
|
387
|
+
lines.push("No changes. Infrastructure is up to date.");
|
|
388
|
+
for (const item of planResult.noChange) {
|
|
389
|
+
lines.push(` ${item.summary}`);
|
|
390
|
+
}
|
|
391
|
+
return lines.join("\n");
|
|
392
|
+
}
|
|
393
|
+
if (planResult.toCreate.length > 0) {
|
|
394
|
+
lines.push("Resources to CREATE:");
|
|
395
|
+
for (const item of planResult.toCreate) {
|
|
396
|
+
lines.push(` + ${item.summary}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (planResult.toUpdate.length > 0) {
|
|
400
|
+
lines.push("Resources to UPDATE:");
|
|
401
|
+
for (const item of planResult.toUpdate) {
|
|
402
|
+
lines.push(` ~ ${item.summary}`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (planResult.toDelete.length > 0) {
|
|
406
|
+
lines.push("Resources to DELETE:");
|
|
407
|
+
for (const item of planResult.toDelete) {
|
|
408
|
+
lines.push(` - ${item.summary}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
lines.push(
|
|
412
|
+
`
|
|
413
|
+
Plan: ${planResult.toCreate.length} to add, ${planResult.toUpdate.length} to change, ${planResult.toDelete.length} to destroy.`
|
|
414
|
+
);
|
|
415
|
+
return lines.join("\n");
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/engine/applier.ts
|
|
419
|
+
async function apply(plan2, config, opts) {
|
|
420
|
+
const stateDir = opts.stateDir ?? ".forge";
|
|
421
|
+
if (!plan2.hasChanges) {
|
|
422
|
+
const state2 = createState(config, opts.environment);
|
|
423
|
+
return {
|
|
424
|
+
success: true,
|
|
425
|
+
applied: [],
|
|
426
|
+
skipped: plan2.noChange,
|
|
427
|
+
state: state2
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
if (opts.dryRun) {
|
|
431
|
+
const state2 = createState(config, opts.environment);
|
|
432
|
+
return {
|
|
433
|
+
success: true,
|
|
434
|
+
applied: [],
|
|
435
|
+
skipped: [...plan2.toCreate, ...plan2.toUpdate, ...plan2.toDelete],
|
|
436
|
+
state: state2
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
const applied = [...plan2.toCreate, ...plan2.toUpdate, ...plan2.toDelete];
|
|
440
|
+
const state = createState(config, opts.environment);
|
|
441
|
+
await writeState(stateDir, state);
|
|
442
|
+
return {
|
|
443
|
+
success: true,
|
|
444
|
+
applied,
|
|
445
|
+
skipped: plan2.noChange,
|
|
446
|
+
state
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// src/commands/deploy.ts
|
|
451
|
+
async function deployCommand(opts) {
|
|
452
|
+
const baseConfig = await loadConfig(opts.config);
|
|
453
|
+
const config = resolveEnvironment(baseConfig, opts.env);
|
|
454
|
+
console.log(
|
|
455
|
+
import_chalk2.default.blue("\u2192 Agent:"),
|
|
456
|
+
config.agent.name,
|
|
457
|
+
import_chalk2.default.blue("| Environment:"),
|
|
458
|
+
opts.env,
|
|
459
|
+
import_chalk2.default.blue("| Model:"),
|
|
460
|
+
`${config.model.provider}/${config.model.name}`
|
|
461
|
+
);
|
|
462
|
+
const currentState = await readState(".forge");
|
|
463
|
+
const planResult = plan(config, currentState);
|
|
464
|
+
console.log("\n" + formatPlan(planResult));
|
|
465
|
+
if (!planResult.hasChanges) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (opts.dryRun) {
|
|
469
|
+
console.log(import_chalk2.default.yellow("\n\u26A0 Dry run \u2014 no changes applied."));
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (!opts.autoApprove) {
|
|
473
|
+
console.log(import_chalk2.default.yellow("\nDo you want to apply these changes?"));
|
|
474
|
+
console.log(import_chalk2.default.dim(" Use --auto-approve to skip this prompt.\n"));
|
|
475
|
+
}
|
|
476
|
+
const preHooks = config.hooks?.pre_deploy ?? [];
|
|
477
|
+
const postHooks = config.hooks?.post_deploy ?? [];
|
|
478
|
+
if (preHooks.length > 0 || postHooks.length > 0) {
|
|
479
|
+
console.log(import_chalk2.default.yellow("\n\u26A0 Hooks detected in configuration:"));
|
|
480
|
+
for (const hook of preHooks) {
|
|
481
|
+
console.log(import_chalk2.default.yellow(` pre_deploy: ${hook.run}`));
|
|
482
|
+
}
|
|
483
|
+
for (const hook of postHooks) {
|
|
484
|
+
console.log(import_chalk2.default.yellow(` post_deploy: ${hook.run}`));
|
|
485
|
+
}
|
|
486
|
+
if (!opts.allowHooks) {
|
|
487
|
+
console.log(
|
|
488
|
+
import_chalk2.default.yellow(" Hooks will NOT be executed. Pass --allow-hooks to enable hook execution.\n")
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
const applyOpts = {
|
|
493
|
+
dryRun: opts.dryRun,
|
|
494
|
+
environment: opts.env,
|
|
495
|
+
autoApprove: opts.autoApprove
|
|
496
|
+
};
|
|
497
|
+
const result = await apply(planResult, config, applyOpts);
|
|
498
|
+
if (result.success) {
|
|
499
|
+
console.log(import_chalk2.default.green(`
|
|
500
|
+
\u2713 Successfully deployed "${config.agent.name}" to ${opts.env}`));
|
|
501
|
+
console.log(import_chalk2.default.dim(` State written to .forge/state.json`));
|
|
502
|
+
console.log(import_chalk2.default.dim(` Config hash: ${result.state.configHash.slice(0, 12)}...`));
|
|
503
|
+
} else {
|
|
504
|
+
console.error(import_chalk2.default.red(`
|
|
505
|
+
\u2717 Deploy failed: ${result.error}`));
|
|
506
|
+
process.exit(1);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// src/commands/diff.ts
|
|
511
|
+
var import_chalk3 = __toESM(require("chalk"), 1);
|
|
512
|
+
async function diffCommand(opts) {
|
|
513
|
+
const baseConfig = await loadConfig(opts.config);
|
|
514
|
+
const config = resolveEnvironment(baseConfig, opts.env);
|
|
515
|
+
const currentState = await readState(".forge");
|
|
516
|
+
const planResult = plan(config, currentState);
|
|
517
|
+
if (!planResult.hasChanges) {
|
|
518
|
+
console.log(import_chalk3.default.green("\u2713 No changes. Infrastructure matches configuration."));
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
for (const item of planResult.toCreate) {
|
|
522
|
+
console.log(import_chalk3.default.green(`+ ${item.summary}`));
|
|
523
|
+
if (item.newValue) {
|
|
524
|
+
const lines = JSON.stringify(item.newValue, null, 2).split("\n");
|
|
525
|
+
for (const line of lines) {
|
|
526
|
+
console.log(import_chalk3.default.green(` + ${line}`));
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
for (const item of planResult.toUpdate) {
|
|
531
|
+
console.log(import_chalk3.default.yellow(`~ ${item.summary}`));
|
|
532
|
+
if (item.oldValue !== void 0) {
|
|
533
|
+
console.log(import_chalk3.default.red(` - ${JSON.stringify(item.oldValue)}`));
|
|
534
|
+
}
|
|
535
|
+
if (item.newValue !== void 0) {
|
|
536
|
+
console.log(import_chalk3.default.green(` + ${JSON.stringify(item.newValue)}`));
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
for (const item of planResult.toDelete) {
|
|
540
|
+
console.log(import_chalk3.default.red(`- ${item.summary}`));
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// src/commands/rollback.ts
|
|
545
|
+
var import_chalk4 = __toESM(require("chalk"), 1);
|
|
546
|
+
async function rollbackCommand(opts) {
|
|
547
|
+
const currentState = await readState(".forge");
|
|
548
|
+
if (!currentState) {
|
|
549
|
+
console.error(import_chalk4.default.red("\u2717 No state found. Nothing to roll back."));
|
|
550
|
+
process.exit(1);
|
|
551
|
+
}
|
|
552
|
+
console.log(import_chalk4.default.blue("\u2192 Current state:"));
|
|
553
|
+
console.log(import_chalk4.default.dim(` Agent: ${currentState.agentName}`));
|
|
554
|
+
console.log(import_chalk4.default.dim(` Environment: ${currentState.environment}`));
|
|
555
|
+
console.log(import_chalk4.default.dim(` Deployed: ${currentState.lastDeployed}`));
|
|
556
|
+
console.log(import_chalk4.default.dim(` Hash: ${currentState.configHash.slice(0, 12)}...`));
|
|
557
|
+
if (opts.targetHash) {
|
|
558
|
+
console.log(import_chalk4.default.yellow(`
|
|
559
|
+
\u26A0 Rollback to ${opts.targetHash} is not yet implemented.`));
|
|
560
|
+
console.log(import_chalk4.default.dim(" State history tracking coming in a future release."));
|
|
561
|
+
} else {
|
|
562
|
+
console.log(import_chalk4.default.yellow("\n\u26A0 Specify a target hash to roll back to."));
|
|
563
|
+
console.log(import_chalk4.default.dim(" Usage: forge rollback --target <hash>"));
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// src/commands/validate.ts
|
|
568
|
+
var import_promises3 = require("fs/promises");
|
|
569
|
+
var import_node_path3 = require("path");
|
|
570
|
+
var import_chalk5 = __toESM(require("chalk"), 1);
|
|
571
|
+
async function validateCommand(opts) {
|
|
572
|
+
const configPath = (0, import_node_path3.resolve)(opts.config);
|
|
573
|
+
let raw;
|
|
574
|
+
try {
|
|
575
|
+
raw = await (0, import_promises3.readFile)(configPath, "utf-8");
|
|
576
|
+
} catch {
|
|
577
|
+
console.error(import_chalk5.default.red("\u2717 Could not read config file:"), configPath);
|
|
578
|
+
process.exit(1);
|
|
579
|
+
}
|
|
580
|
+
const result = parseForgeYaml(raw);
|
|
581
|
+
if (result.success) {
|
|
582
|
+
const config = result.config;
|
|
583
|
+
console.log(import_chalk5.default.green("\u2713 Configuration is valid."));
|
|
584
|
+
console.log(import_chalk5.default.dim(` Agent: ${config.agent.name}`));
|
|
585
|
+
console.log(import_chalk5.default.dim(` Model: ${config.model.provider}/${config.model.name}`));
|
|
586
|
+
if (config.environments) {
|
|
587
|
+
const envs = Object.keys(config.environments);
|
|
588
|
+
console.log(import_chalk5.default.dim(` Environments: ${envs.join(", ")}`));
|
|
589
|
+
}
|
|
590
|
+
} else {
|
|
591
|
+
console.error(import_chalk5.default.red("\u2717 Validation failed:"));
|
|
592
|
+
for (const err of result.errors ?? []) {
|
|
593
|
+
console.error(import_chalk5.default.red(` \u2022 ${err}`));
|
|
594
|
+
}
|
|
595
|
+
process.exit(1);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// src/index.ts
|
|
600
|
+
var program = new import_commander.Command();
|
|
601
|
+
program.name("forge").description("Agent infrastructure as code \u2014 the Terraform for AI agents").version("0.1.0");
|
|
602
|
+
program.command("deploy").description("Deploy an agent from a forge.yaml configuration").option("-c, --config <path>", "Path to forge.yaml", "forge.yaml").option("-e, --env <environment>", "Target environment", "dev").option("--auto-approve", "Skip confirmation prompt", false).option("--dry-run", "Show plan without applying changes", false).option("--allow-hooks", "Allow execution of pre_deploy and post_deploy hooks", false).action((opts) => {
|
|
603
|
+
return deployCommand({
|
|
604
|
+
config: opts.config,
|
|
605
|
+
env: opts.env,
|
|
606
|
+
autoApprove: opts.autoApprove,
|
|
607
|
+
dryRun: opts.dryRun,
|
|
608
|
+
allowHooks: opts.allowHooks
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
program.command("diff").description("Show what would change between config and deployed state").option("-c, --config <path>", "Path to forge.yaml", "forge.yaml").option("-e, --env <environment>", "Target environment", "dev").action((opts) => {
|
|
612
|
+
return diffCommand({
|
|
613
|
+
config: opts.config,
|
|
614
|
+
env: opts.env
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
program.command("rollback").description("Roll back to a previous deployment state").option("--target <hash>", "Target state hash to roll back to").action((opts) => {
|
|
618
|
+
return rollbackCommand({
|
|
619
|
+
targetHash: opts.target
|
|
620
|
+
});
|
|
621
|
+
});
|
|
622
|
+
program.command("validate").description("Validate a forge.yaml configuration file").option("-c, --config <path>", "Path to forge.yaml", "forge.yaml").action((opts) => {
|
|
623
|
+
return validateCommand({
|
|
624
|
+
config: opts.config
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
program.parse();
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
#!/usr/bin/env node
|
|
3
|
+
|
|
4
|
+
// src/index.ts
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
|
|
7
|
+
// src/commands/deploy.ts
|
|
8
|
+
import chalk2 from "chalk";
|
|
9
|
+
|
|
10
|
+
// src/parser/validate.ts
|
|
11
|
+
import { parse as parseYaml } from "yaml";
|
|
12
|
+
|
|
13
|
+
// src/parser/schema.ts
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
var modelProviderSchema = z.enum(["anthropic", "openai", "google", "ollama", "bedrock"]);
|
|
16
|
+
var modelConfigSchema = z.object({
|
|
17
|
+
provider: modelProviderSchema,
|
|
18
|
+
name: z.string().min(1),
|
|
19
|
+
temperature: z.number().min(0).max(2).optional(),
|
|
20
|
+
max_tokens: z.number().int().positive().optional()
|
|
21
|
+
});
|
|
22
|
+
var systemPromptSchema = z.object({
|
|
23
|
+
file: z.string().optional(),
|
|
24
|
+
inline: z.string().optional()
|
|
25
|
+
}).refine(
|
|
26
|
+
(data) => data.file || data.inline,
|
|
27
|
+
{ message: "system_prompt must specify either 'file' or 'inline'" }
|
|
28
|
+
);
|
|
29
|
+
var mcpServerSchema = z.object({
|
|
30
|
+
name: z.string().min(1),
|
|
31
|
+
command: z.string().min(1),
|
|
32
|
+
args: z.array(z.string()).optional(),
|
|
33
|
+
env: z.record(z.string()).optional()
|
|
34
|
+
});
|
|
35
|
+
var toolsConfigSchema = z.object({
|
|
36
|
+
mcp_servers: z.array(mcpServerSchema).optional()
|
|
37
|
+
});
|
|
38
|
+
var memoryTypeSchema = z.enum(["none", "in-context", "vector"]);
|
|
39
|
+
var memoryProviderSchema = z.enum(["chroma", "pinecone", "weaviate"]);
|
|
40
|
+
var memoryConfigSchema = z.object({
|
|
41
|
+
type: memoryTypeSchema,
|
|
42
|
+
provider: memoryProviderSchema.optional(),
|
|
43
|
+
collection: z.string().optional()
|
|
44
|
+
}).refine(
|
|
45
|
+
(data) => data.type !== "vector" || data.provider,
|
|
46
|
+
{ message: "vector memory type requires a provider" }
|
|
47
|
+
);
|
|
48
|
+
var hookStepSchema = z.object({
|
|
49
|
+
run: z.string().min(1)
|
|
50
|
+
});
|
|
51
|
+
var hooksConfigSchema = z.object({
|
|
52
|
+
pre_deploy: z.array(hookStepSchema).optional(),
|
|
53
|
+
post_deploy: z.array(hookStepSchema).optional()
|
|
54
|
+
});
|
|
55
|
+
var environmentOverrideSchema = z.object({
|
|
56
|
+
model: modelConfigSchema.partial().optional(),
|
|
57
|
+
tools: toolsConfigSchema.optional(),
|
|
58
|
+
memory: memoryConfigSchema.optional()
|
|
59
|
+
});
|
|
60
|
+
var agentConfigSchema = z.object({
|
|
61
|
+
name: z.string().min(1).regex(/^[a-z0-9-]+$/, "Agent name must be lowercase alphanumeric with hyphens"),
|
|
62
|
+
description: z.string().optional()
|
|
63
|
+
});
|
|
64
|
+
var forgeConfigSchema = z.object({
|
|
65
|
+
version: z.literal("1"),
|
|
66
|
+
agent: agentConfigSchema,
|
|
67
|
+
model: modelConfigSchema,
|
|
68
|
+
system_prompt: systemPromptSchema.optional(),
|
|
69
|
+
tools: toolsConfigSchema.optional(),
|
|
70
|
+
memory: memoryConfigSchema.optional(),
|
|
71
|
+
environments: z.record(environmentOverrideSchema).optional(),
|
|
72
|
+
hooks: hooksConfigSchema.optional()
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// src/parser/validate.ts
|
|
76
|
+
function parseForgeYaml(raw) {
|
|
77
|
+
let parsed;
|
|
78
|
+
try {
|
|
79
|
+
parsed = parseYaml(raw);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
return {
|
|
82
|
+
success: false,
|
|
83
|
+
errors: [`YAML parse error: ${err instanceof Error ? err.message : String(err)}`]
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const result = forgeConfigSchema.safeParse(parsed);
|
|
87
|
+
if (!result.success) {
|
|
88
|
+
return {
|
|
89
|
+
success: false,
|
|
90
|
+
errors: result.error.issues.map(
|
|
91
|
+
(issue) => `${issue.path.join(".")}: ${issue.message}`
|
|
92
|
+
)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
success: true,
|
|
97
|
+
config: result.data
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function resolveEnvironment(config, env) {
|
|
101
|
+
if (!config.environments?.[env]) {
|
|
102
|
+
return config;
|
|
103
|
+
}
|
|
104
|
+
const override = config.environments[env];
|
|
105
|
+
const resolved = { ...config };
|
|
106
|
+
if (override.model) {
|
|
107
|
+
resolved.model = { ...config.model, ...override.model };
|
|
108
|
+
}
|
|
109
|
+
if (override.tools) {
|
|
110
|
+
resolved.tools = override.tools;
|
|
111
|
+
}
|
|
112
|
+
if (override.memory) {
|
|
113
|
+
resolved.memory = override.memory;
|
|
114
|
+
}
|
|
115
|
+
return resolved;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/parser/load.ts
|
|
119
|
+
import { readFile } from "fs/promises";
|
|
120
|
+
import { resolve } from "path";
|
|
121
|
+
import chalk from "chalk";
|
|
122
|
+
async function loadConfig(configPath) {
|
|
123
|
+
const resolved = resolve(configPath);
|
|
124
|
+
console.log(chalk.blue("\u2192 Reading configuration from"), resolved);
|
|
125
|
+
let raw;
|
|
126
|
+
try {
|
|
127
|
+
raw = await readFile(resolved, "utf-8");
|
|
128
|
+
} catch {
|
|
129
|
+
console.error(chalk.red("\u2717 Could not read config file:"), resolved);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
const result = parseForgeYaml(raw);
|
|
133
|
+
if (!result.success || !result.config) {
|
|
134
|
+
console.error(chalk.red("\u2717 Validation errors:"));
|
|
135
|
+
for (const err of result.errors ?? []) {
|
|
136
|
+
console.error(chalk.red(` \u2022 ${err}`));
|
|
137
|
+
}
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
return result.config;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/engine/state.ts
|
|
144
|
+
import { readFile as readFile2, writeFile, mkdir } from "fs/promises";
|
|
145
|
+
import { createHash } from "crypto";
|
|
146
|
+
import { join } from "path";
|
|
147
|
+
var STATE_FILE = "state.json";
|
|
148
|
+
function sortDeep(obj) {
|
|
149
|
+
if (Array.isArray(obj)) return obj.map(sortDeep);
|
|
150
|
+
if (obj !== null && typeof obj === "object") {
|
|
151
|
+
const record = obj;
|
|
152
|
+
return Object.fromEntries(
|
|
153
|
+
Object.keys(record).sort().map((key) => [key, sortDeep(record[key])])
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return obj;
|
|
157
|
+
}
|
|
158
|
+
function hashConfig(config) {
|
|
159
|
+
const normalized = JSON.stringify(sortDeep(config));
|
|
160
|
+
return createHash("sha256").update(normalized).digest("hex");
|
|
161
|
+
}
|
|
162
|
+
async function readState(stateDir) {
|
|
163
|
+
const statePath = join(stateDir, STATE_FILE);
|
|
164
|
+
try {
|
|
165
|
+
const raw = await readFile2(statePath, "utf-8");
|
|
166
|
+
const parsed = JSON.parse(raw);
|
|
167
|
+
if (typeof parsed !== "object" || parsed === null || typeof parsed.configHash !== "string" || typeof parsed.agentName !== "string") {
|
|
168
|
+
console.warn("Warning: State file has invalid structure. Treating as no prior state.");
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
return parsed;
|
|
172
|
+
} catch (err) {
|
|
173
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
console.warn("Warning: Failed to read state file. Treating as no prior state.");
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function writeState(stateDir, state) {
|
|
181
|
+
const statePath = join(stateDir, STATE_FILE);
|
|
182
|
+
await mkdir(stateDir, { recursive: true, mode: 448 });
|
|
183
|
+
await writeFile(statePath, JSON.stringify(state, null, 2), {
|
|
184
|
+
encoding: "utf-8",
|
|
185
|
+
mode: 384
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
function redactConfig(config) {
|
|
189
|
+
const cloned = JSON.parse(JSON.stringify(config));
|
|
190
|
+
if (cloned.tools?.mcp_servers) {
|
|
191
|
+
for (const server of cloned.tools.mcp_servers) {
|
|
192
|
+
if (server.env) {
|
|
193
|
+
for (const [key, value] of Object.entries(server.env)) {
|
|
194
|
+
if (!/^\$\{.+\}$/.test(value)) {
|
|
195
|
+
server.env[key] = "[REDACTED]";
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return cloned;
|
|
202
|
+
}
|
|
203
|
+
function createState(config, environment) {
|
|
204
|
+
return {
|
|
205
|
+
configHash: hashConfig(config),
|
|
206
|
+
lastDeployed: (/* @__PURE__ */ new Date()).toISOString(),
|
|
207
|
+
environment,
|
|
208
|
+
agentName: config.agent.name,
|
|
209
|
+
config: redactConfig(config)
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// src/engine/planner.ts
|
|
214
|
+
function plan(desired, actual) {
|
|
215
|
+
const result = {
|
|
216
|
+
toCreate: [],
|
|
217
|
+
toUpdate: [],
|
|
218
|
+
toDelete: [],
|
|
219
|
+
noChange: [],
|
|
220
|
+
hasChanges: false
|
|
221
|
+
};
|
|
222
|
+
if (!actual) {
|
|
223
|
+
result.toCreate.push({
|
|
224
|
+
resource: "agent",
|
|
225
|
+
newValue: desired.agent,
|
|
226
|
+
summary: `Create agent "${desired.agent.name}"`
|
|
227
|
+
});
|
|
228
|
+
result.toCreate.push({
|
|
229
|
+
resource: "model",
|
|
230
|
+
newValue: desired.model,
|
|
231
|
+
summary: `Configure model ${desired.model.provider}/${desired.model.name}`
|
|
232
|
+
});
|
|
233
|
+
if (desired.system_prompt) {
|
|
234
|
+
result.toCreate.push({
|
|
235
|
+
resource: "system_prompt",
|
|
236
|
+
newValue: desired.system_prompt,
|
|
237
|
+
summary: `Set system prompt from ${desired.system_prompt.file ?? "inline"}`
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
if (desired.tools?.mcp_servers) {
|
|
241
|
+
for (const server of desired.tools.mcp_servers) {
|
|
242
|
+
result.toCreate.push({
|
|
243
|
+
resource: "mcp_server",
|
|
244
|
+
field: server.name,
|
|
245
|
+
newValue: server,
|
|
246
|
+
summary: `Add MCP server "${server.name}"`
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (desired.memory && desired.memory.type !== "none") {
|
|
251
|
+
result.toCreate.push({
|
|
252
|
+
resource: "memory",
|
|
253
|
+
newValue: desired.memory,
|
|
254
|
+
summary: `Configure ${desired.memory.type} memory`
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
result.hasChanges = result.toCreate.length > 0;
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
const desiredHash = hashConfig(desired);
|
|
261
|
+
if (desiredHash === actual.configHash) {
|
|
262
|
+
result.noChange.push({
|
|
263
|
+
resource: "agent",
|
|
264
|
+
summary: `Agent "${desired.agent.name}" is up to date (hash: ${desiredHash.slice(0, 8)})`
|
|
265
|
+
});
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
const actualConfig = actual.config;
|
|
269
|
+
if (desired.agent.name !== actualConfig.agent.name) {
|
|
270
|
+
result.toUpdate.push({
|
|
271
|
+
resource: "agent",
|
|
272
|
+
field: "name",
|
|
273
|
+
oldValue: actualConfig.agent.name,
|
|
274
|
+
newValue: desired.agent.name,
|
|
275
|
+
summary: `Rename agent "${actualConfig.agent.name}" \u2192 "${desired.agent.name}"`
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
if (desired.agent.description !== actualConfig.agent.description) {
|
|
279
|
+
result.toUpdate.push({
|
|
280
|
+
resource: "agent",
|
|
281
|
+
field: "description",
|
|
282
|
+
oldValue: actualConfig.agent.description,
|
|
283
|
+
newValue: desired.agent.description,
|
|
284
|
+
summary: `Update agent description`
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
if (desired.model.provider !== actualConfig.model.provider) {
|
|
288
|
+
result.toUpdate.push({
|
|
289
|
+
resource: "model",
|
|
290
|
+
field: "provider",
|
|
291
|
+
oldValue: actualConfig.model.provider,
|
|
292
|
+
newValue: desired.model.provider,
|
|
293
|
+
summary: `Change model provider: ${actualConfig.model.provider} \u2192 ${desired.model.provider}`
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
if (desired.model.name !== actualConfig.model.name) {
|
|
297
|
+
result.toUpdate.push({
|
|
298
|
+
resource: "model",
|
|
299
|
+
field: "name",
|
|
300
|
+
oldValue: actualConfig.model.name,
|
|
301
|
+
newValue: desired.model.name,
|
|
302
|
+
summary: `Change model: ${actualConfig.model.name} \u2192 ${desired.model.name}`
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
if (desired.model.temperature !== actualConfig.model.temperature) {
|
|
306
|
+
result.toUpdate.push({
|
|
307
|
+
resource: "model",
|
|
308
|
+
field: "temperature",
|
|
309
|
+
oldValue: actualConfig.model.temperature,
|
|
310
|
+
newValue: desired.model.temperature,
|
|
311
|
+
summary: `Change temperature: ${actualConfig.model.temperature} \u2192 ${desired.model.temperature}`
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
if (desired.model.max_tokens !== actualConfig.model.max_tokens) {
|
|
315
|
+
result.toUpdate.push({
|
|
316
|
+
resource: "model",
|
|
317
|
+
field: "max_tokens",
|
|
318
|
+
oldValue: actualConfig.model.max_tokens,
|
|
319
|
+
newValue: desired.model.max_tokens,
|
|
320
|
+
summary: `Change max_tokens: ${actualConfig.model.max_tokens} \u2192 ${desired.model.max_tokens}`
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
const desiredServers = desired.tools?.mcp_servers ?? [];
|
|
324
|
+
const actualServers = actualConfig.tools?.mcp_servers ?? [];
|
|
325
|
+
const actualServerMap = new Map(actualServers.map((s) => [s.name, s]));
|
|
326
|
+
const desiredServerMap = new Map(desiredServers.map((s) => [s.name, s]));
|
|
327
|
+
for (const server of desiredServers) {
|
|
328
|
+
if (!actualServerMap.has(server.name)) {
|
|
329
|
+
result.toCreate.push({
|
|
330
|
+
resource: "mcp_server",
|
|
331
|
+
field: server.name,
|
|
332
|
+
newValue: server,
|
|
333
|
+
summary: `Add MCP server "${server.name}"`
|
|
334
|
+
});
|
|
335
|
+
} else {
|
|
336
|
+
const existing = actualServerMap.get(server.name);
|
|
337
|
+
if (JSON.stringify(server) !== JSON.stringify(existing)) {
|
|
338
|
+
result.toUpdate.push({
|
|
339
|
+
resource: "mcp_server",
|
|
340
|
+
field: server.name,
|
|
341
|
+
oldValue: existing,
|
|
342
|
+
newValue: server,
|
|
343
|
+
summary: `Update MCP server "${server.name}"`
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
for (const server of actualServers) {
|
|
349
|
+
if (!desiredServerMap.has(server.name)) {
|
|
350
|
+
result.toDelete.push({
|
|
351
|
+
resource: "mcp_server",
|
|
352
|
+
field: server.name,
|
|
353
|
+
oldValue: server,
|
|
354
|
+
summary: `Remove MCP server "${server.name}"`
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
result.hasChanges = result.toCreate.length > 0 || result.toUpdate.length > 0 || result.toDelete.length > 0;
|
|
359
|
+
return result;
|
|
360
|
+
}
|
|
361
|
+
function formatPlan(planResult) {
|
|
362
|
+
const lines = [];
|
|
363
|
+
if (!planResult.hasChanges) {
|
|
364
|
+
lines.push("No changes. Infrastructure is up to date.");
|
|
365
|
+
for (const item of planResult.noChange) {
|
|
366
|
+
lines.push(` ${item.summary}`);
|
|
367
|
+
}
|
|
368
|
+
return lines.join("\n");
|
|
369
|
+
}
|
|
370
|
+
if (planResult.toCreate.length > 0) {
|
|
371
|
+
lines.push("Resources to CREATE:");
|
|
372
|
+
for (const item of planResult.toCreate) {
|
|
373
|
+
lines.push(` + ${item.summary}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (planResult.toUpdate.length > 0) {
|
|
377
|
+
lines.push("Resources to UPDATE:");
|
|
378
|
+
for (const item of planResult.toUpdate) {
|
|
379
|
+
lines.push(` ~ ${item.summary}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (planResult.toDelete.length > 0) {
|
|
383
|
+
lines.push("Resources to DELETE:");
|
|
384
|
+
for (const item of planResult.toDelete) {
|
|
385
|
+
lines.push(` - ${item.summary}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
lines.push(
|
|
389
|
+
`
|
|
390
|
+
Plan: ${planResult.toCreate.length} to add, ${planResult.toUpdate.length} to change, ${planResult.toDelete.length} to destroy.`
|
|
391
|
+
);
|
|
392
|
+
return lines.join("\n");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// src/engine/applier.ts
|
|
396
|
+
async function apply(plan2, config, opts) {
|
|
397
|
+
const stateDir = opts.stateDir ?? ".forge";
|
|
398
|
+
if (!plan2.hasChanges) {
|
|
399
|
+
const state2 = createState(config, opts.environment);
|
|
400
|
+
return {
|
|
401
|
+
success: true,
|
|
402
|
+
applied: [],
|
|
403
|
+
skipped: plan2.noChange,
|
|
404
|
+
state: state2
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
if (opts.dryRun) {
|
|
408
|
+
const state2 = createState(config, opts.environment);
|
|
409
|
+
return {
|
|
410
|
+
success: true,
|
|
411
|
+
applied: [],
|
|
412
|
+
skipped: [...plan2.toCreate, ...plan2.toUpdate, ...plan2.toDelete],
|
|
413
|
+
state: state2
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
const applied = [...plan2.toCreate, ...plan2.toUpdate, ...plan2.toDelete];
|
|
417
|
+
const state = createState(config, opts.environment);
|
|
418
|
+
await writeState(stateDir, state);
|
|
419
|
+
return {
|
|
420
|
+
success: true,
|
|
421
|
+
applied,
|
|
422
|
+
skipped: plan2.noChange,
|
|
423
|
+
state
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// src/commands/deploy.ts
|
|
428
|
+
async function deployCommand(opts) {
|
|
429
|
+
const baseConfig = await loadConfig(opts.config);
|
|
430
|
+
const config = resolveEnvironment(baseConfig, opts.env);
|
|
431
|
+
console.log(
|
|
432
|
+
chalk2.blue("\u2192 Agent:"),
|
|
433
|
+
config.agent.name,
|
|
434
|
+
chalk2.blue("| Environment:"),
|
|
435
|
+
opts.env,
|
|
436
|
+
chalk2.blue("| Model:"),
|
|
437
|
+
`${config.model.provider}/${config.model.name}`
|
|
438
|
+
);
|
|
439
|
+
const currentState = await readState(".forge");
|
|
440
|
+
const planResult = plan(config, currentState);
|
|
441
|
+
console.log("\n" + formatPlan(planResult));
|
|
442
|
+
if (!planResult.hasChanges) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (opts.dryRun) {
|
|
446
|
+
console.log(chalk2.yellow("\n\u26A0 Dry run \u2014 no changes applied."));
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (!opts.autoApprove) {
|
|
450
|
+
console.log(chalk2.yellow("\nDo you want to apply these changes?"));
|
|
451
|
+
console.log(chalk2.dim(" Use --auto-approve to skip this prompt.\n"));
|
|
452
|
+
}
|
|
453
|
+
const preHooks = config.hooks?.pre_deploy ?? [];
|
|
454
|
+
const postHooks = config.hooks?.post_deploy ?? [];
|
|
455
|
+
if (preHooks.length > 0 || postHooks.length > 0) {
|
|
456
|
+
console.log(chalk2.yellow("\n\u26A0 Hooks detected in configuration:"));
|
|
457
|
+
for (const hook of preHooks) {
|
|
458
|
+
console.log(chalk2.yellow(` pre_deploy: ${hook.run}`));
|
|
459
|
+
}
|
|
460
|
+
for (const hook of postHooks) {
|
|
461
|
+
console.log(chalk2.yellow(` post_deploy: ${hook.run}`));
|
|
462
|
+
}
|
|
463
|
+
if (!opts.allowHooks) {
|
|
464
|
+
console.log(
|
|
465
|
+
chalk2.yellow(" Hooks will NOT be executed. Pass --allow-hooks to enable hook execution.\n")
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const applyOpts = {
|
|
470
|
+
dryRun: opts.dryRun,
|
|
471
|
+
environment: opts.env,
|
|
472
|
+
autoApprove: opts.autoApprove
|
|
473
|
+
};
|
|
474
|
+
const result = await apply(planResult, config, applyOpts);
|
|
475
|
+
if (result.success) {
|
|
476
|
+
console.log(chalk2.green(`
|
|
477
|
+
\u2713 Successfully deployed "${config.agent.name}" to ${opts.env}`));
|
|
478
|
+
console.log(chalk2.dim(` State written to .forge/state.json`));
|
|
479
|
+
console.log(chalk2.dim(` Config hash: ${result.state.configHash.slice(0, 12)}...`));
|
|
480
|
+
} else {
|
|
481
|
+
console.error(chalk2.red(`
|
|
482
|
+
\u2717 Deploy failed: ${result.error}`));
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// src/commands/diff.ts
|
|
488
|
+
import chalk3 from "chalk";
|
|
489
|
+
async function diffCommand(opts) {
|
|
490
|
+
const baseConfig = await loadConfig(opts.config);
|
|
491
|
+
const config = resolveEnvironment(baseConfig, opts.env);
|
|
492
|
+
const currentState = await readState(".forge");
|
|
493
|
+
const planResult = plan(config, currentState);
|
|
494
|
+
if (!planResult.hasChanges) {
|
|
495
|
+
console.log(chalk3.green("\u2713 No changes. Infrastructure matches configuration."));
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
for (const item of planResult.toCreate) {
|
|
499
|
+
console.log(chalk3.green(`+ ${item.summary}`));
|
|
500
|
+
if (item.newValue) {
|
|
501
|
+
const lines = JSON.stringify(item.newValue, null, 2).split("\n");
|
|
502
|
+
for (const line of lines) {
|
|
503
|
+
console.log(chalk3.green(` + ${line}`));
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
for (const item of planResult.toUpdate) {
|
|
508
|
+
console.log(chalk3.yellow(`~ ${item.summary}`));
|
|
509
|
+
if (item.oldValue !== void 0) {
|
|
510
|
+
console.log(chalk3.red(` - ${JSON.stringify(item.oldValue)}`));
|
|
511
|
+
}
|
|
512
|
+
if (item.newValue !== void 0) {
|
|
513
|
+
console.log(chalk3.green(` + ${JSON.stringify(item.newValue)}`));
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
for (const item of planResult.toDelete) {
|
|
517
|
+
console.log(chalk3.red(`- ${item.summary}`));
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// src/commands/rollback.ts
|
|
522
|
+
import chalk4 from "chalk";
|
|
523
|
+
async function rollbackCommand(opts) {
|
|
524
|
+
const currentState = await readState(".forge");
|
|
525
|
+
if (!currentState) {
|
|
526
|
+
console.error(chalk4.red("\u2717 No state found. Nothing to roll back."));
|
|
527
|
+
process.exit(1);
|
|
528
|
+
}
|
|
529
|
+
console.log(chalk4.blue("\u2192 Current state:"));
|
|
530
|
+
console.log(chalk4.dim(` Agent: ${currentState.agentName}`));
|
|
531
|
+
console.log(chalk4.dim(` Environment: ${currentState.environment}`));
|
|
532
|
+
console.log(chalk4.dim(` Deployed: ${currentState.lastDeployed}`));
|
|
533
|
+
console.log(chalk4.dim(` Hash: ${currentState.configHash.slice(0, 12)}...`));
|
|
534
|
+
if (opts.targetHash) {
|
|
535
|
+
console.log(chalk4.yellow(`
|
|
536
|
+
\u26A0 Rollback to ${opts.targetHash} is not yet implemented.`));
|
|
537
|
+
console.log(chalk4.dim(" State history tracking coming in a future release."));
|
|
538
|
+
} else {
|
|
539
|
+
console.log(chalk4.yellow("\n\u26A0 Specify a target hash to roll back to."));
|
|
540
|
+
console.log(chalk4.dim(" Usage: forge rollback --target <hash>"));
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// src/commands/validate.ts
|
|
545
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
546
|
+
import { resolve as resolve2 } from "path";
|
|
547
|
+
import chalk5 from "chalk";
|
|
548
|
+
async function validateCommand(opts) {
|
|
549
|
+
const configPath = resolve2(opts.config);
|
|
550
|
+
let raw;
|
|
551
|
+
try {
|
|
552
|
+
raw = await readFile3(configPath, "utf-8");
|
|
553
|
+
} catch {
|
|
554
|
+
console.error(chalk5.red("\u2717 Could not read config file:"), configPath);
|
|
555
|
+
process.exit(1);
|
|
556
|
+
}
|
|
557
|
+
const result = parseForgeYaml(raw);
|
|
558
|
+
if (result.success) {
|
|
559
|
+
const config = result.config;
|
|
560
|
+
console.log(chalk5.green("\u2713 Configuration is valid."));
|
|
561
|
+
console.log(chalk5.dim(` Agent: ${config.agent.name}`));
|
|
562
|
+
console.log(chalk5.dim(` Model: ${config.model.provider}/${config.model.name}`));
|
|
563
|
+
if (config.environments) {
|
|
564
|
+
const envs = Object.keys(config.environments);
|
|
565
|
+
console.log(chalk5.dim(` Environments: ${envs.join(", ")}`));
|
|
566
|
+
}
|
|
567
|
+
} else {
|
|
568
|
+
console.error(chalk5.red("\u2717 Validation failed:"));
|
|
569
|
+
for (const err of result.errors ?? []) {
|
|
570
|
+
console.error(chalk5.red(` \u2022 ${err}`));
|
|
571
|
+
}
|
|
572
|
+
process.exit(1);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// src/index.ts
|
|
577
|
+
var program = new Command();
|
|
578
|
+
program.name("forge").description("Agent infrastructure as code \u2014 the Terraform for AI agents").version("0.1.0");
|
|
579
|
+
program.command("deploy").description("Deploy an agent from a forge.yaml configuration").option("-c, --config <path>", "Path to forge.yaml", "forge.yaml").option("-e, --env <environment>", "Target environment", "dev").option("--auto-approve", "Skip confirmation prompt", false).option("--dry-run", "Show plan without applying changes", false).option("--allow-hooks", "Allow execution of pre_deploy and post_deploy hooks", false).action((opts) => {
|
|
580
|
+
return deployCommand({
|
|
581
|
+
config: opts.config,
|
|
582
|
+
env: opts.env,
|
|
583
|
+
autoApprove: opts.autoApprove,
|
|
584
|
+
dryRun: opts.dryRun,
|
|
585
|
+
allowHooks: opts.allowHooks
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
program.command("diff").description("Show what would change between config and deployed state").option("-c, --config <path>", "Path to forge.yaml", "forge.yaml").option("-e, --env <environment>", "Target environment", "dev").action((opts) => {
|
|
589
|
+
return diffCommand({
|
|
590
|
+
config: opts.config,
|
|
591
|
+
env: opts.env
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
program.command("rollback").description("Roll back to a previous deployment state").option("--target <hash>", "Target state hash to roll back to").action((opts) => {
|
|
595
|
+
return rollbackCommand({
|
|
596
|
+
targetHash: opts.target
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
program.command("validate").description("Validate a forge.yaml configuration file").option("-c, --config <path>", "Path to forge.yaml", "forge.yaml").action((opts) => {
|
|
600
|
+
return validateCommand({
|
|
601
|
+
config: opts.config
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@openforge-ai/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Agent infrastructure as code — the Terraform for AI agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/seanfraserio/forge.git",
|
|
10
|
+
"directory": "packages/cli"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"forge",
|
|
20
|
+
"ai",
|
|
21
|
+
"agents",
|
|
22
|
+
"infrastructure-as-code",
|
|
23
|
+
"mcp",
|
|
24
|
+
"cli",
|
|
25
|
+
"terraform"
|
|
26
|
+
],
|
|
27
|
+
"bin": {
|
|
28
|
+
"forge": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"main": "./dist/index.cjs",
|
|
31
|
+
"module": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"import": "./dist/index.js",
|
|
36
|
+
"require": "./dist/index.cjs"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"chalk": "^5.3.0",
|
|
41
|
+
"commander": "^12.0.0",
|
|
42
|
+
"yaml": "^2.4.0",
|
|
43
|
+
"zod": "^3.22.0",
|
|
44
|
+
"@openforge-ai/sdk": "0.1.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^20.0.0",
|
|
48
|
+
"tsup": "^8.0.0",
|
|
49
|
+
"typescript": "^5.4.0",
|
|
50
|
+
"vitest": "^1.4.0"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsup",
|
|
54
|
+
"test": "vitest run",
|
|
55
|
+
"dev": "tsup src/index.ts --format esm,cjs --dts --watch",
|
|
56
|
+
"typecheck": "tsc --noEmit"
|
|
57
|
+
}
|
|
58
|
+
}
|