@capagents/forgeqe 1.3.1 → 2.0.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/HOW_TO_USE.md +373 -9
- package/README.md +55 -4
- package/docs/ARCHITECTURE.md +62 -1
- package/docs/CONTAINERS.md +227 -0
- package/docs/COST_ARCHITECTURE.md +32 -0
- package/docs/DESKTOP-CI.md +87 -0
- package/docs/DESKTOP.md +387 -0
- package/docs/MOBILE.md +228 -0
- package/docs/feature-test-prompts.md +25 -0
- package/docs/security-data-flow.html +440 -3
- package/docs/user-guide.html +797 -415
- package/env.example.yaml +67 -0
- package/forgeqe.example.yaml +56 -1
- package/llms.example.json +7 -0
- package/package.json +4 -1
- package/prompts/brain.md +31 -0
- package/prompts/find-tests.md +1 -0
- package/py/forgeqe_desktop/__init__.py +1 -0
- package/py/forgeqe_desktop/__main__.py +3 -0
- package/py/forgeqe_desktop/apps.py +80 -0
- package/py/forgeqe_desktop/checks.py +168 -0
- package/py/forgeqe_desktop/cli.py +90 -0
- package/py/forgeqe_desktop/handhooks.py +293 -0
- package/py/forgeqe_desktop/handrecord.py +239 -0
- package/py/forgeqe_desktop/heal.py +107 -0
- package/py/forgeqe_desktop/models.py +208 -0
- package/py/forgeqe_desktop/pywinauto_driver.py +217 -0
- package/py/forgeqe_desktop/record.py +150 -0
- package/py/forgeqe_desktop/replay.py +345 -0
- package/py/forgeqe_desktop/service.py +356 -0
- package/py/forgeqe_desktop/session.py +60 -0
- package/py/forgeqe_desktop/ufo_bridge.py +86 -0
- package/py/forgeqe_desktop/ufo_runner.py +305 -0
- package/py/forgeqe_desktop/visual.py +112 -0
- package/py/forgeqe_desktop/workflow.py +338 -0
- package/py/forgeqe_engine/cli.py +10 -0
- package/py/forgeqe_engine/llm.py +54 -4
- package/py/forgeqe_engine/model_params.py +169 -0
- package/py/forgeqe_engine/summary.py +6 -0
- package/py/forgeqe_mobile/__init__.py +1 -0
- package/py/forgeqe_mobile/__main__.py +3 -0
- package/py/forgeqe_mobile/appium_driver.py +342 -0
- package/py/forgeqe_mobile/apps.py +84 -0
- package/py/forgeqe_mobile/cli.py +70 -0
- package/py/forgeqe_mobile/explore.py +203 -0
- package/py/forgeqe_mobile/hyperexecute.py +261 -0
- package/py/forgeqe_mobile/maestro.py +321 -0
- package/py/forgeqe_mobile/service.py +332 -0
- package/skills/testcontainers/SKILL.md +42 -0
- package/src/agents.ts +11 -2
- package/src/api/plan.ts +2 -0
- package/src/api/write.ts +2 -0
- package/src/auth/copilot.ts +130 -0
- package/src/auth/signin.ts +54 -1
- package/src/brain/index.ts +26 -4
- package/src/capabilities/guide.ts +2 -0
- package/src/capabilities/registry.ts +110 -2
- package/src/capabilities/status.ts +15 -0
- package/src/cases/defects.ts +2 -1
- package/src/cases/rca.ts +2 -0
- package/src/childEnv.ts +4 -1
- package/src/cli/args.ts +3 -2
- package/src/cli/help.ts +24 -3
- package/src/cli/index.ts +47 -3
- package/src/code/find.ts +26 -0
- package/src/code/loop.ts +4 -1
- package/src/code/prompt.ts +2 -1
- package/src/code/prreview.ts +2 -0
- package/src/code/review.ts +6 -2
- package/src/code/runtime.ts +34 -3
- package/src/commands.ts +6 -1
- package/src/config.ts +159 -3
- package/src/containers/command.ts +68 -0
- package/src/containers/compose.ts +173 -0
- package/src/containers/config.ts +283 -0
- package/src/containers/mock.ts +156 -0
- package/src/containers/run.ts +362 -0
- package/src/containers/tests.ts +80 -0
- package/src/context.ts +33 -6
- package/src/copilot.ts +155 -0
- package/src/desktop/bridge.ts +229 -0
- package/src/desktop/command.ts +350 -0
- package/src/desktop/edit.ts +217 -0
- package/src/desktop/junit.ts +35 -0
- package/src/desktop/report.ts +155 -0
- package/src/desktop/run.ts +252 -0
- package/src/desktop/setup.ts +134 -0
- package/src/engine/bridge.ts +12 -5
- package/src/engine/setup.ts +6 -3
- package/src/environments.ts +566 -0
- package/src/evals/domain.ts +1 -0
- package/src/explore/charters.ts +2 -0
- package/src/guardrails.ts +145 -0
- package/src/heal/loop.ts +4 -1
- package/src/heal/prompt.ts +2 -1
- package/src/init.ts +5 -2
- package/src/llm.ts +23 -2
- package/src/mcp/attachtools.ts +16 -4
- package/src/mcp/captools.ts +27 -0
- package/src/mcp/containertools.ts +212 -0
- package/src/mcp/desktoptools.ts +334 -0
- package/src/mcp/mobiletools.ts +132 -0
- package/src/mcp/reporttools.ts +17 -2
- package/src/mcp/repotests.ts +22 -3
- package/src/mcp/routing.ts +11 -5
- package/src/mcp/scope.ts +210 -0
- package/src/mcp/server.ts +80 -5
- package/src/mcp/tools.ts +38 -1
- package/src/mobile/command.ts +245 -0
- package/src/mobile/run.ts +226 -0
- package/src/pack/pipeline.ts +44 -2
- package/src/refine.ts +3 -0
- package/src/report/html.ts +3 -0
- package/src/report/template.html +48 -48
- package/src/run.ts +7 -3
- package/src/tokenomics/ledger.ts +3 -1
- package/src/tui/App.tsx +23 -0
- package/src/update.ts +118 -8
- package/src/usage.ts +1 -1
package/src/run.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { createRequire } from "node:module"
|
|
|
4
4
|
import path from "node:path"
|
|
5
5
|
|
|
6
6
|
import { assertGoalUrlsAllowed, hostAllowed, normalizeHttpUrl } from "./guard.ts"
|
|
7
|
+
import { ghAuthToken } from "./cases/github.ts"
|
|
7
8
|
import { missingKey } from "./llm.ts"
|
|
8
9
|
import type { AgentEvent, ReplayMode, RunResult } from "./types.ts"
|
|
9
10
|
import { browserConfigFor, type AppConfig, type BrowserProvider } from "./config.ts"
|
|
@@ -21,7 +22,9 @@ import { historyFile, recordQuality } from "./quality/history.ts"
|
|
|
21
22
|
import { compareVisual, engineVisual } from "./quality/visual.ts"
|
|
22
23
|
import { enginePrompts, prompt } from "./prompts.ts"
|
|
23
24
|
import { knowledgeBlock, readKnowledge, relevantKnowledge } from "./knowledge.ts"
|
|
25
|
+
import { environmentBrief } from "./environments.ts"
|
|
24
26
|
import { registryMode } from "./registry/index.ts"
|
|
27
|
+
import { guardrailsNotes } from "./guardrails.ts"
|
|
25
28
|
|
|
26
29
|
export type ExecuteOptions = {
|
|
27
30
|
config: AppConfig
|
|
@@ -211,7 +214,7 @@ export async function executeRun(opts: ExecuteOptions): Promise<{ result: RunRes
|
|
|
211
214
|
return withReport(out)
|
|
212
215
|
}
|
|
213
216
|
|
|
214
|
-
const missing = missingKey(opts.config.llm)
|
|
217
|
+
const missing = missingKey(opts.config.llm, "engine", ghAuthToken)
|
|
215
218
|
if (missing) throw new Error(missing)
|
|
216
219
|
// A remote grid's keys are checked here, before the engine starts, rather than failing inside it.
|
|
217
220
|
if (browser.remote) {
|
|
@@ -220,9 +223,10 @@ export async function executeRun(opts: ExecuteOptions): Promise<{ result: RunRes
|
|
|
220
223
|
}
|
|
221
224
|
const log = (text: string) => opts.onEvent?.({ type: "log", level: "info", text })
|
|
222
225
|
const python = await ensureEngine(log)
|
|
223
|
-
// The browser agent's added instructions: forgeqe.yaml's, prompts/browser-extra.md,
|
|
226
|
+
// The browser agent's added instructions: forgeqe.yaml's, prompts/browser-extra.md, the product knowledge that matches the goal, and the guardrails.
|
|
224
227
|
const knowledge = opts.config.knowledge && opts.config.knowledge.browserMaxChars > 0 ? knowledgeBlock(relevantKnowledge(readKnowledge(opts.config.knowledge.dir), goal, opts.config.knowledge.browserMaxChars, 2)) : ""
|
|
225
|
-
|
|
228
|
+
// The team's guardrails come last, so nothing added after them reads as overriding them.
|
|
229
|
+
const extend = [opts.config.prompts.stepInstruction, prompt("browser-extra"), knowledge, environmentBrief(opts.config, "browser"), guardrailsNotes(opts.config) || ""]
|
|
226
230
|
.map((s) => s.trim())
|
|
227
231
|
.filter(Boolean)
|
|
228
232
|
.join("\n\n")
|
package/src/tokenomics/ledger.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { costOf, recordUsage, type Price, type Role } from "../usage.ts"
|
|
|
18
18
|
export type Trigger = "tui" | "cli" | "mcp" | "ci" | "monitor" | "schedule"
|
|
19
19
|
|
|
20
20
|
/** What a call was for, from the tools it asked for: no tools is the model thinking or answering. */
|
|
21
|
-
export type Purpose = "reasoning" | "planning" | "search" | "file read" | "edit" | "shell" | "browser" | "integration" | "delegation" | "unattributed"
|
|
21
|
+
export type Purpose = "reasoning" | "planning" | "search" | "file read" | "edit" | "shell" | "browser" | "desktop" | "mobile" | "integration" | "delegation" | "unattributed"
|
|
22
22
|
|
|
23
23
|
/** What ForgeQE was doing when the call was made. Set with withUsageScope; inner scopes add to outer ones. */
|
|
24
24
|
export type UsageScope = {
|
|
@@ -130,6 +130,8 @@ export function purposeOf(tools: string[] | undefined): Purpose {
|
|
|
130
130
|
const DEFAULT_JOB: Record<Role, string> = {
|
|
131
131
|
"ForgeQE's agent": "chat",
|
|
132
132
|
"browser agent": "browser test",
|
|
133
|
+
"desktop agent": "desktop test",
|
|
134
|
+
"mobile agent": "mobile test",
|
|
133
135
|
Scribe: "write tests",
|
|
134
136
|
reviewer: "review",
|
|
135
137
|
judge: "eval",
|
package/src/tui/App.tsx
CHANGED
|
@@ -15,6 +15,7 @@ import { FAILURE_ACTION, FAILURE_LABEL } from "../cases/failure.ts"
|
|
|
15
15
|
import { authStatus, openInBrowser, signIn, signOut, type AuthIO } from "../auth/signin.ts"
|
|
16
16
|
import { HELP, parseCommand } from "../commands.ts"
|
|
17
17
|
import { loadConfig, selectProfile, type AppConfig, type BrowserProvider } from "../config.ts"
|
|
18
|
+
import { appsText, exportEnvironment, withEnvironment } from "../environments.ts"
|
|
18
19
|
import { TOOLS, type AskQuestion, type ToolEvent } from "../mcp/tools.ts"
|
|
19
20
|
import type { executeRun } from "../run.ts"
|
|
20
21
|
import { bundleLogs, logEvent } from "../sessionLog.ts"
|
|
@@ -1443,6 +1444,28 @@ export function App({ options }: { options: TuiOptions }) {
|
|
|
1443
1444
|
}
|
|
1444
1445
|
return
|
|
1445
1446
|
}
|
|
1447
|
+
if (cmd.type === "env") {
|
|
1448
|
+
try {
|
|
1449
|
+
const cfg = configRef.current
|
|
1450
|
+
if (!cfg) throw new Error("Config is not loaded.")
|
|
1451
|
+
if (!cmd.name) {
|
|
1452
|
+
push("sys", appsText(cfg))
|
|
1453
|
+
return
|
|
1454
|
+
}
|
|
1455
|
+
configRef.current = withEnvironment(cfg, cmd.name)
|
|
1456
|
+
exportEnvironment(configRef.current)
|
|
1457
|
+
// Reloads in this session (/init, a profile switch) keep the environment.
|
|
1458
|
+
process.env.FORGEQE_ENV = cmd.name
|
|
1459
|
+
push("sys", `environment ${cmd.name}`)
|
|
1460
|
+
if (brainRef.current) {
|
|
1461
|
+
closeBrain()
|
|
1462
|
+
push("sys", "The agent switches to this environment on your next message and keeps the conversation.")
|
|
1463
|
+
}
|
|
1464
|
+
} catch (err) {
|
|
1465
|
+
push("err", (err as Error).message)
|
|
1466
|
+
}
|
|
1467
|
+
return
|
|
1468
|
+
}
|
|
1446
1469
|
if (cmd.type === "init") {
|
|
1447
1470
|
// forgeqe init in this folder, then the session reads what it wrote: the start screen updates at once.
|
|
1448
1471
|
try {
|
package/src/update.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process"
|
|
2
|
-
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs"
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process"
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs"
|
|
3
|
+
import { tmpdir } from "node:os"
|
|
3
4
|
import path from "node:path"
|
|
4
5
|
import { packageVersion } from "./tui/theme.ts"
|
|
5
6
|
|
|
@@ -28,19 +29,50 @@ export interface UpdateDeps {
|
|
|
28
29
|
platform: NodeJS.Platform
|
|
29
30
|
sleep: (ms: number) => Promise<void>
|
|
30
31
|
now: () => number
|
|
32
|
+
/**
|
|
33
|
+
* Windows: starts `script` (a .cmd) detached, to run once this process has exited, and returns its log file.
|
|
34
|
+
* A running ForgeQE keeps files of its own install open (its launcher, native libraries), and Windows does not
|
|
35
|
+
* let an installer replace a file that is in use, so the update cannot run from inside ForgeQE.
|
|
36
|
+
*/
|
|
37
|
+
defer?: (script: string) => string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* How to start `cmd` on `platform`. Windows runs .cmd and .bat files (npm, pyenv-win) only through a shell, which Node
|
|
42
|
+
* refuses to do on its own (EINVAL), so those go through cmd.exe with each argument quoted.
|
|
43
|
+
*/
|
|
44
|
+
export function spawnPlan(cmd: string, args: string[], platform: NodeJS.Platform): { cmd: string; args: string[]; shell: boolean } {
|
|
45
|
+
if (platform !== "win32" || /\.exe$/i.test(cmd)) return { cmd, args, shell: false }
|
|
46
|
+
return { cmd: winQuote(cmd), args: args.map(winQuote), shell: true }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** One argument for cmd.exe: quoted when it has a space or a character cmd.exe would read. */
|
|
50
|
+
export function winQuote(a: string): string {
|
|
51
|
+
return /[\s&|<>^()%!"]/.test(a) ? `"${a.replace(/"/g, '""')}"` : a
|
|
31
52
|
}
|
|
32
53
|
|
|
33
54
|
export function defaultDeps(): UpdateDeps {
|
|
34
55
|
return {
|
|
35
56
|
fetch,
|
|
36
57
|
run: (cmd, args) => {
|
|
37
|
-
const
|
|
58
|
+
const plan = spawnPlan(cmd, args, process.platform)
|
|
59
|
+
const r = spawnSync(plan.cmd, plan.args, { encoding: "utf8", shell: plan.shell, windowsHide: true })
|
|
38
60
|
return { status: r.error ? null : r.status, stdout: r.stdout || "", stderr: r.stderr || (r.error ? String(r.error.message) : "") }
|
|
39
61
|
},
|
|
40
62
|
env: process.env,
|
|
41
63
|
platform: process.platform,
|
|
42
64
|
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
43
65
|
now: () => Date.now(),
|
|
66
|
+
defer: (script) => {
|
|
67
|
+
const dir = path.join(tmpdir(), "forgeqe-update")
|
|
68
|
+
mkdirSync(dir, { recursive: true })
|
|
69
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-")
|
|
70
|
+
const file = path.join(dir, `update-${stamp}.cmd`)
|
|
71
|
+
const log = path.join(dir, `update-${stamp}.log`)
|
|
72
|
+
writeFileSync(file, script.replace(/\{LOG\}/g, log).replace(/\n/g, "\r\n"))
|
|
73
|
+
spawn("cmd.exe", ["/d", "/c", file], { detached: true, stdio: "ignore", windowsHide: true }).unref()
|
|
74
|
+
return log
|
|
75
|
+
},
|
|
44
76
|
}
|
|
45
77
|
}
|
|
46
78
|
|
|
@@ -142,12 +174,33 @@ function packageDirOf(file: string): string | undefined {
|
|
|
142
174
|
return undefined
|
|
143
175
|
}
|
|
144
176
|
|
|
177
|
+
/**
|
|
178
|
+
* The package a `forgeqe` on PATH belongs to when it is a copy or a wrapper, not a link: npm on Windows writes
|
|
179
|
+
* forgeqe.cmd next to node_modules; bun on Windows writes forgeqe.exe in ~/.bun/bin for ~/.bun/install/global; an npm
|
|
180
|
+
* prefix elsewhere keeps packages in <prefix>/lib/node_modules.
|
|
181
|
+
*/
|
|
182
|
+
function packageOfShim(binDir: string): string | undefined {
|
|
183
|
+
const parts = NPM_PACKAGE.split("/")
|
|
184
|
+
for (const dir of [path.join(binDir, "node_modules", ...parts), path.join(binDir, "..", "install", "global", "node_modules", ...parts), path.join(binDir, "..", "lib", "node_modules", ...parts)]) {
|
|
185
|
+
const found = packageDirOf(path.join(dir, "package.json"))
|
|
186
|
+
if (found) return path.resolve(found)
|
|
187
|
+
}
|
|
188
|
+
return undefined
|
|
189
|
+
}
|
|
190
|
+
|
|
145
191
|
function pythonOfScript(script: string, platform: NodeJS.Platform): string | undefined {
|
|
146
192
|
if (platform === "win32") {
|
|
147
193
|
const dir = path.dirname(script)
|
|
148
|
-
if (path.basename(dir).toLowerCase() !== "scripts") return undefined
|
|
149
194
|
const python = path.join(path.dirname(dir), "python.exe")
|
|
150
|
-
|
|
195
|
+
if (path.basename(dir).toLowerCase() === "scripts" && existsSync(python)) return python
|
|
196
|
+
// pip's forgeqe.exe carries the Python it was installed for (`#!C:\...\python.exe` near its end): the way to find
|
|
197
|
+
// it for `pip install --user`, whose Scripts folder is not next to python.exe.
|
|
198
|
+
try {
|
|
199
|
+
const text = readFileSync(script).toString("latin1")
|
|
200
|
+
const m = [...text.matchAll(/#!\s*"?([^"\r\n]*?pythonw?\.exe)"?/gi)].pop()
|
|
201
|
+
if (m && existsSync(m[1])) return m[1]
|
|
202
|
+
} catch {}
|
|
203
|
+
return undefined
|
|
151
204
|
}
|
|
152
205
|
let first = ""
|
|
153
206
|
try {
|
|
@@ -182,7 +235,10 @@ export function findInstalls(deps: UpdateDeps): Install[] {
|
|
|
182
235
|
}
|
|
183
236
|
if (seen.has(target)) continue
|
|
184
237
|
seen.add(target)
|
|
185
|
-
const pkgDir = packageDirOf(target)
|
|
238
|
+
const pkgDir = packageDirOf(target) || packageOfShim(dir)
|
|
239
|
+
// npm on Windows writes forgeqe, forgeqe.cmd and forgeqe.ps1 for one install.
|
|
240
|
+
if (pkgDir && seen.has(`pkg:${pkgDir}`)) continue
|
|
241
|
+
if (pkgDir) seen.add(`pkg:${pkgDir}`)
|
|
186
242
|
if (pkgDir) {
|
|
187
243
|
let version = ""
|
|
188
244
|
try {
|
|
@@ -233,6 +289,47 @@ export function commandLine([cmd, args]: [string, string[]]): string {
|
|
|
233
289
|
return [cmd, ...args].map((a) => (/\s/.test(a) ? JSON.stringify(a) : a)).join(" ")
|
|
234
290
|
}
|
|
235
291
|
|
|
292
|
+
/** What to do when an installer refuses for a reason the machine sets, not ForgeQE (Linux: root-owned npm, PEP 668). */
|
|
293
|
+
export function failureHint(kind: InstallKind, output: string, platform: NodeJS.Platform): string {
|
|
294
|
+
if (/EACCES|permission denied/i.test(output) && kind === "npm") {
|
|
295
|
+
return platform === "win32"
|
|
296
|
+
? "npm's global folder is not writable: run the terminal as administrator, or install with bun (bun add -g @capagents/forgeqe)."
|
|
297
|
+
: "npm's global folder belongs to root: run sudo npm install -g @capagents/forgeqe, or give npm a folder of your own (npm config set prefix ~/.npm-global)."
|
|
298
|
+
}
|
|
299
|
+
if (/externally-managed-environment/i.test(output)) {
|
|
300
|
+
return "This Python is managed by the system and pip will not change it: install with pipx install forgeqe-cli, or in a virtual environment."
|
|
301
|
+
}
|
|
302
|
+
if (/EBUSY|EPERM|being used by another process/i.test(output)) {
|
|
303
|
+
return "A file of the install is in use: close every ForgeQE window and run forgeqe update again."
|
|
304
|
+
}
|
|
305
|
+
return ""
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* The .cmd that updates Windows installs after the ForgeQE that started it has exited: it waits for these processes
|
|
310
|
+
* (ForgeQE and its launcher), runs each install's commands, and writes everything to {LOG}.
|
|
311
|
+
*/
|
|
312
|
+
export function windowsUpdateScript(steps: { kind: InstallKind; bin: string; commands: Array<[string, string[]]> }[], pids: number[], version: string): string {
|
|
313
|
+
const lines = ["@echo off", "setlocal", ":wait"]
|
|
314
|
+
for (const pid of pids.filter((p) => Number.isInteger(p) && p > 0)) {
|
|
315
|
+
// ping waits a second where timeout cannot (a detached script has no console); System32 paths, so Git's find
|
|
316
|
+
// or another tasklist on PATH is never the one that runs.
|
|
317
|
+
lines.push(`"%SystemRoot%\\System32\\tasklist.exe" /FI "PID eq ${pid}" 2>nul | "%SystemRoot%\\System32\\find.exe" " ${pid} " >nul && (ping -n 2 127.0.0.1 >nul & goto wait)`)
|
|
318
|
+
}
|
|
319
|
+
lines.push(`echo Updating ForgeQE to ${version} > "{LOG}"`)
|
|
320
|
+
for (const step of steps) {
|
|
321
|
+
lines.push(`echo. >> "{LOG}"`, `echo Updating the ${step.kind} install (${step.bin}) >> "{LOG}"`)
|
|
322
|
+
for (const [cmd, args] of step.commands) {
|
|
323
|
+
const line = [cmd, ...args].map(winQuote).join(" ")
|
|
324
|
+
const cacheStep = args[0] === "pm" && args[1] === "cache"
|
|
325
|
+
lines.push(`echo $ ${line.replace(/[&|<>^]/g, "^$&")} >> "{LOG}"`)
|
|
326
|
+
lines.push(`call ${line} >> "{LOG}" 2>&1${cacheStep ? "" : ` || echo FAILED: ${step.kind} install >> "{LOG}"`}`)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
lines.push(`echo. >> "{LOG}"`, `echo Done. Open a new terminal and run: forgeqe --version >> "{LOG}"`)
|
|
330
|
+
return `${lines.join("\n")}\n`
|
|
331
|
+
}
|
|
332
|
+
|
|
236
333
|
export interface UpdateOptions {
|
|
237
334
|
/** A version to install instead of the latest. */
|
|
238
335
|
version?: string
|
|
@@ -283,6 +380,16 @@ export async function update(opts: UpdateOptions = {}, deps: UpdateDeps = defaul
|
|
|
283
380
|
}
|
|
284
381
|
}
|
|
285
382
|
|
|
383
|
+
if (deps.platform === "win32" && deps.defer) {
|
|
384
|
+
// Windows will not replace files the running ForgeQE holds open, so the installs update once it has exited.
|
|
385
|
+
const steps = stale.map((inst) => ({ kind: inst.kind, bin: inst.bin, commands: updateCommands(inst, target, deps.env) }))
|
|
386
|
+
for (const step of steps) for (const c of step.commands) log(` $ ${commandLine(c)}`)
|
|
387
|
+
const logFile = deps.defer(windowsUpdateScript(steps, [process.pid, process.ppid], target))
|
|
388
|
+
log(`\nThe update to ${target} starts as soon as this command exits (Windows locks the files a running ForgeQE uses).`)
|
|
389
|
+
log(`It writes what it does to ${logFile}. Then open a new terminal and run forgeqe --version.`)
|
|
390
|
+
return 0
|
|
391
|
+
}
|
|
392
|
+
|
|
286
393
|
let failed = 0
|
|
287
394
|
for (const inst of stale) {
|
|
288
395
|
log(`\nUpdating the ${inst.kind} install (${inst.bin})`)
|
|
@@ -293,7 +400,10 @@ export async function update(opts: UpdateOptions = {}, deps: UpdateDeps = defaul
|
|
|
293
400
|
const cacheStep = c[1][0] === "pm" && c[1][1] === "cache"
|
|
294
401
|
if (cacheStep) continue
|
|
295
402
|
failed++
|
|
296
|
-
|
|
403
|
+
const output = (r.stderr || r.stdout).trim()
|
|
404
|
+
log(output.split("\n").slice(-5).map((l) => ` ${l}`).join("\n"))
|
|
405
|
+
const hint = failureHint(inst.kind, output, deps.platform)
|
|
406
|
+
if (hint) log(` ${hint}`)
|
|
297
407
|
break
|
|
298
408
|
}
|
|
299
409
|
}
|
|
@@ -306,6 +416,6 @@ export async function update(opts: UpdateOptions = {}, deps: UpdateDeps = defaul
|
|
|
306
416
|
log(`\n${behind.length || failed} install(s) are still behind ${target}; the output above says why.`)
|
|
307
417
|
return 1
|
|
308
418
|
}
|
|
309
|
-
log(`\nforgeqe ${target} is installed. Open a new terminal (or run hash -r) if the shell still finds the old one.`)
|
|
419
|
+
log(`\nforgeqe ${target} is installed. Open a new terminal${deps.platform === "win32" ? "" : " (or run hash -r)"} if the shell still finds the old one.`)
|
|
310
420
|
return 0
|
|
311
421
|
}
|
package/src/usage.ts
CHANGED
|
@@ -98,7 +98,7 @@ export function usageLine(usage: Usage, cost?: Cost | null): string {
|
|
|
98
98
|
// The meter: every model session reports here, by role
|
|
99
99
|
// ---------------------------------------------------------------------------------------------
|
|
100
100
|
|
|
101
|
-
export type Role = "ForgeQE's agent" | "browser agent" | "Scribe" | "reviewer" | "judge" | "refiner" | "root-cause analyst"
|
|
101
|
+
export type Role = "ForgeQE's agent" | "browser agent" | "desktop agent" | "mobile agent" | "Scribe" | "reviewer" | "judge" | "refiner" | "root-cause analyst"
|
|
102
102
|
|
|
103
103
|
type Entry = { usage: Usage; cost?: Cost; priced: boolean }
|
|
104
104
|
|