forge-cw 0.1.5 → 0.1.7
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/console/assets/{index-D_58OQhA.css → index-C3X9_uj-.css} +1 -1
- package/console/assets/index-DSHKQ0Wq.js +36 -0
- package/console/index.html +2 -2
- package/dist/index.js +96 -21
- package/modules/mod-qa/forge-module.json +74 -0
- package/package.json +5 -4
- package/console/assets/index-CER8DSNM.js +0 -33
package/console/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Forge Console</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-DSHKQ0Wq.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C3X9_uj-.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="app"></div>
|
package/dist/index.js
CHANGED
|
@@ -191,24 +191,36 @@ var init_db = __esm({
|
|
|
191
191
|
// ../core/dist/modules.js
|
|
192
192
|
import { readdirSync, readFileSync, existsSync } from "node:fs";
|
|
193
193
|
import { join } from "node:path";
|
|
194
|
-
var ModuleLoader;
|
|
194
|
+
var BUNDLED_MODULES, ModuleLoader;
|
|
195
195
|
var init_modules = __esm({
|
|
196
196
|
"../core/dist/modules.js"() {
|
|
197
197
|
"use strict";
|
|
198
|
+
BUNDLED_MODULES = ["mod-qa"];
|
|
198
199
|
ModuleLoader = class {
|
|
199
200
|
modulesDir;
|
|
201
|
+
bundledDir;
|
|
200
202
|
loaded = /* @__PURE__ */ new Map();
|
|
201
|
-
constructor(modulesDir) {
|
|
203
|
+
constructor(modulesDir, bundledDir) {
|
|
202
204
|
this.modulesDir = modulesDir;
|
|
205
|
+
this.bundledDir = bundledDir;
|
|
203
206
|
}
|
|
204
207
|
discover() {
|
|
205
208
|
this.loaded.clear();
|
|
206
|
-
if (!existsSync(this.modulesDir))
|
|
207
|
-
return [];
|
|
208
|
-
const entries = readdirSync(this.modulesDir, { withFileTypes: true });
|
|
209
209
|
const manifests = [];
|
|
210
|
-
|
|
211
|
-
|
|
210
|
+
const bundled = /* @__PURE__ */ new Set();
|
|
211
|
+
if (this.bundledDir) {
|
|
212
|
+
for (const name of BUNDLED_MODULES) {
|
|
213
|
+
const manifest = this.loadFrom(this.bundledDir, name);
|
|
214
|
+
if (!manifest)
|
|
215
|
+
continue;
|
|
216
|
+
bundled.add(name);
|
|
217
|
+
manifests.push(manifest);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (!existsSync(this.modulesDir))
|
|
221
|
+
return manifests;
|
|
222
|
+
for (const entry of readdirSync(this.modulesDir, { withFileTypes: true })) {
|
|
223
|
+
if (!entry.isDirectory() || bundled.has(entry.name))
|
|
212
224
|
continue;
|
|
213
225
|
const manifest = this.load(entry.name);
|
|
214
226
|
if (manifest)
|
|
@@ -217,7 +229,10 @@ var init_modules = __esm({
|
|
|
217
229
|
return manifests;
|
|
218
230
|
}
|
|
219
231
|
load(dirName) {
|
|
220
|
-
|
|
232
|
+
return this.loadFrom(this.modulesDir, dirName);
|
|
233
|
+
}
|
|
234
|
+
loadFrom(dir, dirName) {
|
|
235
|
+
const manifestPath = join(dir, dirName, "forge-module.json");
|
|
221
236
|
if (!existsSync(manifestPath))
|
|
222
237
|
return void 0;
|
|
223
238
|
try {
|
|
@@ -307,6 +322,47 @@ var init_runner = __esm({
|
|
|
307
322
|
}
|
|
308
323
|
});
|
|
309
324
|
|
|
325
|
+
// ../core/dist/action-params.js
|
|
326
|
+
function validateParams(def, params) {
|
|
327
|
+
const given = params ?? {};
|
|
328
|
+
if (typeof given !== "object" || Array.isArray(given)) {
|
|
329
|
+
return { error: "params must be an object of strings" };
|
|
330
|
+
}
|
|
331
|
+
const values = given;
|
|
332
|
+
const declared = def.params ?? {};
|
|
333
|
+
const extra = Object.keys(values).filter((k) => !Object.hasOwn(declared, k));
|
|
334
|
+
if (extra.length > 0) {
|
|
335
|
+
return { error: `Unknown param${extra.length > 1 ? "s" : ""} for action ${def.id}: ${extra.join(", ")}` };
|
|
336
|
+
}
|
|
337
|
+
const env = {};
|
|
338
|
+
for (const [name, { pattern }] of Object.entries(declared)) {
|
|
339
|
+
if (!NAME.test(name))
|
|
340
|
+
return { error: `Action ${def.id} declares an invalid param name: ${name}` };
|
|
341
|
+
let re;
|
|
342
|
+
try {
|
|
343
|
+
re = new RegExp(`^(?:${pattern})$`);
|
|
344
|
+
} catch {
|
|
345
|
+
return { error: `Action ${def.id} declares an invalid pattern for ${name}` };
|
|
346
|
+
}
|
|
347
|
+
const value = values[name];
|
|
348
|
+
if (value === void 0)
|
|
349
|
+
return { error: `Missing param ${name}` };
|
|
350
|
+
if (typeof value !== "string")
|
|
351
|
+
return { error: `Param ${name} must be a string` };
|
|
352
|
+
if (!re.test(value))
|
|
353
|
+
return { error: `Param ${name} does not match ${pattern}` };
|
|
354
|
+
env[`FORGE_PARAM_${name.toUpperCase()}`] = value;
|
|
355
|
+
}
|
|
356
|
+
return { env };
|
|
357
|
+
}
|
|
358
|
+
var NAME;
|
|
359
|
+
var init_action_params = __esm({
|
|
360
|
+
"../core/dist/action-params.js"() {
|
|
361
|
+
"use strict";
|
|
362
|
+
NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
|
|
310
366
|
// ../core/dist/auth.js
|
|
311
367
|
function bearerAuth(token) {
|
|
312
368
|
return async (c, next) => {
|
|
@@ -3886,7 +3942,7 @@ function createForgeServer(options) {
|
|
|
3886
3942
|
const dbPath = join12(dataDir, "forge.db");
|
|
3887
3943
|
const modulesDir = join12(dataDir, "modules");
|
|
3888
3944
|
const db = externalDb ?? new ForgeDB(dbPath);
|
|
3889
|
-
const loader = new ModuleLoader(modulesDir);
|
|
3945
|
+
const loader = new ModuleLoader(modulesDir, options.bundledModulesDir);
|
|
3890
3946
|
const runner = new ActionRunner();
|
|
3891
3947
|
loader.discover();
|
|
3892
3948
|
const app = new Hono4();
|
|
@@ -3894,7 +3950,7 @@ function createForgeServer(options) {
|
|
|
3894
3950
|
if (authToken) {
|
|
3895
3951
|
app.use("/api/*", bearerAuth(authToken));
|
|
3896
3952
|
}
|
|
3897
|
-
const cwReader = new CWReader();
|
|
3953
|
+
const cwReader = new CWReader(options.cwDir);
|
|
3898
3954
|
const loginManager = new LoginManager(resolveCwBin(cwReader.cwHome));
|
|
3899
3955
|
app.route("/api/cw", cwRoutes(cwReader, { loginManager, localOnly }));
|
|
3900
3956
|
app.route("/api/skills", skillRoutes(cwReader));
|
|
@@ -3931,25 +3987,37 @@ function createForgeServer(options) {
|
|
|
3931
3987
|
async function resolveAction(c) {
|
|
3932
3988
|
const moduleName = c.req.param("module");
|
|
3933
3989
|
const actionId = c.req.param("action");
|
|
3934
|
-
const { projectId } = await c.req.json();
|
|
3990
|
+
const { projectId, cwProject, params } = await c.req.json();
|
|
3935
3991
|
const action = loader.getAction(moduleName, actionId);
|
|
3936
3992
|
if (!action) {
|
|
3937
3993
|
return c.json({ error: "Action not found" }, 404);
|
|
3938
3994
|
}
|
|
3939
|
-
const
|
|
3940
|
-
|
|
3995
|
+
const checked = validateParams(action, params);
|
|
3996
|
+
if ("error" in checked) {
|
|
3997
|
+
return c.json({ error: checked.error }, 400);
|
|
3998
|
+
}
|
|
3999
|
+
let cwd;
|
|
4000
|
+
if (cwProject !== void 0) {
|
|
4001
|
+
const projects = cwReader.getProjects();
|
|
4002
|
+
const path = Object.hasOwn(projects, cwProject) ? projects[cwProject]?.path : void 0;
|
|
4003
|
+
if (!path)
|
|
4004
|
+
return c.json({ error: `Unknown CW project: ${cwProject}` }, 400);
|
|
4005
|
+
cwd = path;
|
|
4006
|
+
} else {
|
|
4007
|
+
cwd = (projectId ? db.getProject(projectId) : void 0)?.path ?? process.cwd();
|
|
4008
|
+
}
|
|
3941
4009
|
const logId = db.logAction({
|
|
3942
4010
|
projectId: projectId ?? null,
|
|
3943
4011
|
moduleId: moduleName,
|
|
3944
4012
|
actionId,
|
|
3945
4013
|
command: action.command
|
|
3946
4014
|
});
|
|
3947
|
-
return { action, cwd, logId };
|
|
4015
|
+
return { action, cwd, logId, env: checked.env };
|
|
3948
4016
|
}
|
|
3949
4017
|
app.get("/api/health", (c) => {
|
|
3950
4018
|
return c.json({
|
|
3951
4019
|
status: "ok",
|
|
3952
|
-
version:
|
|
4020
|
+
version: options.version ?? null,
|
|
3953
4021
|
modules: loader.getLoadedCount()
|
|
3954
4022
|
});
|
|
3955
4023
|
});
|
|
@@ -3982,8 +4050,8 @@ function createForgeServer(options) {
|
|
|
3982
4050
|
const resolved = await resolveAction(c);
|
|
3983
4051
|
if (resolved instanceof Response)
|
|
3984
4052
|
return resolved;
|
|
3985
|
-
const { action, cwd, logId } = resolved;
|
|
3986
|
-
const result2 = await runner.exec(action.command, { cwd });
|
|
4053
|
+
const { action, cwd, logId, env } = resolved;
|
|
4054
|
+
const result2 = await runner.exec(action.command, { cwd, env });
|
|
3987
4055
|
db.completeAction(logId, result2.exitCode);
|
|
3988
4056
|
return c.json({
|
|
3989
4057
|
logId,
|
|
@@ -3996,7 +4064,7 @@ function createForgeServer(options) {
|
|
|
3996
4064
|
const resolved = await resolveAction(c);
|
|
3997
4065
|
if (resolved instanceof Response)
|
|
3998
4066
|
return resolved;
|
|
3999
|
-
const { action, cwd, logId } = resolved;
|
|
4067
|
+
const { action, cwd, logId, env } = resolved;
|
|
4000
4068
|
return new Response(new ReadableStream({
|
|
4001
4069
|
async start(controller) {
|
|
4002
4070
|
const encoder = new TextEncoder();
|
|
@@ -4009,6 +4077,7 @@ data: ${JSON.stringify(data)}
|
|
|
4009
4077
|
send("start", { logId, command: action.command });
|
|
4010
4078
|
const result2 = await runner.exec(action.command, {
|
|
4011
4079
|
cwd,
|
|
4080
|
+
env,
|
|
4012
4081
|
onData: (chunk) => send("output", { chunk })
|
|
4013
4082
|
});
|
|
4014
4083
|
db.completeAction(logId, result2.exitCode);
|
|
@@ -4104,6 +4173,7 @@ var init_server = __esm({
|
|
|
4104
4173
|
init_db();
|
|
4105
4174
|
init_modules();
|
|
4106
4175
|
init_runner();
|
|
4176
|
+
init_action_params();
|
|
4107
4177
|
init_auth();
|
|
4108
4178
|
init_origin_guard();
|
|
4109
4179
|
init_cw_reader();
|
|
@@ -4489,6 +4559,7 @@ __export(dist_exports, {
|
|
|
4489
4559
|
resolveListenOptions: () => resolveListenOptions,
|
|
4490
4560
|
skillRoutes: () => skillRoutes,
|
|
4491
4561
|
supports: () => supports,
|
|
4562
|
+
validateParams: () => validateParams,
|
|
4492
4563
|
waitForListening: () => waitForListening
|
|
4493
4564
|
});
|
|
4494
4565
|
var init_dist = __esm({
|
|
@@ -4498,6 +4569,7 @@ var init_dist = __esm({
|
|
|
4498
4569
|
init_db();
|
|
4499
4570
|
init_modules();
|
|
4500
4571
|
init_runner();
|
|
4572
|
+
init_action_params();
|
|
4501
4573
|
init_auth();
|
|
4502
4574
|
init_origin_guard();
|
|
4503
4575
|
init_db_factory();
|
|
@@ -4517,7 +4589,7 @@ var init_dist = __esm({
|
|
|
4517
4589
|
});
|
|
4518
4590
|
|
|
4519
4591
|
// src/index.ts
|
|
4520
|
-
import { existsSync as existsSync14 } from "node:fs";
|
|
4592
|
+
import { existsSync as existsSync14, readFileSync as readFileSync10 } from "node:fs";
|
|
4521
4593
|
import { homedir as homedir6 } from "node:os";
|
|
4522
4594
|
import { join as join15 } from "node:path";
|
|
4523
4595
|
function parseArgs(argv) {
|
|
@@ -4533,6 +4605,8 @@ function parseArgs(argv) {
|
|
|
4533
4605
|
var packageRoot = join15(import.meta.dirname, "..");
|
|
4534
4606
|
var isCheckout = existsSync14(join15(packageRoot, "src"));
|
|
4535
4607
|
var consoleDist = isCheckout ? join15(packageRoot, "../console/dist") : join15(packageRoot, "console");
|
|
4608
|
+
var forgeVersion = JSON.parse(readFileSync10(join15(packageRoot, "package.json"), "utf-8")).version;
|
|
4609
|
+
var bundledModulesDir = isCheckout ? join15(packageRoot, "../../modules") : join15(packageRoot, "modules");
|
|
4536
4610
|
async function main() {
|
|
4537
4611
|
const { ensureForgeDir: ensureForgeDir2, createForgeServer: createForgeServer2, createDatabase: createDatabase2, resolveListenOptions: resolveListenOptions2, ensureCw: ensureCw2, pathWithCw: pathWithCw2, quietSqliteWarning: quietSqliteWarning2, waitForListening: waitForListening2, probePort: probePort2, portInUseMessage: portInUseMessage2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
|
|
4538
4612
|
quietSqliteWarning2();
|
|
@@ -4562,10 +4636,11 @@ async function main() {
|
|
|
4562
4636
|
port,
|
|
4563
4637
|
db,
|
|
4564
4638
|
authToken: isTeam ? authToken : void 0,
|
|
4565
|
-
localOnly
|
|
4639
|
+
localOnly,
|
|
4640
|
+
bundledModulesDir,
|
|
4641
|
+
version: forgeVersion
|
|
4566
4642
|
});
|
|
4567
4643
|
const { serveStatic } = await import("@hono/node-server/serve-static");
|
|
4568
|
-
const { readFileSync: readFileSync10 } = await import("node:fs");
|
|
4569
4644
|
if (existsSync14(join15(consoleDist, "index.html"))) {
|
|
4570
4645
|
server.app.use("/*", serveStatic({ root: consoleDist }));
|
|
4571
4646
|
const indexHtml = readFileSync10(join15(consoleDist, "index.html"), "utf-8");
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@forge-dev/mod-qa",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"displayName": "Quality Assurance",
|
|
5
|
+
"description": "qa-pilot escalation inbox: pull requests that need a human, with the decision's gates, checks, findings and visual changes",
|
|
6
|
+
"icon": "shield-check",
|
|
7
|
+
"color": "#10b981",
|
|
8
|
+
"panels": [
|
|
9
|
+
{ "id": "escalations", "title": "Escalamientos", "component": "./panels/Escalations", "default": true }
|
|
10
|
+
],
|
|
11
|
+
"actions": [
|
|
12
|
+
{
|
|
13
|
+
"id": "list-escalations",
|
|
14
|
+
"label": "List Escalations",
|
|
15
|
+
"icon": "inbox",
|
|
16
|
+
"command": "gh pr list --label qa:needs-human --state open --json number,title,url,updatedAt,author,headRefOid --limit 50",
|
|
17
|
+
"tags": ["qa"],
|
|
18
|
+
"hidden": true
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "get-decision",
|
|
22
|
+
"label": "Get Decision",
|
|
23
|
+
"icon": "file-search",
|
|
24
|
+
"command": "gh pr view \"$FORGE_PARAM_PR\" --json number,title,url,headRefOid,headRefName,author,comments,additions,deletions,changedFiles",
|
|
25
|
+
"params": { "PR": { "pattern": "\\d+" } },
|
|
26
|
+
"tags": ["qa"],
|
|
27
|
+
"hidden": true
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "get-evidence",
|
|
31
|
+
"label": "Get Evidence Image",
|
|
32
|
+
"icon": "image",
|
|
33
|
+
"command": "node -e \"const out = require('child_process').execFileSync('gh', ['api', '-H', 'Accept: application/vnd.github.raw', 'repos/{owner}/{repo}/contents/' + process.env.FORGE_PARAM_PATH + '?ref=' + process.env.FORGE_PARAM_REF], { maxBuffer: 8e6 }); process.stdout.write(out.toString('base64'))\"",
|
|
34
|
+
"params": {
|
|
35
|
+
"REF": { "pattern": "[0-9a-f]{40}" },
|
|
36
|
+
"PATH": { "pattern": "pr-\\d+/[0-9a-f]{7}/artifacts/[A-Za-z0-9._-]+\\.png" }
|
|
37
|
+
},
|
|
38
|
+
"tags": ["qa"],
|
|
39
|
+
"hidden": true
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"id": "week-stats",
|
|
43
|
+
"label": "Week Stats",
|
|
44
|
+
"icon": "bar-chart",
|
|
45
|
+
"command": "gh pr list --state all --search \"updated:>=$(node -e 'process.stdout.write(new Date(Date.now() - 7 * 864e5).toISOString().slice(0, 10))')\" --json number,labels --limit 200",
|
|
46
|
+
"tags": ["qa"],
|
|
47
|
+
"hidden": true
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"id": "approve",
|
|
51
|
+
"label": "Approve",
|
|
52
|
+
"icon": "check",
|
|
53
|
+
"command": "gh pr edit \"$FORGE_PARAM_PR\" --add-label qa:approved",
|
|
54
|
+
"params": { "PR": { "pattern": "\\d+" } },
|
|
55
|
+
"tags": ["qa"],
|
|
56
|
+
"hidden": true
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"id": "request-changes",
|
|
60
|
+
"label": "Request Changes",
|
|
61
|
+
"icon": "message-square",
|
|
62
|
+
"command": "gh pr comment \"$FORGE_PARAM_PR\" --body \"$(printf 'Cambios pedidos desde Forge:\\n\\n%s\\n' \"$FORGE_PARAM_MSG\")\"",
|
|
63
|
+
"params": {
|
|
64
|
+
"PR": { "pattern": "\\d+" },
|
|
65
|
+
"MSG": { "pattern": "[A-Za-z0-9À-ÿ ¿¡.,;:!?()'\"/#@_+%*=-]{1,500}" }
|
|
66
|
+
},
|
|
67
|
+
"tags": ["qa"],
|
|
68
|
+
"hidden": true
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
"detectors": [
|
|
72
|
+
{ "tool": "qa-pilot", "files": ["qa/qa-pilot.yaml"], "suggestion": "qa-pilot contract found. Review escalated pull requests from QA." }
|
|
73
|
+
]
|
|
74
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forge-cw",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "A visual dashboard for your coding agents on Claude Code, Codex, Pi or OpenCode. Installs CW for you.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"files": [
|
|
17
17
|
"dist",
|
|
18
18
|
"console",
|
|
19
|
+
"modules",
|
|
19
20
|
"cw"
|
|
20
21
|
],
|
|
21
22
|
"engines": {
|
|
@@ -39,13 +40,13 @@
|
|
|
39
40
|
"@types/node": "^22.0.0",
|
|
40
41
|
"esbuild": "^0.25.0",
|
|
41
42
|
"typescript": "^5.8.0",
|
|
42
|
-
"@forge-dev/
|
|
43
|
-
"@forge-dev/
|
|
43
|
+
"@forge-dev/core": "0.1.0",
|
|
44
|
+
"@forge-dev/console": "0.1.0"
|
|
44
45
|
},
|
|
45
46
|
"scripts": {
|
|
46
47
|
"build": "tsc",
|
|
47
48
|
"dev": "tsc --watch",
|
|
48
|
-
"clean": "rm -rf dist console cw",
|
|
49
|
+
"clean": "rm -rf dist console modules cw",
|
|
49
50
|
"bundle-cw": "node scripts/bundle-cw.mjs"
|
|
50
51
|
}
|
|
51
52
|
}
|