@aion0/forge 0.10.87 → 0.10.89
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/RELEASE_NOTES.md +7 -7
- package/app/api/auth/check/route.ts +1 -1
- package/app/api/auth/keys/route.ts +1 -1
- package/app/api/auth/verify/route.ts +4 -21
- package/app/api/mcp/route.ts +1 -1
- package/app/api/onboarding/route.ts +4 -2
- package/bin/forge-server.mjs +32 -1
- package/instrumentation.ts +2 -0
- package/lib/api-tokens.ts +31 -0
- package/lib/browser-bridge-standalone.ts +1 -0
- package/lib/chat/protocols/http.ts +16 -4
- package/lib/chat-standalone.ts +1 -0
- package/lib/connectors/test-runner.ts +8 -2
- package/lib/help-docs/10-troubleshooting.md +13 -0
- package/lib/memory-standalone.ts +2 -0
- package/lib/projects.ts +38 -15
- package/lib/proxy-setup.ts +21 -0
- package/lib/telegram-standalone.ts +1 -0
- package/lib/terminal-standalone.ts +1 -0
- package/lib/workspace-standalone.ts +1 -0
- package/next.config.ts +7 -0
- package/package.json +4 -1
- package/proxy.ts +1 -1
- package/publish.sh +19 -0
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
# Forge v0.10.
|
|
1
|
+
# Forge v0.10.89
|
|
2
2
|
|
|
3
3
|
Released: 2026-06-17
|
|
4
4
|
|
|
5
|
-
## Changes since v0.10.
|
|
5
|
+
## Changes since v0.10.88
|
|
6
6
|
|
|
7
7
|
### Other
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
- feat(
|
|
8
|
+
- fix(build): clean install on fresh machines — webpack build + valid route exports
|
|
9
|
+
- fix(projects): strict resolve requires git-backed match + fresh-clone visibility
|
|
10
|
+
- refactor(projects): auto-clone lands in project root, retire cloned-projects
|
|
11
|
+
- feat(projects): auto-clone lands in a real project root, not the cache
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.
|
|
14
|
+
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.88...v0.10.89
|
|
@@ -2,7 +2,7 @@ import { NextResponse } from 'next/server';
|
|
|
2
2
|
import { verifyAdmin, getAdminPassword } from '@/lib/password';
|
|
3
3
|
import { createApiKey, listApiKeys } from '@/lib/api-keys';
|
|
4
4
|
import { isAuthenticated } from '@/lib/api-auth';
|
|
5
|
-
import { isValidToken } from '@/
|
|
5
|
+
import { isValidToken } from '@/lib/api-tokens';
|
|
6
6
|
|
|
7
7
|
function isDev(): boolean {
|
|
8
8
|
return process.env.NODE_ENV !== 'production' || process.env.FORGE_DEV === '1';
|
|
@@ -1,23 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const tokenKey = Symbol.for('forge-api-tokens');
|
|
6
|
-
const g = globalThis as any;
|
|
7
|
-
if (!g[tokenKey]) g[tokenKey] = new Set<string>();
|
|
8
|
-
export const validTokens: Set<string> = g[tokenKey];
|
|
9
|
-
|
|
10
|
-
/** Verify a token is valid (for use in other API routes) */
|
|
11
|
-
export function isValidToken(req: Request): boolean {
|
|
12
|
-
// Check header
|
|
13
|
-
const headerToken = new Headers(req.headers).get('x-forge-token');
|
|
14
|
-
if (headerToken && validTokens.has(headerToken)) return true;
|
|
15
|
-
// Check cookie
|
|
16
|
-
const cookieHeader = new Headers(req.headers).get('cookie') || '';
|
|
17
|
-
const match = cookieHeader.match(/forge-api-token=([^;]+)/);
|
|
18
|
-
if (match && validTokens.has(match[1])) return true;
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
2
|
+
// Token store/helpers live in lib/api-tokens (a route file may only export
|
|
3
|
+
// HTTP handlers + Next's allowed config fields — Next 16 type-checks this).
|
|
4
|
+
import { mintToken } from '@/lib/api-tokens';
|
|
21
5
|
|
|
22
6
|
export async function POST(req: Request) {
|
|
23
7
|
const body = await req.json();
|
|
@@ -32,8 +16,7 @@ export async function POST(req: Request) {
|
|
|
32
16
|
return NextResponse.json({ error: 'invalid password' }, { status: 401 });
|
|
33
17
|
}
|
|
34
18
|
|
|
35
|
-
const token =
|
|
36
|
-
validTokens.add(token);
|
|
19
|
+
const token = mintToken();
|
|
37
20
|
|
|
38
21
|
const res = NextResponse.json({ ok: true, token });
|
|
39
22
|
res.cookies.set('forge-api-token', token, {
|
package/app/api/mcp/route.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* forwards JSON-RPC frames here and carries the Mcp-Session-Id header.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { isValidToken } from '@/
|
|
18
|
+
import { isValidToken } from '@/lib/api-tokens';
|
|
19
19
|
import { isAuthenticated } from '@/lib/api-auth';
|
|
20
20
|
import { createManagementMcpServer } from '@/mcp/server';
|
|
21
21
|
import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
|
|
@@ -544,7 +544,9 @@ function preprocessTemplate(
|
|
|
544
544
|
// / default_project_path) after a Reinstall without forcing the user
|
|
545
545
|
// to re-run the entire wizard. Existing non-empty values are preserved
|
|
546
546
|
// — see the merge logic inside.
|
|
547
|
-
export
|
|
547
|
+
// Module-local (not exported): a route file may only export HTTP handlers +
|
|
548
|
+
// Next's config fields — exporting helpers fails Next 16's route type-check.
|
|
549
|
+
async function applyConnectors(
|
|
548
550
|
values: Record<string, string> | undefined,
|
|
549
551
|
selectedConnectors: string[] | undefined,
|
|
550
552
|
sourceId?: string,
|
|
@@ -758,7 +760,7 @@ export async function applyConnectors(
|
|
|
758
760
|
* template omits `_pipelines`, falls back to `suggested_pipelines` (the
|
|
759
761
|
* marketplace's fortinet-* prefix heuristic).
|
|
760
762
|
*/
|
|
761
|
-
|
|
763
|
+
async function applyTemplatePipelines(sourceId?: string, deptId?: string): Promise<{
|
|
762
764
|
installed: string[];
|
|
763
765
|
errors: Array<{ name: string; error: string }>;
|
|
764
766
|
}> {
|
package/bin/forge-server.mjs
CHANGED
|
@@ -28,6 +28,32 @@ import { homedir } from 'node:os';
|
|
|
28
28
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
29
29
|
const ROOT = join(__dirname, '..');
|
|
30
30
|
|
|
31
|
+
// Node version guard — Forge is tested on Node 20–22 (LTS). Bleeding-edge
|
|
32
|
+
// majors crash `next build` with a cryptic Turbopack error ("Expected process
|
|
33
|
+
// result to be a module"); old majors lack required APIs. Warn early so the
|
|
34
|
+
// real cause is visible before the build runs.
|
|
35
|
+
{
|
|
36
|
+
const major = Number(process.versions.node.split('.')[0]);
|
|
37
|
+
if (major < 20 || major > 24) {
|
|
38
|
+
console.warn(
|
|
39
|
+
`\n⚠️ Forge is tested on Node 20–22 (LTS); you're on Node ${process.versions.node}.\n` +
|
|
40
|
+
` If startup fails with a Turbopack / native-module error, switch to Node 22:\n` +
|
|
41
|
+
` nvm install 22 && nvm use 22 && npm i -g @aion0/forge\n`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Pre-install EnvHttpProxyAgent before any dynamic import('undici') can
|
|
47
|
+
// clobber Node's NODE_USE_ENV_PROXY agent (see lib/proxy-setup.ts).
|
|
48
|
+
// No-op off-corp (no proxy env), so non-docker deployments are unchanged.
|
|
49
|
+
if (process.env.HTTPS_PROXY || process.env.HTTP_PROXY ||
|
|
50
|
+
process.env.https_proxy || process.env.http_proxy) {
|
|
51
|
+
try {
|
|
52
|
+
const { setGlobalDispatcher, EnvHttpProxyAgent } = await import('undici');
|
|
53
|
+
setGlobalDispatcher(new EnvHttpProxyAgent());
|
|
54
|
+
} catch { /* undici unavailable — leave Node's default dispatcher */ }
|
|
55
|
+
}
|
|
56
|
+
|
|
31
57
|
/** Build Next.js — install devDependencies first if missing */
|
|
32
58
|
function buildNext() {
|
|
33
59
|
// Check if devDependencies are installed (e.g. @tailwindcss/postcss)
|
|
@@ -40,7 +66,12 @@ function buildNext() {
|
|
|
40
66
|
// installed npm-package copy (.npmrc isn't published).
|
|
41
67
|
execSync('npm install --include=dev --legacy-peer-deps', { cwd: ROOT, stdio: 'inherit' });
|
|
42
68
|
}
|
|
43
|
-
|
|
69
|
+
// Build with webpack, not Turbopack (Next 16's default). Turbopack chokes on
|
|
70
|
+
// the Proxy/middleware bundle — proxy.ts pulls auth + a route module that
|
|
71
|
+
// transitively imports Node-native deps (better-sqlite3 …) — and dies with
|
|
72
|
+
// "TurbopackInternalError: Expected process result to be a module". Webpack
|
|
73
|
+
// (the pre-16 path) handles the Node-runtime middleware fine.
|
|
74
|
+
execSync('npx next build --webpack', { cwd: ROOT, stdio: 'inherit', env: { ...process.env } });
|
|
44
75
|
}
|
|
45
76
|
|
|
46
77
|
/**
|
package/instrumentation.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Next.js instrumentation — runs once when the server starts.
|
|
3
3
|
* Loads .env.local and prints login password.
|
|
4
4
|
*/
|
|
5
|
+
import './lib/proxy-setup'; // MUST be first — see proxy-setup.ts header.
|
|
6
|
+
|
|
5
7
|
export async function register() {
|
|
6
8
|
// Only run on server, not Edge
|
|
7
9
|
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* In-memory admin-minted API tokens, shared across API routes + the proxy in
|
|
5
|
+
* the same process. Lives in lib/ (not an API route) on purpose: Next 16's
|
|
6
|
+
* route type-check rejects non-handler exports from a route file, and importing
|
|
7
|
+
* a route module into proxy.ts dragged Node-native deps into the middleware
|
|
8
|
+
* bundle — which crashed the Turbopack build.
|
|
9
|
+
*/
|
|
10
|
+
const tokenKey = Symbol.for('forge-api-tokens');
|
|
11
|
+
const g = globalThis as any;
|
|
12
|
+
if (!g[tokenKey]) g[tokenKey] = new Set<string>();
|
|
13
|
+
export const validTokens: Set<string> = g[tokenKey];
|
|
14
|
+
|
|
15
|
+
/** Mint + register a new token (called by the password-verify route). */
|
|
16
|
+
export function mintToken(): string {
|
|
17
|
+
const token = randomUUID();
|
|
18
|
+
validTokens.add(token);
|
|
19
|
+
return token;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** True if the request carries a valid token (x-forge-token header or
|
|
23
|
+
* forge-api-token cookie). */
|
|
24
|
+
export function isValidToken(req: Request): boolean {
|
|
25
|
+
const headerToken = new Headers(req.headers).get('x-forge-token');
|
|
26
|
+
if (headerToken && validTokens.has(headerToken)) return true;
|
|
27
|
+
const cookieHeader = new Headers(req.headers).get('cookie') || '';
|
|
28
|
+
const match = cookieHeader.match(/forge-api-token=([^;]+)/);
|
|
29
|
+
if (match && validTokens.has(match[1])) return true;
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
* Usage: npx tsx lib/browser-bridge-standalone.ts [--forge-port=8403]
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
|
+
import './proxy-setup'; // MUST be first — see proxy-setup.ts header.
|
|
42
43
|
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
|
|
43
44
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
44
45
|
import { randomUUID, createHash } from 'node:crypto';
|
|
@@ -241,8 +241,14 @@ async function exchangeBearerToken(
|
|
|
241
241
|
const exchangeFetchInit = { method: auth.exchange_method || 'POST', headers, body };
|
|
242
242
|
let res: Response;
|
|
243
243
|
if (verifyTls === false) {
|
|
244
|
-
|
|
245
|
-
|
|
244
|
+
// Per-call dispatcher bypasses the global proxy agent — route via
|
|
245
|
+
// ProxyAgent when a proxy is set (corp), else plain Agent (direct).
|
|
246
|
+
const { fetch: undiciFetch, Agent, ProxyAgent } = await import('undici');
|
|
247
|
+
const proxyUrl = process.env.HTTPS_PROXY || process.env.https_proxy ||
|
|
248
|
+
process.env.HTTP_PROXY || process.env.http_proxy;
|
|
249
|
+
const dispatcher = proxyUrl
|
|
250
|
+
? new ProxyAgent({ uri: proxyUrl, requestTls: { rejectUnauthorized: false } })
|
|
251
|
+
: new Agent({ connect: { rejectUnauthorized: false } });
|
|
246
252
|
res = await undiciFetch(exchangeUrl, { ...exchangeFetchInit, dispatcher }) as unknown as Response;
|
|
247
253
|
} else {
|
|
248
254
|
res = await fetch(exchangeUrl, exchangeFetchInit);
|
|
@@ -488,8 +494,14 @@ export async function runHttp({ tool, settings, args, connectorAuth, noTruncatio
|
|
|
488
494
|
// fetch are version-matched; passing an external undici Agent
|
|
489
495
|
// into Node's bundled global fetch fails with UND_ERR_INVALID_ARG
|
|
490
496
|
// because Node 22 ships an older undici than the installed one.
|
|
491
|
-
|
|
492
|
-
|
|
497
|
+
// Per-call dispatcher bypasses the global proxy agent — route via
|
|
498
|
+
// ProxyAgent when a proxy is set (corp), else plain Agent (direct).
|
|
499
|
+
const { fetch: undiciFetch, Agent, ProxyAgent } = await import('undici');
|
|
500
|
+
const proxyUrl = process.env.HTTPS_PROXY || process.env.https_proxy ||
|
|
501
|
+
process.env.HTTP_PROXY || process.env.http_proxy;
|
|
502
|
+
const dispatcher = proxyUrl
|
|
503
|
+
? new ProxyAgent({ uri: proxyUrl, requestTls: { rejectUnauthorized: false } })
|
|
504
|
+
: new Agent({ connect: { rejectUnauthorized: false } });
|
|
493
505
|
res = await undiciFetch(url, { method, headers, body, signal: controller.signal, dispatcher }) as unknown as Response;
|
|
494
506
|
} else {
|
|
495
507
|
res = await fetch(url, { method, headers, body, signal: controller.signal });
|
package/lib/chat-standalone.ts
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
* keep their existing WS path; HTTP-only surfaces use SSE.
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
|
+
import './proxy-setup'; // MUST be first — see proxy-setup.ts header.
|
|
33
34
|
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
|
|
34
35
|
import {
|
|
35
36
|
createSession, getSession, listSessions, updateSession, deleteSession, listMessages,
|
|
@@ -134,8 +134,14 @@ async function runHttpProbe(
|
|
|
134
134
|
// NAC, ESXi …) need undici with rejectUnauthorized:false, same as http.ts.
|
|
135
135
|
const fetchInit = { method, headers, body, signal: ctrl.signal };
|
|
136
136
|
if (def.http?.verify_tls === false) {
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
// A per-call dispatcher bypasses the global proxy agent, so when a proxy
|
|
138
|
+
// is set (corp) route through ProxyAgent; else plain Agent (direct).
|
|
139
|
+
const { fetch: undiciFetch, Agent, ProxyAgent } = await import('undici');
|
|
140
|
+
const proxyUrl = process.env.HTTPS_PROXY || process.env.https_proxy ||
|
|
141
|
+
process.env.HTTP_PROXY || process.env.http_proxy;
|
|
142
|
+
const dispatcher = proxyUrl
|
|
143
|
+
? new ProxyAgent({ uri: proxyUrl, requestTls: { rejectUnauthorized: false } })
|
|
144
|
+
: new Agent({ connect: { rejectUnauthorized: false } });
|
|
139
145
|
res = await undiciFetch(url, { ...fetchInit, dispatcher } as any) as unknown as Response;
|
|
140
146
|
} else {
|
|
141
147
|
res = await fetch(url, fetchInit);
|
|
@@ -76,6 +76,19 @@ The WebSocket dropped (system suspend, network blip). Forge auto-reconnects afte
|
|
|
76
76
|
gh auth login
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
+
### Connector Test fails with `ENETUNREACH` behind a corp proxy
|
|
80
|
+
Symptom: a brand-new deployment behind a corporate proxy/ZTNA, configure a
|
|
81
|
+
connector (e.g. GitLab) → **Test** → `request failed: fetch failed: connect
|
|
82
|
+
ENETUNREACH <backend-ip>:443`.
|
|
83
|
+
|
|
84
|
+
Forge honours `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` automatically — every
|
|
85
|
+
entry point pre-installs an `EnvHttpProxyAgent` before any code makes a request,
|
|
86
|
+
so a proxy set in the environment is used for all connector calls (including
|
|
87
|
+
`verify_tls: false` self-signed appliances like FortiNAC). Just make sure the
|
|
88
|
+
proxy env vars are exported for the Forge process (in Docker, via the
|
|
89
|
+
container's environment). Off-corp deployments with no proxy env are unaffected
|
|
90
|
+
— requests go direct as before.
|
|
91
|
+
|
|
79
92
|
### Skills not syncing
|
|
80
93
|
Click "Sync" in Skills tab. Check `skillsRepoUrl` in Settings points to valid registry.
|
|
81
94
|
|
package/lib/memory-standalone.ts
CHANGED
package/lib/projects.ts
CHANGED
|
@@ -297,11 +297,12 @@ export interface StrictResolveResult {
|
|
|
297
297
|
* - else gitlab connector default_project_path
|
|
298
298
|
* - else → error('no_repo_specified')
|
|
299
299
|
* 3. Re-scan locally for a checkout whose origin matches the target → use it
|
|
300
|
-
* 4. Clone via tryGitlabClone —
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
300
|
+
* 4. Clone via tryGitlabClone — lands in a real project root so the
|
|
301
|
+
* checkout persists and is reused next run. In container mode we register
|
|
302
|
+
* the docker-internal /data/project (a host-mounted path) as the root
|
|
303
|
+
* first, so the clone shows up as a normal project; on a host install it
|
|
304
|
+
* takes the first configured project path (projectRoots[0]), falling back
|
|
305
|
+
* to ~/IdeaProjects / <dataDir>/scratch.
|
|
305
306
|
* - gitlab connector missing → error('no_repo_specified')
|
|
306
307
|
* - clone failed → error('clone_failed')
|
|
307
308
|
*/
|
|
@@ -309,25 +310,30 @@ export function resolveProjectStrict(name: string | undefined): StrictResolveRes
|
|
|
309
310
|
const trimmed = (name || '').trim();
|
|
310
311
|
|
|
311
312
|
// Step 1: existing match — reuse the lenient resolver's match logic by
|
|
312
|
-
// delegating, but discard its scratch fallback.
|
|
313
|
+
// delegating, but discard its scratch fallback. Crucially, only accept a
|
|
314
|
+
// GIT-BACKED match: this resolver exists for requires_real_project pipelines,
|
|
315
|
+
// so a non-git project (e.g. the built-in 'default' / 'scratch', or a plain
|
|
316
|
+
// folder the chat agent named) must NOT satisfy it — fall through and clone
|
|
317
|
+
// the real repo instead. Without this, `project: default` was returned as-is
|
|
318
|
+
// and the node's `git rev-parse` then failed with "not a git repository".
|
|
313
319
|
if (trimmed) {
|
|
314
320
|
const hit = getProjectInfo(trimmed);
|
|
315
|
-
if (hit) return { project: hit, source: 'existing' };
|
|
321
|
+
if (hit?.hasGit) return { project: hit, source: 'existing' };
|
|
316
322
|
|
|
317
323
|
const projects = scanProjects();
|
|
318
324
|
if (trimmed.includes('/')) {
|
|
319
325
|
const want = normalizeRepoPath(trimmed) || trimmed.toLowerCase().replace(/^\/+|\/+$/g, '');
|
|
320
326
|
const wantBase = want.split('/').pop()!;
|
|
321
327
|
const byRepo = projects.filter((p) => p.repo && p.repo === want);
|
|
322
|
-
if (byRepo.length === 1) return { project: byRepo[0], source: 'existing' };
|
|
328
|
+
if (byRepo.length === 1 && byRepo[0].hasGit) return { project: byRepo[0], source: 'existing' };
|
|
323
329
|
const byRepoBase = projects.filter((p) => p.repo && p.repo.split('/').pop() === wantBase);
|
|
324
|
-
if (byRepoBase.length === 1) return { project: byRepoBase[0], source: 'existing' };
|
|
330
|
+
if (byRepoBase.length === 1 && byRepoBase[0].hasGit) return { project: byRepoBase[0], source: 'existing' };
|
|
325
331
|
}
|
|
326
332
|
const base = trimmed.split('/').pop()!.trim();
|
|
327
333
|
const ownerRepo = trimmed.replace(/\//g, '-');
|
|
328
334
|
const cands = projects.filter((p) =>
|
|
329
335
|
p.name === base || p.name === ownerRepo || p.name.toLowerCase() === base.toLowerCase());
|
|
330
|
-
if (cands.length === 1) return { project: cands[0], source: 'existing' };
|
|
336
|
+
if (cands.length === 1 && cands[0].hasGit) return { project: cands[0], source: 'existing' };
|
|
331
337
|
}
|
|
332
338
|
|
|
333
339
|
// Step 2: derive target repo path
|
|
@@ -450,9 +456,17 @@ function tryGitlabClone(repoPath: string): LocalProject | null {
|
|
|
450
456
|
const path = repoPath.replace(/^\/+|\/+$/g, '');
|
|
451
457
|
if (!path || !path.includes('/')) return null; // not a repo path shape
|
|
452
458
|
const safe = path.replace(/[^A-Za-z0-9._-]+/g, '-');
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
459
|
+
// Clone into a real project root so the checkout persists as a normal
|
|
460
|
+
// project and is reused across runs (no delete needed). In container mode
|
|
461
|
+
// ensureContainerRoot registers the docker-internal /data/project (a
|
|
462
|
+
// host-mounted path) as the first root; on a host install we just take the
|
|
463
|
+
// first configured project path. getDefaultCloneRoot encodes that priority
|
|
464
|
+
// (projectRoots[0] → ~/IdeaProjects → <dataDir>/scratch). The old
|
|
465
|
+
// <dataDir>/cloned-projects hidden cache is retired.
|
|
466
|
+
ensureContainerRoot();
|
|
467
|
+
const cloneRoot = getDefaultCloneRoot();
|
|
468
|
+
const target = join(cloneRoot, safe);
|
|
469
|
+
try { mkdirSync(cloneRoot, { recursive: true }); } catch {}
|
|
456
470
|
|
|
457
471
|
// Inject PAT into URL. gitlab accepts `oauth2:<token>` basic auth on
|
|
458
472
|
// HTTPS. URL-encode the token in case it contains '@' or '/'.
|
|
@@ -467,6 +481,11 @@ function tryGitlabClone(repoPath: string): LocalProject | null {
|
|
|
467
481
|
if (!alreadyCloned) {
|
|
468
482
|
execSync(`git clone --quiet "${authedUrl}" "${target}"`, { stdio: 'pipe', timeout: 120000 });
|
|
469
483
|
_cloneFreshAt.set(target, Date.now());
|
|
484
|
+
// The new checkout must be discoverable right away: a scanProjects() cache
|
|
485
|
+
// populated earlier this run (e.g. by resolveProjectStrict's pre-clone
|
|
486
|
+
// scan) wouldn't include it, so getProjectInfo would miss the repo we
|
|
487
|
+
// just cloned and downstream resolution could fall back to scratch.
|
|
488
|
+
invalidateProjectScan();
|
|
470
489
|
} else if (!stillFresh) {
|
|
471
490
|
// Best-effort refresh; failures (offline, network, auth drift) are
|
|
472
491
|
// non-fatal — fall back to whatever's cached. Pipelines see stale
|
|
@@ -487,9 +506,13 @@ function tryGitlabClone(repoPath: string): LocalProject | null {
|
|
|
487
506
|
try { mtime = statSync(target).mtime.toISOString(); }
|
|
488
507
|
catch { mtime = new Date().toISOString(); }
|
|
489
508
|
return {
|
|
490
|
-
name
|
|
509
|
+
// Use the on-disk dir name (== scanProjects' entry.name) so the name we
|
|
510
|
+
// seed back into input.project matches what getProjectInfo will find next
|
|
511
|
+
// scan. Returning the slashed repo path here desynced the two and made
|
|
512
|
+
// computePipelineTmpDir's getProjectInfo miss → tmp_dir fell to scratch.
|
|
513
|
+
name: safe,
|
|
491
514
|
path: target,
|
|
492
|
-
root:
|
|
515
|
+
root: cloneRoot,
|
|
493
516
|
hasGit: existsSync(join(target, '.git')),
|
|
494
517
|
hasClaudeMd: existsSync(join(target, 'CLAUDE.md')),
|
|
495
518
|
language: null,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-install an EnvHttpProxyAgent on undici's global dispatcher BEFORE
|
|
3
|
+
* anything dynamic-imports the npm undici (8.x).
|
|
4
|
+
*
|
|
5
|
+
* Why: Node honours NODE_USE_ENV_PROXY by putting EnvHttpProxyAgent on
|
|
6
|
+
* undici v1's dispatcher symbol. The npm undici uses v2; on load its
|
|
7
|
+
* `if (getGlobalDispatcher() === undefined) setGlobalDispatcher(new Agent())`
|
|
8
|
+
* reads v2 (undefined) and writes a plain Agent into BOTH v1 and v2,
|
|
9
|
+
* silently clobbering Node's proxy agent — after which every fetch ignores
|
|
10
|
+
* HTTPS_PROXY and direct-connects (ENETUNREACH behind corp ZTNA).
|
|
11
|
+
* Setting v2 here makes that check non-undefined, so npm undici skips it.
|
|
12
|
+
*
|
|
13
|
+
* MUST be the FIRST import in every entry point. Off-corp (no proxy env)
|
|
14
|
+
* this is a no-op, so non-docker / standalone deployments are unchanged.
|
|
15
|
+
*/
|
|
16
|
+
import { setGlobalDispatcher, EnvHttpProxyAgent } from 'undici';
|
|
17
|
+
|
|
18
|
+
if (process.env.HTTPS_PROXY || process.env.HTTP_PROXY ||
|
|
19
|
+
process.env.https_proxy || process.env.http_proxy) {
|
|
20
|
+
setGlobalDispatcher(new EnvHttpProxyAgent());
|
|
21
|
+
}
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* Usage: npx tsx lib/terminal-standalone.ts
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
+
import './proxy-setup'; // MUST be first — see proxy-setup.ts header.
|
|
27
28
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
28
29
|
import * as pty from 'node-pty';
|
|
29
30
|
import { execSync } from 'node:child_process';
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* FORGE_DATA_DIR — data directory
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import './proxy-setup'; // MUST be first — see proxy-setup.ts header.
|
|
15
16
|
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
|
|
16
17
|
import { readdirSync, statSync } from 'node:fs';
|
|
17
18
|
import { join, resolve } from 'node:path';
|
package/next.config.ts
CHANGED
|
@@ -10,6 +10,13 @@ const localIPs = Object.values(networkInterfaces())
|
|
|
10
10
|
.map(i => i!.address);
|
|
11
11
|
|
|
12
12
|
const nextConfig: NextConfig = {
|
|
13
|
+
// Skip `next build`'s TypeScript step. It duplicates our authoritative
|
|
14
|
+
// `tsc --noEmit` (run in CI / publish.sh) and additionally enforces a
|
|
15
|
+
// route-export shape rule that rejects Forge's intentional patterns —
|
|
16
|
+
// craft-system runtime routes that serve React/jsx as importable modules
|
|
17
|
+
// (app/api/craft-system/runtime/*) export non-handler symbols on purpose.
|
|
18
|
+
// tsc remains the real type gate; this only drops Next's redundant check.
|
|
19
|
+
typescript: { ignoreBuildErrors: true },
|
|
13
20
|
serverExternalPackages: ['better-sqlite3', 'esbuild'],
|
|
14
21
|
allowedDevOrigins: localIPs,
|
|
15
22
|
async rewrites() {
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aion0/forge",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.89",
|
|
4
4
|
"description": "Unified AI workflow platform — multi-model task orchestration, persistent sessions, web terminal, remote access",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20"
|
|
8
|
+
},
|
|
6
9
|
"scripts": {
|
|
7
10
|
"dev": "next dev --turbopack",
|
|
8
11
|
"build": "next build",
|
package/proxy.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
2
|
import { auth } from './lib/auth';
|
|
3
3
|
import { hasValidApiKey } from './lib/api-auth';
|
|
4
|
-
import { isValidToken } from './
|
|
4
|
+
import { isValidToken } from './lib/api-tokens';
|
|
5
5
|
|
|
6
6
|
// Node runtime: the auth() wrapper decodes the NextAuth session JWT with the
|
|
7
7
|
// AUTH_SECRET that the route layer also uses (auto-generated in-process when
|
package/publish.sh
CHANGED
|
@@ -46,6 +46,25 @@ fi
|
|
|
46
46
|
echo "Version: $CURRENT → $NEW_VERSION"
|
|
47
47
|
echo ""
|
|
48
48
|
|
|
49
|
+
# ── Build gate: a broken build must never ship ──────────────────────────────
|
|
50
|
+
# publish.sh used to bump+tag+push with ZERO build verification, so compile /
|
|
51
|
+
# bundler errors only surfaced when a fresh user ran `forge server start` (their
|
|
52
|
+
# first clean `next build`). Run it here with a CLEAN .next so a stale cache
|
|
53
|
+
# can't mask source errors. tsc is the authoritative type gate (next build's own
|
|
54
|
+
# TS step is off via typescript.ignoreBuildErrors); __tests__ are tsx scripts
|
|
55
|
+
# checked separately, so they're excluded.
|
|
56
|
+
echo "Verifying clean build before publish ..."
|
|
57
|
+
rm -rf .next
|
|
58
|
+
npx next build --webpack
|
|
59
|
+
TSC_ERRORS=$(npx tsc --noEmit 2>&1 | grep "error TS" | grep -v "__tests__" || true)
|
|
60
|
+
if [ -n "$TSC_ERRORS" ]; then
|
|
61
|
+
echo "$TSC_ERRORS"
|
|
62
|
+
echo "✗ Type errors — aborting publish (no version bump/commit/tag made)."
|
|
63
|
+
exit 1
|
|
64
|
+
fi
|
|
65
|
+
echo "✓ Build + type-check clean"
|
|
66
|
+
echo ""
|
|
67
|
+
|
|
49
68
|
# Refresh the bundled model-registry snapshot from public-info.
|
|
50
69
|
# This is the offline/first-run fallback for lib/agents/known-models.ts —
|
|
51
70
|
# pulling it here keeps it from drifting against the live registry.
|