forge-cw 0.1.4 → 0.1.6

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.
@@ -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-ciIvVZWo.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-D_58OQhA.css">
7
+ <script type="module" crossorigin src="/assets/index-DEebs8fg.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
- for (const entry of entries) {
211
- if (!entry.isDirectory())
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
- const manifestPath = join(this.modulesDir, dirName, "forge-module.json");
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,20 +3987,32 @@ 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 project = projectId ? db.getProject(projectId) : void 0;
3940
- const cwd = project?.path ?? process.cwd();
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({
@@ -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();
@@ -4533,6 +4605,7 @@ 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 bundledModulesDir = isCheckout ? join15(packageRoot, "../../modules") : join15(packageRoot, "modules");
4536
4609
  async function main() {
4537
4610
  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
4611
  quietSqliteWarning2();
@@ -4562,7 +4635,8 @@ async function main() {
4562
4635
  port,
4563
4636
  db,
4564
4637
  authToken: isTeam ? authToken : void 0,
4565
- localOnly
4638
+ localOnly,
4639
+ bundledModulesDir
4566
4640
  });
4567
4641
  const { serveStatic } = await import("@hono/node-server/serve-static");
4568
4642
  const { readFileSync: readFileSync10 } = await import("node:fs");
@@ -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.4",
3
+ "version": "0.1.6",
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": {
@@ -45,7 +46,7 @@
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
  }