@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 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();
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -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
+ }