@ronaldjdevfs/forge 1.3.0-beta → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -4
- package/package.json +1 -1
- package/skills/forge/SKILL.md +57 -128
- package/skills/forge/command/forge.md +59 -14
- package/skills/forge/reference/adr.md +242 -0
- package/skills/forge/reference/anti-corruption-layer.md +340 -0
- package/skills/forge/reference/api-design.md +7 -0
- package/skills/forge/reference/api-versioning.md +354 -0
- package/skills/forge/reference/architectural-depth-checklist.md +311 -0
- package/skills/forge/reference/architecture-template.md +41 -0
- package/skills/forge/reference/assay.md +6 -0
- package/skills/forge/reference/bounded-contexts.md +311 -0
- package/skills/forge/reference/chain.md +6 -0
- package/skills/forge/reference/cohesion-checklist.md +256 -0
- package/skills/forge/reference/cqrs.md +286 -0
- package/skills/forge/reference/data-patterns.md +6 -0
- package/skills/forge/reference/di-strategies.md +6 -0
- package/skills/forge/reference/errors.md +5 -0
- package/skills/forge/reference/events.md +8 -0
- package/skills/forge/reference/evolutionary-architecture.md +300 -0
- package/skills/forge/reference/forge.md +7 -0
- package/skills/forge/reference/hooks.md +6 -0
- package/skills/forge/reference/idempotency.md +283 -0
- package/skills/forge/reference/inscribe.md +5 -0
- package/skills/forge/reference/inspect.md +6 -0
- package/skills/forge/reference/modular-monolith.md +252 -0
- package/skills/forge/reference/observability.md +5 -0
- package/skills/forge/reference/quench.md +5 -0
- package/skills/forge/reference/relocate.md +6 -0
- package/skills/forge/reference/sagas.md +359 -0
- package/skills/forge/reference/security-patterns.md +6 -0
- package/skills/forge/reference/smelt.md +6 -0
- package/skills/forge/reference/temper.md +6 -0
- package/skills/forge/reference/testing-patterns.md +6 -0
- package/skills/forge/reference/transactional-outbox.md +311 -0
- package/skills/forge/scripts/architecture.mjs +10 -5
- package/skills/forge/scripts/assay.mjs +2 -2
- package/skills/forge/scripts/chain.mjs +31 -5
- package/skills/forge/scripts/context.mjs +24 -4
- package/skills/forge/scripts/detect.mjs +57 -36
- package/skills/forge/scripts/forge-boot.mjs +108 -0
- package/skills/forge/scripts/forge-config.mjs +182 -3
- package/skills/forge/scripts/forge-state.mjs +1 -1
- package/skills/forge/scripts/forgeSentinel-lib.mjs +2 -2
- package/skills/forge/scripts/forgeSentinel.mjs +2 -2
- package/skills/forge/scripts/forgeSmith.mjs +2 -2
- package/skills/forge/scripts/graph.mjs +65 -9
- package/skills/forge/scripts/hook.mjs +2 -2
- package/skills/forge/scripts/inspect.mjs +56 -48
- package/skills/forge/scripts/parse-imports.mjs +0 -2
- package/skills/forge/scripts/posttool.mjs +211 -17
- package/skills/forge/scripts/recommendation-engine.mjs +125 -0
- package/skills/forge/scripts/rename.mjs +62 -14
- package/skills/forge/scripts/rollback.mjs +5 -3
- package/skills/forge/templates/feature/acl-gateway.ts.md +52 -0
- package/skills/forge/templates/feature/acl-repository.ts.md +36 -0
- package/skills/forge/templates/feature/acl-translator.ts.md +24 -0
- package/skills/forge/templates/feature/cqrs-query.ts.md +40 -0
- package/skills/forge/templates/feature/entity.ts.md +1 -1
- package/skills/forge/templates/feature/mapper.ts.md +1 -1
- package/skills/forge/templates/feature/outbox-repository.ts.md +36 -0
- package/skills/forge/templates/feature/repository-impl.ts.md +2 -2
- package/skills/forge/templates/feature/repository-interface.ts.md +2 -2
- package/skills/forge/templates/feature/saga-orchestrator.ts.md +72 -0
- package/skills/forge/templates/feature/schema.ts.md +1 -1
- package/skills/forge/templates/feature/use-case.ts.md +2 -2
- package/skills/forge/templates/platform/outbox-relayer.ts.md +80 -0
- package/skills/forge/tests/core.test.mjs +288 -4
- package/src/cli.js +26 -13
|
@@ -125,6 +125,9 @@ describe("profile.mjs", () => {
|
|
|
125
125
|
});
|
|
126
126
|
|
|
127
127
|
describe("graph.mjs", () => {
|
|
128
|
+
before(() => scaffoldProject({}));
|
|
129
|
+
after(() => cleanup());
|
|
130
|
+
|
|
128
131
|
it("builds an empty graph for an empty project", async () => {
|
|
129
132
|
const { buildGraph } = await import("../scripts/graph.mjs");
|
|
130
133
|
const graph = buildGraph(process.cwd());
|
|
@@ -138,6 +141,9 @@ describe("graph.mjs", () => {
|
|
|
138
141
|
});
|
|
139
142
|
|
|
140
143
|
describe("armorer.mjs", () => {
|
|
144
|
+
before(() => scaffoldProject({}));
|
|
145
|
+
after(() => cleanup());
|
|
146
|
+
|
|
141
147
|
it("builds a healthy report for an empty project", async () => {
|
|
142
148
|
const { buildOwnershipReport } = await import("../scripts/armorer.mjs");
|
|
143
149
|
const report = buildOwnershipReport(process.cwd());
|
|
@@ -180,6 +186,9 @@ describe("forge-config.mjs", () => {
|
|
|
180
186
|
});
|
|
181
187
|
|
|
182
188
|
describe("chain.mjs", () => {
|
|
189
|
+
before(() => scaffoldProject({}));
|
|
190
|
+
after(() => cleanup());
|
|
191
|
+
|
|
183
192
|
it("builds an empty dependency graph for empty project", async () => {
|
|
184
193
|
const { buildDependencyGraph } = await import("../scripts/chain.mjs");
|
|
185
194
|
const graph = buildDependencyGraph();
|
|
@@ -318,10 +327,13 @@ describe("detect.mjs — inline ignores", () => {
|
|
|
318
327
|
});
|
|
319
328
|
});
|
|
320
329
|
|
|
321
|
-
describe("
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
330
|
+
describe("posttool.mjs", () => {
|
|
331
|
+
before(() => scaffoldProject({}));
|
|
332
|
+
after(() => cleanup());
|
|
333
|
+
|
|
334
|
+
it("postToolCheck returns empty for no files", async () => {
|
|
335
|
+
const { postToolCheck } = await import("../scripts/posttool.mjs");
|
|
336
|
+
const result = await postToolCheck([], {});
|
|
325
337
|
assert.equal(result.total, 0);
|
|
326
338
|
assert.ok(result.summary);
|
|
327
339
|
});
|
|
@@ -401,3 +413,275 @@ describe("assay.mjs", () => {
|
|
|
401
413
|
assert.ok(opinion.includes("R1") || opinion.includes("R8") || opinion.includes("acoplamiento") || opinion.includes("importan"));
|
|
402
414
|
});
|
|
403
415
|
});
|
|
416
|
+
|
|
417
|
+
describe("transactional-outbox pattern", () => {
|
|
418
|
+
it("outbox entry tracks lifecycle states", () => {
|
|
419
|
+
const entry = { id: "1", eventType: "OrderPlaced", processedAt: null, retryCount: 0, lastError: null };
|
|
420
|
+
assert.equal(entry.processedAt, null);
|
|
421
|
+
assert.equal(entry.retryCount, 0);
|
|
422
|
+
|
|
423
|
+
entry.processedAt = new Date();
|
|
424
|
+
entry.retryCount = 2;
|
|
425
|
+
entry.lastError = "timeout";
|
|
426
|
+
assert.ok(entry.processedAt instanceof Date);
|
|
427
|
+
assert.equal(entry.retryCount, 2);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("retry policy respects max retries and backoff", () => {
|
|
431
|
+
const maxRetries = 3;
|
|
432
|
+
const baseDelay = 100;
|
|
433
|
+
let attempts = 0;
|
|
434
|
+
|
|
435
|
+
async function executeWithRetry(fn) {
|
|
436
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
437
|
+
try {
|
|
438
|
+
return await fn();
|
|
439
|
+
} catch (error) {
|
|
440
|
+
if (attempt === maxRetries) throw error;
|
|
441
|
+
// exponential backoff: baseDelay * 2^(attempt-1)
|
|
442
|
+
const delay = baseDelay * Math.pow(2, attempt - 1);
|
|
443
|
+
// not actually waiting in test
|
|
444
|
+
void delay;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
assert.rejects(async () => {
|
|
450
|
+
await executeWithRetry(() => Promise.reject(new Error("fail")));
|
|
451
|
+
}, /fail/);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("outbox relayer moves event to DLQ after exceeding retries", () => {
|
|
455
|
+
const MAX_RETRIES = 10;
|
|
456
|
+
const entry = { id: "bad-event", retryCount: 9 };
|
|
457
|
+
assert.ok(entry.retryCount < MAX_RETRIES);
|
|
458
|
+
|
|
459
|
+
entry.retryCount++;
|
|
460
|
+
assert.equal(entry.retryCount, 10);
|
|
461
|
+
|
|
462
|
+
const shouldGoToDLQ = entry.retryCount >= MAX_RETRIES;
|
|
463
|
+
assert.ok(shouldGoToDLQ);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it("outbox entry requires event_id, aggregate_id, and payload", () => {
|
|
467
|
+
const validEntry = {
|
|
468
|
+
id: crypto.randomUUID(),
|
|
469
|
+
eventType: "OrderPlaced",
|
|
470
|
+
aggregateId: "order-123",
|
|
471
|
+
aggregateType: "Order",
|
|
472
|
+
payload: { total: 100 },
|
|
473
|
+
occurredAt: new Date(),
|
|
474
|
+
processedAt: null,
|
|
475
|
+
retryCount: 0,
|
|
476
|
+
lastError: null,
|
|
477
|
+
};
|
|
478
|
+
assert.ok(validEntry.id);
|
|
479
|
+
assert.ok(validEntry.aggregateId);
|
|
480
|
+
assert.ok(validEntry.payload);
|
|
481
|
+
assert.equal(validEntry.eventType, "OrderPlaced");
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it("processed outbox entries are no longer picked up by relayer", () => {
|
|
485
|
+
const unprocessed = { processedAt: null };
|
|
486
|
+
const processed = { processedAt: new Date() };
|
|
487
|
+
|
|
488
|
+
const isPending = (entry) => entry.processedAt === null;
|
|
489
|
+
assert.ok(isPending(unprocessed));
|
|
490
|
+
assert.ok(!isPending(processed));
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
describe("idempotency pattern", () => {
|
|
495
|
+
it("idempotency key must be a valid UUID", () => {
|
|
496
|
+
function isValidUUID(key) {
|
|
497
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(key);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
assert.ok(isValidUUID("550e8400-e29b-41d4-a716-446655440000"));
|
|
501
|
+
assert.ok(!isValidUUID("not-a-uuid"));
|
|
502
|
+
assert.ok(!isValidUUID(""));
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it("same idempotency key returns cached response", async () => {
|
|
506
|
+
const cache = new Map();
|
|
507
|
+
|
|
508
|
+
async function handleRequest(key, handler) {
|
|
509
|
+
if (cache.has(key)) return cache.get(key);
|
|
510
|
+
const result = await handler();
|
|
511
|
+
cache.set(key, result);
|
|
512
|
+
return result;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
let callCount = 0;
|
|
516
|
+
async function processPayment() {
|
|
517
|
+
callCount++;
|
|
518
|
+
return { status: "success", transactionId: "txn-1" };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const key = "550e8400-e29b-41d4-a716-446655440000";
|
|
522
|
+
const result1 = await handleRequest(key, processPayment);
|
|
523
|
+
const result2 = await handleRequest(key, processPayment);
|
|
524
|
+
|
|
525
|
+
assert.equal(result1.status, "success");
|
|
526
|
+
assert.equal(result2.status, "success");
|
|
527
|
+
assert.equal(callCount, 1); // solo se ejecutó una vez
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it("different idempotency keys create separate resources", async () => {
|
|
531
|
+
const store = new Map();
|
|
532
|
+
|
|
533
|
+
async function createResource(key, data) {
|
|
534
|
+
if (store.has(key)) return store.get(key);
|
|
535
|
+
const resource = { id: crypto.randomUUID(), ...data };
|
|
536
|
+
store.set(key, resource);
|
|
537
|
+
return resource;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const r1 = await createResource("key-1", { name: "A" });
|
|
541
|
+
const r2 = await createResource("key-2", { name: "B" });
|
|
542
|
+
|
|
543
|
+
assert.notEqual(r1.id, r2.id);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it("idempotency key has TTL and expires", () => {
|
|
547
|
+
const entry = { key: "test", createdAt: new Date() };
|
|
548
|
+
const TTL_MS = 24 * 60 * 60 * 1000; // 24 horas
|
|
549
|
+
|
|
550
|
+
function isExpired(entry) {
|
|
551
|
+
return Date.now() - entry.createdAt.getTime() > TTL_MS;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
assert.ok(!isExpired(entry)); // creado ahora, no ha expirado
|
|
555
|
+
|
|
556
|
+
const oldEntry = { key: "old", createdAt: new Date(Date.now() - TTL_MS - 1) };
|
|
557
|
+
assert.ok(isExpired(oldEntry));
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it("only POST, PATCH, PUT need idempotency keys", () => {
|
|
561
|
+
const methods = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
562
|
+
const needsKey = (method) => ["POST", "PATCH", "PUT"].includes(method);
|
|
563
|
+
|
|
564
|
+
assert.ok(!needsKey("GET"));
|
|
565
|
+
assert.ok(needsKey("POST"));
|
|
566
|
+
assert.ok(needsKey("PUT"));
|
|
567
|
+
assert.ok(needsKey("PATCH"));
|
|
568
|
+
assert.ok(!needsKey("DELETE"));
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
describe("anti-corruption-layer pattern", () => {
|
|
573
|
+
it("translator maps external DTO to domain entity", () => {
|
|
574
|
+
class CustomerTranslator {
|
|
575
|
+
toDomain(dto) {
|
|
576
|
+
return {
|
|
577
|
+
id: dto.customerId,
|
|
578
|
+
name: `${dto.firstName} ${dto.lastName}`,
|
|
579
|
+
email: dto.emailAddress,
|
|
580
|
+
status: dto.active ? "active" : "inactive",
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const externalDTO = {
|
|
586
|
+
customerId: "ext-123",
|
|
587
|
+
firstName: "John",
|
|
588
|
+
lastName: "Doe",
|
|
589
|
+
emailAddress: "john@example.com",
|
|
590
|
+
active: true,
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
const translator = new CustomerTranslator();
|
|
594
|
+
const entity = translator.toDomain(externalDTO);
|
|
595
|
+
|
|
596
|
+
assert.equal(entity.id, "ext-123");
|
|
597
|
+
assert.equal(entity.name, "John Doe");
|
|
598
|
+
assert.equal(entity.email, "john@example.com");
|
|
599
|
+
assert.equal(entity.status, "active");
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it("translator maps domain entity back to external DTO", () => {
|
|
603
|
+
class CustomerTranslator {
|
|
604
|
+
toExternal(entity) {
|
|
605
|
+
return {
|
|
606
|
+
customerId: entity.id,
|
|
607
|
+
firstName: entity.name.split(" ")[0],
|
|
608
|
+
lastName: entity.name.split(" ").slice(1).join(" "),
|
|
609
|
+
emailAddress: entity.email,
|
|
610
|
+
active: entity.status === "active",
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const entity = {
|
|
616
|
+
id: "dom-456",
|
|
617
|
+
name: "Jane Smith",
|
|
618
|
+
email: "jane@example.com",
|
|
619
|
+
status: "active",
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
const translator = new CustomerTranslator();
|
|
623
|
+
const dto = translator.toExternal(entity);
|
|
624
|
+
|
|
625
|
+
assert.equal(dto.customerId, "dom-456");
|
|
626
|
+
assert.equal(dto.firstName, "Jane");
|
|
627
|
+
assert.equal(dto.lastName, "Smith");
|
|
628
|
+
assert.equal(dto.active, true);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it("translator handles null/missing values gracefully", () => {
|
|
632
|
+
class CustomerTranslator {
|
|
633
|
+
toDomain(dto) {
|
|
634
|
+
return {
|
|
635
|
+
id: dto.customerId ?? "unknown",
|
|
636
|
+
name: dto.firstName ? `${dto.firstName} ${dto.lastName ?? ""}`.trim() : "Unknown",
|
|
637
|
+
email: dto.emailAddress ?? "",
|
|
638
|
+
status: dto.active === true ? "active" : "inactive",
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const emptyDTO = { customerId: null, firstName: null, lastName: null, emailAddress: null, active: null };
|
|
644
|
+
const translator = new CustomerTranslator();
|
|
645
|
+
const entity = translator.toDomain(emptyDTO);
|
|
646
|
+
|
|
647
|
+
assert.equal(entity.id, "unknown");
|
|
648
|
+
assert.equal(entity.name, "Unknown");
|
|
649
|
+
assert.equal(entity.status, "inactive");
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it("gateway enforces timeout and returns null on 404", () => {
|
|
653
|
+
async function fetchCustomer(id) {
|
|
654
|
+
const response = { status: 404, ok: false, statusText: "Not Found" };
|
|
655
|
+
if (response.status === 404) return null;
|
|
656
|
+
if (!response.ok) throw new Error(response.statusText);
|
|
657
|
+
return { id };
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
assert.doesNotReject(async () => {
|
|
661
|
+
const result = await fetchCustomer("nonexistent");
|
|
662
|
+
assert.equal(result, null);
|
|
663
|
+
});
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it("ACL delegates to translator and gateway in correct order", async () => {
|
|
667
|
+
let order = [];
|
|
668
|
+
|
|
669
|
+
const gateway = {
|
|
670
|
+
async fetch(id) { order.push("gateway"); return { customerId: id, firstName: "A", lastName: "B", emailAddress: "a@b.com", active: true }; },
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
const translator = {
|
|
674
|
+
toDomain(dto) { order.push("translator"); return { id: dto.customerId, name: `${dto.firstName} ${dto.lastName}`, email: dto.emailAddress, status: "active" }; },
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
async function findById(id) {
|
|
678
|
+
const dto = await gateway.fetch(id);
|
|
679
|
+
return dto ? translator.toDomain(dto) : null;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const result = await findById("123");
|
|
683
|
+
assert.deepEqual(order, ["gateway", "translator"]);
|
|
684
|
+
assert.equal(result.id, "123");
|
|
685
|
+
assert.equal(result.name, "A B");
|
|
686
|
+
});
|
|
687
|
+
});
|
package/src/cli.js
CHANGED
|
@@ -86,16 +86,20 @@ function ensureDependencies(configDir) {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
const COMMANDS = [
|
|
89
|
-
{ name: "forge-forge", desc: "Forge — inicializar proyecto arquitectónicamente" },
|
|
90
|
-
{ name: "forge-cast", desc: "Cast — crear un nuevo feature hexagonal desde cero" },
|
|
91
|
-
{ name: "forge-inspect", desc: "Inspect — inspeccionar la conformidad arquitectónica" },
|
|
92
|
-
{ name: "forge-
|
|
93
|
-
{ name: "forge-
|
|
94
|
-
{ name: "forge-
|
|
95
|
-
{ name: "forge-
|
|
96
|
-
{ name: "forge-
|
|
97
|
-
{ name: "forge-
|
|
98
|
-
{ name: "forge-
|
|
89
|
+
{ name: "forge-forge", desc: "Forge — inicializar proyecto arquitectónicamente", flags: "" },
|
|
90
|
+
{ name: "forge-cast", desc: "Cast — crear un nuevo feature hexagonal desde cero", flags: "" },
|
|
91
|
+
{ name: "forge-inspect", desc: "Inspect — inspeccionar la conformidad arquitectónica", flags: "--json | --diff | --full | --summary | --severity=<nivel> | --force" },
|
|
92
|
+
{ name: "forge-assay", desc: "Assay — ensayo arquitectónico multi-persona", flags: "--persona=<id> | --json | --save | history" },
|
|
93
|
+
{ name: "forge-graph", desc: "Graph — construir el grafo arquitectónico del proyecto", flags: "--json" },
|
|
94
|
+
{ name: "forge-armorer", desc: "Armorer — reporte de ownership, huérfanos y duplicados", flags: "" },
|
|
95
|
+
{ name: "forge-bootstrap", desc: "Bootstrap — inicializar platform, shared e infra layers", flags: "" },
|
|
96
|
+
{ name: "forge-relocate", desc: "Relocate — migrar feature legacy a la nueva estructura", flags: "" },
|
|
97
|
+
{ name: "forge-reforge", desc: "Reforge — refactorizar la arquitectura de un feature", flags: "--cycles" },
|
|
98
|
+
{ name: "forge-quench", desc: "Quench — verificar reglas arquitectónicas del proyecto", flags: "--fix | --show-ignores | --severity=<nivel> | --json" },
|
|
99
|
+
{ name: "forge-temper", desc: "Temper — endurecer la arquitectura (DI, seguridad)", flags: "" },
|
|
100
|
+
{ name: "forge-chain", desc: "Chain — analizar cadena de dependencias entre features", flags: "--json" },
|
|
101
|
+
{ name: "forge-inscribe", desc: "Inscribe — generar y mantener ARCHITECTURE.md", flags: "--output=<path>" },
|
|
102
|
+
{ name: "forge-smelt", desc: "Smelt — extraer código reutilizable a shared/", flags: "" },
|
|
99
103
|
];
|
|
100
104
|
|
|
101
105
|
function generateCommands(configDir) {
|
|
@@ -103,7 +107,8 @@ function generateCommands(configDir) {
|
|
|
103
107
|
mkdirSync(cmdsDir, { recursive: true });
|
|
104
108
|
for (const cmd of COMMANDS) {
|
|
105
109
|
const sub = cmd.name.replace("forge-", "");
|
|
106
|
-
const
|
|
110
|
+
const hasFlags = cmd.flags && cmd.flags.length > 0;
|
|
111
|
+
const lines = [
|
|
107
112
|
"---",
|
|
108
113
|
`description: ${cmd.desc}`,
|
|
109
114
|
"agent: build",
|
|
@@ -111,8 +116,16 @@ function generateCommands(configDir) {
|
|
|
111
116
|
"",
|
|
112
117
|
`Ejecuta el subcomando ${sub} de Forge con los argumentos: $ARGUMENTS`,
|
|
113
118
|
"",
|
|
114
|
-
]
|
|
115
|
-
|
|
119
|
+
];
|
|
120
|
+
if (hasFlags) {
|
|
121
|
+
lines.push(
|
|
122
|
+
`Flags disponibles: ${cmd.flags}`,
|
|
123
|
+
"",
|
|
124
|
+
`Si $ARGUMENTS está vacío, pregunta al usuario qué flags quiere usar con la tool question (checkboxes múltiples). Si no selecciona ninguna, ejecuta sin flags.`,
|
|
125
|
+
"",
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
writeFileSync(join(cmdsDir, `${cmd.name}.md`), lines.join("\n"));
|
|
116
129
|
}
|
|
117
130
|
}
|
|
118
131
|
|