@lazyingart/agintiflow 0.20.122 → 0.20.124
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 +7 -3
- package/package.json +10 -2
- package/references/agintiflow-aaps-slogan-subtext-v3.md +1 -1
- package/scripts/postinstall-webapp.js +23 -0
- package/scripts/smoke-cli-chat.js +19 -0
- package/scripts/smoke-web-autostart.js +47 -0
- package/scripts/smoke-web-port-fallback.js +82 -0
- package/scripts/smoke-webapp-command.js +97 -0
- package/src/cli.js +36 -1
- package/src/i18n.js +1 -0
- package/src/interactive-cli.js +46 -15
- package/src/web-autostart.js +122 -0
- package/web.js +32 -5
package/README.md
CHANGED
|
@@ -103,11 +103,15 @@ Provider signup and key pages:
|
|
|
103
103
|
| Qwen / DashScope | [https://bailian.console.aliyun.com/](https://bailian.console.aliyun.com/) | `https://dashscope-intl.aliyuncs.com/compatible-mode/v1` |
|
|
104
104
|
| GRS AI image tools | [https://grsai.ai/dashboard/api-keys](https://grsai.ai/dashboard/api-keys) | Configure with `/auxiliary grsai` or `aginti login grsai` |
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
The CLI quietly auto-starts or reuses the local web UI from the same project. It tries `http://127.0.0.1:3210` first, then `3211`, `3212`, and so on if the port is already occupied. The active URL is shown in the CLI launch header. If startup is blocked or unavailable, the same header row shows the recovery hint; run `/webapp [port]` inside the CLI to retry.
|
|
107
|
+
|
|
108
|
+
Package installation also makes a best-effort, non-blocking webapp initialization. Install never fails because the optional local webapp could not start.
|
|
109
|
+
|
|
110
|
+
Launch the web UI explicitly when you want a foreground web server:
|
|
107
111
|
|
|
108
112
|
```bash
|
|
109
113
|
aginti web --port 3210
|
|
110
|
-
#
|
|
114
|
+
# opens http://127.0.0.1:3210, or the next available port
|
|
111
115
|
```
|
|
112
116
|
|
|
113
117
|
Run without live model credentials for smoke tests:
|
|
@@ -129,7 +133,7 @@ aginti --language de
|
|
|
129
133
|
| Goal | Command |
|
|
130
134
|
| --- | --- |
|
|
131
135
|
| Start interactive chat | `aginti` or `aginti chat` |
|
|
132
|
-
| Start local web app | `aginti web --port 3210` |
|
|
136
|
+
| Start local web app | Auto-starts with `aginti`; foreground mode is `aginti web --port 3210` |
|
|
133
137
|
| Save provider keys | `aginti auth`, `/auth`, `/login` |
|
|
134
138
|
| Review current repo | `/review [focus]` |
|
|
135
139
|
| Toggle SCS quality gate | `/scs` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.124",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-aware intelligence, software automation, and industrial workflows.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"scripts/install-docker-ubuntu.sh",
|
|
51
51
|
"scripts/setup-agent-toolchain-docker.sh",
|
|
52
52
|
"scripts/real-deepseek-capabilities.js",
|
|
53
|
+
"scripts/postinstall-webapp.js",
|
|
53
54
|
"scripts/smoke-auxiliary-tools.js",
|
|
54
55
|
"scripts/smoke-auth.js",
|
|
55
56
|
"scripts/smoke-aaps-adapter.js",
|
|
@@ -68,7 +69,10 @@
|
|
|
68
69
|
"scripts/smoke-skillmesh.js",
|
|
69
70
|
"scripts/smoke-tmux-tools.js",
|
|
70
71
|
"scripts/smoke-toolchain-docker.js",
|
|
72
|
+
"scripts/smoke-web-autostart.js",
|
|
71
73
|
"scripts/smoke-web-api.js",
|
|
74
|
+
"scripts/smoke-webapp-command.js",
|
|
75
|
+
"scripts/smoke-web-port-fallback.js",
|
|
72
76
|
"src/",
|
|
73
77
|
"run.js",
|
|
74
78
|
"skills/",
|
|
@@ -100,11 +104,15 @@
|
|
|
100
104
|
"smoke:permission-modes": "node scripts/smoke-permission-modes.js",
|
|
101
105
|
"smoke:tmux-tools": "node scripts/smoke-tmux-tools.js",
|
|
102
106
|
"smoke:web-api": "node scripts/smoke-web-api.js",
|
|
107
|
+
"smoke:web-autostart": "node scripts/smoke-web-autostart.js",
|
|
108
|
+
"smoke:webapp-command": "node scripts/smoke-webapp-command.js",
|
|
109
|
+
"smoke:web-port-fallback": "node scripts/smoke-web-port-fallback.js",
|
|
103
110
|
"smoke:autoupdate": "node scripts/smoke-auto-update.js",
|
|
104
111
|
"real:deepseek": "node scripts/real-deepseek-capabilities.js",
|
|
112
|
+
"postinstall": "node scripts/postinstall-webapp.js",
|
|
105
113
|
"supervision:seed": "node scripts/seed-supervised-homework.js",
|
|
106
114
|
"storage:migrate": "node bin/aginti-cli.js storage migrate",
|
|
107
|
-
"test": "npm run check && npm run smoke:autoupdate && npm run smoke:web-api && npm run smoke:coding-tools && npm run smoke:dynamic-step-budget && npm run smoke:aaps-adapter && npm run smoke:auxiliary-tools && npm run smoke:perception-research && npm run smoke:auth && npm run smoke:canvas-artifacts && npm run smoke:capabilities && npm run smoke:model-roles && npm run smoke:platform && npm run smoke:permission-modes && npm run smoke:skills && npm run smoke:skillmesh && npm run smoke:tmux-tools && npm run smoke:cli-chat && npm run smoke:inbox",
|
|
115
|
+
"test": "npm run check && npm run smoke:autoupdate && npm run smoke:web-api && npm run smoke:web-autostart && npm run smoke:webapp-command && npm run smoke:web-port-fallback && npm run smoke:coding-tools && npm run smoke:dynamic-step-budget && npm run smoke:aaps-adapter && npm run smoke:auxiliary-tools && npm run smoke:perception-research && npm run smoke:auth && npm run smoke:canvas-artifacts && npm run smoke:capabilities && npm run smoke:model-roles && npm run smoke:platform && npm run smoke:permission-modes && npm run smoke:skills && npm run smoke:skillmesh && npm run smoke:tmux-tools && npm run smoke:cli-chat && npm run smoke:inbox",
|
|
108
116
|
"pack:dry-run": "npm pack --dry-run",
|
|
109
117
|
"smoke:capabilities": "node scripts/smoke-capabilities.js"
|
|
110
118
|
},
|
|
@@ -14,7 +14,7 @@ AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-a
|
|
|
14
14
|
|
|
15
15
|
Project-oriented programming for agentic workflows.
|
|
16
16
|
|
|
17
|
-
AAPS is a prompt-native programming language and visual studio for turning prompts into structured, verifiable pipelines. It connects wet and dry experiments, hardware and software, and human intent with executable agent work through tasks, typed inputs, declared outputs, validation gates, recovery steps, and durable artifacts.
|
|
17
|
+
AAPS is a project oriented and prompt-native programming language and visual studio for turning prompts into structured, verifiable pipelines. It connects wet and dry experiments, hardware and software, and human intent with executable agent work through tasks, typed inputs, declared outputs, validation gates, recovery steps, and durable artifacts.
|
|
18
18
|
|
|
19
19
|
## Shorter Version
|
|
20
20
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { ensureAgintiWebApp } from "../src/web-autostart.js";
|
|
5
|
+
|
|
6
|
+
const packageDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
7
|
+
const cwd = process.env.INIT_CWD || process.cwd();
|
|
8
|
+
|
|
9
|
+
if (process.env.CI === "true" || process.env.AGINTIFLOW_SKIP_POSTINSTALL_WEBAPP === "1") {
|
|
10
|
+
process.exit(0);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
await ensureAgintiWebApp({
|
|
15
|
+
packageDir,
|
|
16
|
+
cwd,
|
|
17
|
+
host: process.env.AGINTI_WEB_HOST || "127.0.0.1",
|
|
18
|
+
preferredPort: process.env.AGINTI_WEB_PORT || 3210,
|
|
19
|
+
language: process.env.AGINTI_LANGUAGE || "",
|
|
20
|
+
});
|
|
21
|
+
} catch {
|
|
22
|
+
// Installation must not fail because the optional local webapp could not be started.
|
|
23
|
+
}
|
|
@@ -317,6 +317,24 @@ try {
|
|
|
317
317
|
if (launchHeader.includes("launchTagline")) {
|
|
318
318
|
throw new Error("empty launch tagline should not render the i18n key name");
|
|
319
319
|
}
|
|
320
|
+
const launchHeaderWithWeb = buildLaunchHeaderLines({
|
|
321
|
+
width: 120,
|
|
322
|
+
packageVersion: "0.0.0",
|
|
323
|
+
animated: false,
|
|
324
|
+
webAppUrl: "http://127.0.0.1:3210",
|
|
325
|
+
}).join("\n");
|
|
326
|
+
if (!launchHeaderWithWeb.includes("webapp: http://127.0.0.1:3210")) {
|
|
327
|
+
throw new Error("launch header did not render the active webapp URL in the tagline row");
|
|
328
|
+
}
|
|
329
|
+
const launchHeaderWithWebError = buildLaunchHeaderLines({
|
|
330
|
+
width: 120,
|
|
331
|
+
packageVersion: "0.0.0",
|
|
332
|
+
animated: false,
|
|
333
|
+
webAppNotice: "webapp unavailable - use /webapp to retry; error: smoke failure",
|
|
334
|
+
}).join("\n");
|
|
335
|
+
if (!launchHeaderWithWebError.includes("webapp unavailable - use /webapp to retry")) {
|
|
336
|
+
throw new Error("launch header did not render webapp recovery advice in the tagline row");
|
|
337
|
+
}
|
|
320
338
|
if (
|
|
321
339
|
formatElapsedDuration(0) !== "00:00" ||
|
|
322
340
|
formatElapsedDuration(65_000) !== "01:05" ||
|
|
@@ -641,6 +659,7 @@ try {
|
|
|
641
659
|
!helpResult.stdout.includes("/auxiliary") ||
|
|
642
660
|
!helpResult.stdout.includes("/review") ||
|
|
643
661
|
!helpResult.stdout.includes("/scs") ||
|
|
662
|
+
!helpResult.stdout.includes("/webapp") ||
|
|
644
663
|
!helpResult.stdout.includes("/safe") ||
|
|
645
664
|
helpResult.stdout.includes("helpReview") ||
|
|
646
665
|
helpResult.stdout.includes("helpRename") ||
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { ensureAgintiWebApp } from "../src/web-autostart.js";
|
|
7
|
+
|
|
8
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
|
+
const runtimeDir = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-web-autostart-"));
|
|
10
|
+
const preferredPort = 44500 + Math.floor(Math.random() * 1000);
|
|
11
|
+
let childPid = 0;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const first = await ensureAgintiWebApp({
|
|
15
|
+
packageDir: repoRoot,
|
|
16
|
+
cwd: runtimeDir,
|
|
17
|
+
preferredPort,
|
|
18
|
+
host: "127.0.0.1",
|
|
19
|
+
});
|
|
20
|
+
if (!first.ok || !first.started || !first.url.includes(`:${preferredPort}`)) {
|
|
21
|
+
throw new Error(`expected auto-start on preferred port, got ${JSON.stringify(first)}`);
|
|
22
|
+
}
|
|
23
|
+
childPid = Number(first.pid) || 0;
|
|
24
|
+
const health = await fetch(`${first.url}/health`).then((response) => response.json());
|
|
25
|
+
if (!health.ok || Number(health.port) !== preferredPort) {
|
|
26
|
+
throw new Error(`auto-started web health was invalid: ${JSON.stringify(health)}`);
|
|
27
|
+
}
|
|
28
|
+
const second = await ensureAgintiWebApp({
|
|
29
|
+
packageDir: repoRoot,
|
|
30
|
+
cwd: runtimeDir,
|
|
31
|
+
preferredPort,
|
|
32
|
+
host: "127.0.0.1",
|
|
33
|
+
});
|
|
34
|
+
if (!second.ok || !second.reused || second.url !== first.url) {
|
|
35
|
+
throw new Error(`expected second auto-start call to reuse existing webapp, got ${JSON.stringify(second)}`);
|
|
36
|
+
}
|
|
37
|
+
console.log(`web auto-start smoke passed: ${first.url}`);
|
|
38
|
+
} finally {
|
|
39
|
+
if (childPid) {
|
|
40
|
+
try {
|
|
41
|
+
process.kill(childPid, "SIGTERM");
|
|
42
|
+
} catch {
|
|
43
|
+
// The child may have already exited during a failed smoke test.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
await fs.rm(runtimeDir, { recursive: true, force: true });
|
|
47
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import net from "node:net";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
+
const runtimeDir = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-web-port-fallback-"));
|
|
11
|
+
const host = "127.0.0.1";
|
|
12
|
+
const occupiedPort = 43100 + Math.floor(Math.random() * 1200);
|
|
13
|
+
|
|
14
|
+
function listenOccupier(port) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const server = net.createServer();
|
|
17
|
+
server.once("error", reject);
|
|
18
|
+
server.listen(port, host, () => resolve(server));
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function delay(ms) {
|
|
23
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function waitForPrintedPort(child, stdout, stderr) {
|
|
27
|
+
const deadline = Date.now() + 12000;
|
|
28
|
+
while (Date.now() < deadline) {
|
|
29
|
+
if (child.exitCode !== null) break;
|
|
30
|
+
const match = stdout().match(/http:\/\/127\.0\.0\.1:(\d+)/);
|
|
31
|
+
if (match) return Number(match[1]);
|
|
32
|
+
await delay(100);
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`web fallback URL was not printed. stdout=${stdout()} stderr=${stderr()}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function waitForHealth(port, child, stdout, stderr) {
|
|
38
|
+
const deadline = Date.now() + 12000;
|
|
39
|
+
while (Date.now() < deadline) {
|
|
40
|
+
if (child.exitCode !== null) break;
|
|
41
|
+
try {
|
|
42
|
+
const response = await fetch(`http://${host}:${port}/health`);
|
|
43
|
+
const body = await response.json();
|
|
44
|
+
if (response.ok && body.ok && Number(body.port) === port) return body;
|
|
45
|
+
} catch {
|
|
46
|
+
await delay(200);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
throw new Error(`web fallback health failed. stdout=${stdout()} stderr=${stderr()}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const occupier = await listenOccupier(occupiedPort);
|
|
53
|
+
let stdout = "";
|
|
54
|
+
let stderr = "";
|
|
55
|
+
const child = spawn(process.execPath, [path.join(repoRoot, "bin/aginti-cli.js"), "web", "--port", String(occupiedPort), "--host", host], {
|
|
56
|
+
cwd: runtimeDir,
|
|
57
|
+
env: {
|
|
58
|
+
...process.env,
|
|
59
|
+
AGINTIFLOW_RUNTIME_DIR: runtimeDir,
|
|
60
|
+
AGINTIFLOW_HOME: path.join(runtimeDir, ".agintiflow-home"),
|
|
61
|
+
},
|
|
62
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
63
|
+
});
|
|
64
|
+
child.stdout.on("data", (chunk) => {
|
|
65
|
+
stdout += chunk.toString();
|
|
66
|
+
});
|
|
67
|
+
child.stderr.on("data", (chunk) => {
|
|
68
|
+
stderr += chunk.toString();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const actualPort = await waitForPrintedPort(child, () => stdout.slice(-500), () => stderr.slice(-500));
|
|
73
|
+
if (actualPort <= occupiedPort) {
|
|
74
|
+
throw new Error(`web command did not move past occupied port ${occupiedPort}; got ${actualPort}`);
|
|
75
|
+
}
|
|
76
|
+
await waitForHealth(actualPort, child, () => stdout.slice(-500), () => stderr.slice(-500));
|
|
77
|
+
console.log(`web port fallback smoke passed: ${occupiedPort} -> ${actualPort}`);
|
|
78
|
+
} finally {
|
|
79
|
+
child.kill("SIGTERM");
|
|
80
|
+
occupier.close();
|
|
81
|
+
await fs.rm(runtimeDir, { recursive: true, force: true });
|
|
82
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFile as execFileCallback, spawn } from "node:child_process";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
+
const runtimeDir = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-webapp-command-"));
|
|
11
|
+
const execFile = promisify(execFileCallback);
|
|
12
|
+
|
|
13
|
+
function delay(ms) {
|
|
14
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function waitFor(predicate, child, label, output) {
|
|
18
|
+
const deadline = Date.now() + 12000;
|
|
19
|
+
while (Date.now() < deadline) {
|
|
20
|
+
if (predicate()) return true;
|
|
21
|
+
if (child.exitCode !== null) break;
|
|
22
|
+
await delay(150);
|
|
23
|
+
}
|
|
24
|
+
const { stdout, stderr } = output;
|
|
25
|
+
throw new Error(`${label} timed out. stdout=${stdout.slice(-1000)} stderr=${stderr.slice(-1000)}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function killPort(port) {
|
|
29
|
+
try {
|
|
30
|
+
const { stdout: pids } = await execFile("lsof", [`-tiTCP:${port}`, "-sTCP:LISTEN"]);
|
|
31
|
+
for (const pid of pids.split(/\s+/).filter(Boolean)) {
|
|
32
|
+
try {
|
|
33
|
+
process.kill(Number(pid), "SIGTERM");
|
|
34
|
+
} catch {
|
|
35
|
+
// Ignore already-exited listeners.
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
// lsof may be unavailable; the temporary port listener will otherwise be reusable by later tests.
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function runCase({ port, env = {}, expectHeader, label }) {
|
|
44
|
+
const output = { stdout: "", stderr: "" };
|
|
45
|
+
const child = spawn(process.execPath, [path.join(repoRoot, "bin/aginti-cli.js"), "chat", "--provider", "mock", "--routing", "manual", "--port", String(port)], {
|
|
46
|
+
cwd: runtimeDir,
|
|
47
|
+
env: {
|
|
48
|
+
...process.env,
|
|
49
|
+
AGINTIFLOW_NO_ANIMATION: "1",
|
|
50
|
+
AGINTIFLOW_HOME: path.join(runtimeDir, `.agintiflow-home-${label}`),
|
|
51
|
+
...env,
|
|
52
|
+
},
|
|
53
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
child.stdout.on("data", (chunk) => {
|
|
57
|
+
output.stdout += chunk.toString();
|
|
58
|
+
});
|
|
59
|
+
child.stderr.on("data", (chunk) => {
|
|
60
|
+
output.stderr += chunk.toString();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
await waitFor(() => output.stdout.includes(expectHeader), child, `${label} launch header`, output);
|
|
65
|
+
child.stdin.write(`/webapp ${port}\n`);
|
|
66
|
+
await waitFor(() => output.stdout.includes(`webapp=http://127.0.0.1:${port}`), child, `${label} /webapp command`, output);
|
|
67
|
+
const health = await fetch(`http://127.0.0.1:${port}/health`).then((response) => response.json());
|
|
68
|
+
if (!health.ok || health.app !== "agintiflow" || Number(health.port) !== port) {
|
|
69
|
+
throw new Error(`invalid /webapp health response for ${label}: ${JSON.stringify(health)}`);
|
|
70
|
+
}
|
|
71
|
+
} finally {
|
|
72
|
+
child.kill("SIGTERM");
|
|
73
|
+
await killPort(port);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const autoPort = 45600 + Math.floor(Math.random() * 300);
|
|
78
|
+
const manualPort = autoPort + 301;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
await runCase({
|
|
82
|
+
port: autoPort,
|
|
83
|
+
expectHeader: `webapp: http://127.0.0.1:${autoPort}`,
|
|
84
|
+
label: "auto-start",
|
|
85
|
+
});
|
|
86
|
+
await runCase({
|
|
87
|
+
port: manualPort,
|
|
88
|
+
env: { AGINTIFLOW_NO_WEB_AUTO_START: "1" },
|
|
89
|
+
expectHeader: "webapp auto-start disabled - use /webapp to start manually",
|
|
90
|
+
label: "manual-disabled",
|
|
91
|
+
});
|
|
92
|
+
console.log(`webapp slash command smoke passed: http://127.0.0.1:${autoPort}, http://127.0.0.1:${manualPort}`);
|
|
93
|
+
} finally {
|
|
94
|
+
await killPort(autoPort);
|
|
95
|
+
await killPort(manualPort);
|
|
96
|
+
await fs.rm(runtimeDir, { recursive: true, force: true });
|
|
97
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -37,6 +37,7 @@ import { handleSkillMeshCommand } from "./skillmesh.js";
|
|
|
37
37
|
import { handleAapsCliCommand } from "./aaps-adapter.js";
|
|
38
38
|
import { formatInstructionTemplateList, normalizeInstructionTemplate } from "./behavior-contract.js";
|
|
39
39
|
import { applyPermissionMode, normalizePermissionMode } from "./permission-modes.js";
|
|
40
|
+
import { ensureAgintiWebApp } from "./web-autostart.js";
|
|
40
41
|
import fs from "node:fs/promises";
|
|
41
42
|
import path from "node:path";
|
|
42
43
|
import { fileURLToPath } from "node:url";
|
|
@@ -91,6 +92,29 @@ function printUnknownCliOptions(options = []) {
|
|
|
91
92
|
console.error("Use `--` before a prompt that intentionally starts with a dash.");
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
async function maybeEnsureDefaultWebApp(args = {}, { commandCwd = process.cwd() } = {}) {
|
|
96
|
+
if (args.web) return { ok: false, url: "" };
|
|
97
|
+
try {
|
|
98
|
+
return await ensureAgintiWebApp({
|
|
99
|
+
packageDir,
|
|
100
|
+
cwd: args.commandCwd || commandCwd || process.cwd(),
|
|
101
|
+
host: args.host || process.env.AGINTI_WEB_HOST || "127.0.0.1",
|
|
102
|
+
preferredPort: args.port || process.env.AGINTI_WEB_PORT || 3210,
|
|
103
|
+
language: args.language ? resolveLanguage(args.language) : "",
|
|
104
|
+
});
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return { ok: false, url: "", error: error instanceof Error ? error.message : String(error) };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function webAppNotice(result) {
|
|
111
|
+
if (!result || result.ok) return "";
|
|
112
|
+
if (result.disabled) return "webapp auto-start disabled - use /webapp to start manually";
|
|
113
|
+
const error = String(result.error || "unknown").replace(/\s+/g, " ").trim();
|
|
114
|
+
const compact = error.length > 84 ? `${error.slice(0, 83)}…` : error;
|
|
115
|
+
return `webapp unavailable - use /webapp to retry; error: ${compact}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
94
118
|
const CLI_VALUE_OPTIONS = new Set([
|
|
95
119
|
"--port",
|
|
96
120
|
"--host",
|
|
@@ -1734,9 +1758,12 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1734
1758
|
if (!prompt) {
|
|
1735
1759
|
const parsedResumeOptions = parseArgs(resumeOptions.optionArgv);
|
|
1736
1760
|
exitOnUnknownOptions(parsedResumeOptions);
|
|
1761
|
+
const webLaunch = await maybeEnsureDefaultWebApp(parsedResumeOptions, { commandCwd });
|
|
1737
1762
|
await startInteractiveCli(agentDefaults({ ...parsedResumeOptions, resume: sessionId, commandCwd: parsedResumeOptions.commandCwd || commandCwd }), {
|
|
1738
1763
|
packageDir,
|
|
1739
1764
|
packageVersion: packageJson.version,
|
|
1765
|
+
webAppUrl: webLaunch.ok ? webLaunch.url : "",
|
|
1766
|
+
webAppNotice: webAppNotice(webLaunch),
|
|
1740
1767
|
});
|
|
1741
1768
|
return;
|
|
1742
1769
|
}
|
|
@@ -1744,6 +1771,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1744
1771
|
exitOnUnknownOptions(parsedResumeArgs);
|
|
1745
1772
|
const resumeArgs = agentDefaults({ ...parsedResumeArgs, resume: sessionId, goal: prompt, commandCwd: parsedResumeArgs.commandCwd || commandCwd });
|
|
1746
1773
|
if (!(await ensureDeepSeekKeyForOneShot(resumeArgs))) process.exit(1);
|
|
1774
|
+
await maybeEnsureDefaultWebApp(resumeArgs, { commandCwd });
|
|
1747
1775
|
const config = loadConfig(resumeArgs, { packageDir });
|
|
1748
1776
|
await runAgent(config);
|
|
1749
1777
|
return;
|
|
@@ -1763,7 +1791,13 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1763
1791
|
}
|
|
1764
1792
|
|
|
1765
1793
|
if (args.interactive || (!args.goal && !args.resume && process.stdin.isTTY)) {
|
|
1766
|
-
await
|
|
1794
|
+
const webLaunch = await maybeEnsureDefaultWebApp(args, { commandCwd });
|
|
1795
|
+
await startInteractiveCli(agentDefaults(args), {
|
|
1796
|
+
packageDir,
|
|
1797
|
+
packageVersion: packageJson.version,
|
|
1798
|
+
webAppUrl: webLaunch.ok ? webLaunch.url : "",
|
|
1799
|
+
webAppNotice: webAppNotice(webLaunch),
|
|
1800
|
+
});
|
|
1767
1801
|
return;
|
|
1768
1802
|
}
|
|
1769
1803
|
|
|
@@ -1808,6 +1842,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1808
1842
|
|
|
1809
1843
|
const finalArgs = agentDefaults(args);
|
|
1810
1844
|
if (!(await ensureDeepSeekKeyForOneShot(finalArgs))) process.exit(1);
|
|
1845
|
+
await maybeEnsureDefaultWebApp(finalArgs, { commandCwd });
|
|
1811
1846
|
const config = loadConfig(finalArgs, { packageDir });
|
|
1812
1847
|
await runAgent(config);
|
|
1813
1848
|
}
|
package/src/i18n.js
CHANGED
|
@@ -139,6 +139,7 @@ const TRANSLATIONS = {
|
|
|
139
139
|
helpSkills: "List Markdown skills selected for a topic.",
|
|
140
140
|
helpSkillMesh: "Manage strict reviewed skill sharing.",
|
|
141
141
|
helpProfile: "Set task profile, e.g. code, website, latex, maintenance.",
|
|
142
|
+
helpWebapp: "Start or reuse the local webapp and print its URL.",
|
|
142
143
|
helpWebSearch: "Enable or disable the web_search tool.",
|
|
143
144
|
helpEnableScs: "Toggle Student-Committee-Supervisor gated execution.",
|
|
144
145
|
helpScouts: "Enable parallel DeepSeek scouts and set scout count.",
|
package/src/interactive-cli.js
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
permissionModeForApprovalCategory,
|
|
44
44
|
permissionModeLabel,
|
|
45
45
|
} from "./permission-modes.js";
|
|
46
|
+
import { ensureAgintiWebApp } from "./web-autostart.js";
|
|
46
47
|
|
|
47
48
|
const useColor = Boolean(input.isTTY && output.isTTY && process.env.AGINTIFLOW_NO_COLOR !== "1");
|
|
48
49
|
const ansi = {
|
|
@@ -95,6 +96,8 @@ const SLASH_COMMANDS = [
|
|
|
95
96
|
"/skillsync",
|
|
96
97
|
"/profile",
|
|
97
98
|
"/web-search",
|
|
99
|
+
"/webapp",
|
|
100
|
+
"/web",
|
|
98
101
|
"/web-research",
|
|
99
102
|
"/image-read",
|
|
100
103
|
"/research-wrapper",
|
|
@@ -772,10 +775,19 @@ function launchTitleLines(contentWidth) {
|
|
|
772
775
|
return largeWidth <= contentWidth ? LARGE_LAUNCH_TITLE : COMPACT_LAUNCH_TITLE;
|
|
773
776
|
}
|
|
774
777
|
|
|
775
|
-
export function buildLaunchHeaderLines({
|
|
778
|
+
export function buildLaunchHeaderLines({
|
|
779
|
+
packageVersion = "",
|
|
780
|
+
frame = 2,
|
|
781
|
+
width = terminalWidth(),
|
|
782
|
+
animated = true,
|
|
783
|
+
language = cliLanguage,
|
|
784
|
+
webAppUrl = "",
|
|
785
|
+
webAppNotice = "",
|
|
786
|
+
} = {}) {
|
|
776
787
|
const subtitle = t("launchSubtitle", language);
|
|
777
788
|
const credit = t("launchCredit", language);
|
|
778
|
-
const
|
|
789
|
+
const webappLine = webAppUrl ? `webapp: ${webAppUrl}` : webAppNotice;
|
|
790
|
+
const tagline = webappLine || t("launchTagline", language);
|
|
779
791
|
const version = packageVersion ? `v${packageVersion}` : "";
|
|
780
792
|
const terminalColumns = Math.max(Number(width) || 80, 50);
|
|
781
793
|
const contentWidth = Math.min(Math.max(terminalColumns - 8, 58), 112);
|
|
@@ -794,16 +806,16 @@ export function buildLaunchHeaderLines({ packageVersion = "", frame = 2, width =
|
|
|
794
806
|
row(centerLine(subtitle, contentWidth), ansi.dim),
|
|
795
807
|
row(centerLine(credit, contentWidth), ansi.dim),
|
|
796
808
|
tagline ? mid : "",
|
|
797
|
-
tagline ? row(centerLine(tagline, contentWidth), ansi.cyan) : "",
|
|
809
|
+
tagline ? row(centerLine(compactLine(tagline, contentWidth), contentWidth), webAppUrl ? ansi.cyan : ansi.yellow) : "",
|
|
798
810
|
bottom,
|
|
799
811
|
].filter(Boolean);
|
|
800
812
|
const indent = " ".repeat(Math.max(Math.floor((terminalColumns - Math.max(...boxLines.map((line) => visualLength(line)))) / 2), 0));
|
|
801
813
|
return boxLines.map((line) => `${indent}${line}`);
|
|
802
814
|
}
|
|
803
815
|
|
|
804
|
-
async function renderLaunchHeader(packageVersion = "", language = cliLanguage) {
|
|
816
|
+
async function renderLaunchHeader(packageVersion = "", language = cliLanguage, webAppUrl = "", webAppNotice = "") {
|
|
805
817
|
if (!useColor || process.env.AGINTIFLOW_NO_ANIMATION === "1") {
|
|
806
|
-
console.log(buildLaunchHeaderLines({ packageVersion, animated: false, language }).join("\n"));
|
|
818
|
+
console.log(buildLaunchHeaderLines({ packageVersion, animated: false, language, webAppUrl, webAppNotice }).join("\n"));
|
|
807
819
|
return;
|
|
808
820
|
}
|
|
809
821
|
|
|
@@ -812,7 +824,7 @@ async function renderLaunchHeader(packageVersion = "", language = cliLanguage) {
|
|
|
812
824
|
let previousLineCount = 0;
|
|
813
825
|
output.write(ansi.cursorHide);
|
|
814
826
|
for (let frame = 0; frame < 18; frame += 1) {
|
|
815
|
-
const lines = [...paddingLines, ...buildLaunchHeaderLines({ packageVersion, frame, animated: true, language })];
|
|
827
|
+
const lines = [...paddingLines, ...buildLaunchHeaderLines({ packageVersion, frame, animated: true, language, webAppUrl, webAppNotice })];
|
|
816
828
|
if (previousLineCount > 0) output.write(`\x1b[${previousLineCount}A`);
|
|
817
829
|
output.write(lines.map((line) => `\r${ansi.clearLine}${line}`).join("\n"));
|
|
818
830
|
output.write("\n");
|
|
@@ -849,6 +861,7 @@ function printHelp() {
|
|
|
849
861
|
` ${command("/skills [query]", "List Markdown skills selected for a topic.", "helpSkills")}`,
|
|
850
862
|
` ${command("/skillmesh [status|off|record|share|sync]", "Manage strict reviewed skill sharing.", "helpSkillMesh")}`,
|
|
851
863
|
` ${command("/profile <name>", "Set task profile, e.g. code, website, latex, maintenance.", "helpProfile")}`,
|
|
864
|
+
` ${command("/webapp [port]", "Start or reuse the local webapp and print its URL.", "helpWebapp")}`,
|
|
852
865
|
` ${command("/web-search on|off", "Enable or disable the web_search tool.", "helpWebSearch")}`,
|
|
853
866
|
` ${command("/web-research <query>", "Run a sourced web_research turn with persisted evidence.", "helpWebSearch")}`,
|
|
854
867
|
` ${command("/image-read <path> [question]", "Run read_image on a workspace screenshot/image.", "helpWebSearch")}`,
|
|
@@ -2170,6 +2183,8 @@ function createState(args = {}) {
|
|
|
2170
2183
|
dynamicStepHardCap: args.dynamicStepHardCap,
|
|
2171
2184
|
dynamicStepExtensionSize: args.dynamicStepExtensionSize,
|
|
2172
2185
|
sessionId: args.resume || "",
|
|
2186
|
+
webAppUrl: args.webAppUrl || "",
|
|
2187
|
+
webAppNotice: args.webAppNotice || "",
|
|
2173
2188
|
};
|
|
2174
2189
|
}
|
|
2175
2190
|
|
|
@@ -2975,6 +2990,28 @@ async function handleCommand(line, state, packageDir) {
|
|
|
2975
2990
|
);
|
|
2976
2991
|
return true;
|
|
2977
2992
|
}
|
|
2993
|
+
if (command === "webapp" || command === "web") {
|
|
2994
|
+
const port = Number(value) || Number(process.env.AGINTI_WEB_PORT || process.env.PORT || 3210);
|
|
2995
|
+
printSystemLine("webapp=starting");
|
|
2996
|
+
const result = await ensureAgintiWebApp({
|
|
2997
|
+
packageDir,
|
|
2998
|
+
cwd: state.commandCwd || process.cwd(),
|
|
2999
|
+
host: process.env.AGINTI_WEB_HOST || process.env.HOST || "127.0.0.1",
|
|
3000
|
+
preferredPort: port,
|
|
3001
|
+
language: state.language,
|
|
3002
|
+
respectAutoStartDisable: false,
|
|
3003
|
+
}).catch((error) => ({ ok: false, error: error instanceof Error ? error.message : String(error), url: "" }));
|
|
3004
|
+
if (result.ok) {
|
|
3005
|
+
state.webAppUrl = result.url;
|
|
3006
|
+
state.webAppNotice = "";
|
|
3007
|
+
printSystemLine(`webapp=${result.url} ${result.reused ? "reused" : "started"}`);
|
|
3008
|
+
} else {
|
|
3009
|
+
state.webAppUrl = "";
|
|
3010
|
+
state.webAppNotice = `webapp unavailable - use /webapp to retry; error: ${compactLine(result.error || "unknown", 72)}`;
|
|
3011
|
+
printSystemLine(state.webAppNotice);
|
|
3012
|
+
}
|
|
3013
|
+
return true;
|
|
3014
|
+
}
|
|
2978
3015
|
if (command === "language" || command === "lang") {
|
|
2979
3016
|
if (!value) {
|
|
2980
3017
|
printSystemLine(tr("languageSet", { language: state.language || cliLanguage, label: languageLabel(state.language || cliLanguage) }));
|
|
@@ -3599,12 +3636,6 @@ async function handleCommand(line, state, packageDir) {
|
|
|
3599
3636
|
printAgentMessage(`initialized project=${result.projectRoot}\nAGINTI.md=${result.instructionsPath}\ntemplate=${result.template}`);
|
|
3600
3637
|
return true;
|
|
3601
3638
|
}
|
|
3602
|
-
if (command === "web") {
|
|
3603
|
-
const port = value || "3220";
|
|
3604
|
-
printAgentMessage(`Run in another terminal: aginti web --port ${port}`);
|
|
3605
|
-
return true;
|
|
3606
|
-
}
|
|
3607
|
-
|
|
3608
3639
|
const typed = `/${command}`;
|
|
3609
3640
|
const suggestions = SLASH_COMMANDS.filter((candidate) => candidate.startsWith(typed));
|
|
3610
3641
|
printAgentMessage(
|
|
@@ -3808,8 +3839,8 @@ async function runPrompt(prompt, state, packageDir, { approvalDepth = 0 } = {})
|
|
|
3808
3839
|
return queuedAfterFinish;
|
|
3809
3840
|
}
|
|
3810
3841
|
|
|
3811
|
-
export async function startInteractiveCli(args = {}, { packageDir, packageVersion } = {}) {
|
|
3812
|
-
const state = createState(args);
|
|
3842
|
+
export async function startInteractiveCli(args = {}, { packageDir, packageVersion, webAppUrl = "", webAppNotice = "" } = {}) {
|
|
3843
|
+
const state = createState({ ...args, webAppUrl, webAppNotice });
|
|
3813
3844
|
const detachProcessInterrupts = installProcessInterruptHandlers();
|
|
3814
3845
|
setCliLanguage(state.language);
|
|
3815
3846
|
const rl =
|
|
@@ -3822,7 +3853,7 @@ export async function startInteractiveCli(args = {}, { packageDir, packageVersio
|
|
|
3822
3853
|
completer: commandCompleter,
|
|
3823
3854
|
});
|
|
3824
3855
|
|
|
3825
|
-
await renderLaunchHeader(packageVersion, state.language);
|
|
3856
|
+
await renderLaunchHeader(packageVersion, state.language, state.webAppUrl, state.webAppNotice);
|
|
3826
3857
|
printSystemLine(`${tr("project")}: ${process.cwd()}`);
|
|
3827
3858
|
await maybeOnboardDeepSeekKey(state);
|
|
3828
3859
|
printAgentMessage(tr("interactiveIntro"));
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import http from "node:http";
|
|
3
|
+
import net from "node:net";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_HOST = "127.0.0.1";
|
|
7
|
+
const DEFAULT_PORT = 3210;
|
|
8
|
+
const MAX_PORT_ATTEMPTS = 50;
|
|
9
|
+
|
|
10
|
+
function normalizeHost(host) {
|
|
11
|
+
return String(host || process.env.AGINTI_WEB_HOST || process.env.HOST || DEFAULT_HOST).trim() || DEFAULT_HOST;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizePort(port) {
|
|
15
|
+
const parsed = Number(port || process.env.AGINTI_WEB_PORT || process.env.PORT || DEFAULT_PORT);
|
|
16
|
+
return Number.isInteger(parsed) && parsed > 0 && parsed < 65536 ? parsed : DEFAULT_PORT;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function webUrl(host, port) {
|
|
20
|
+
return `http://${host}:${port}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function fetchHealth(host, port, timeoutMs = 450) {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
const req = http.get(`${webUrl(host, port)}/health`, { timeout: timeoutMs }, (res) => {
|
|
26
|
+
let body = "";
|
|
27
|
+
res.setEncoding("utf8");
|
|
28
|
+
res.on("data", (chunk) => {
|
|
29
|
+
body += chunk;
|
|
30
|
+
});
|
|
31
|
+
res.on("end", () => {
|
|
32
|
+
try {
|
|
33
|
+
const json = JSON.parse(body || "{}");
|
|
34
|
+
resolve(Boolean(res.statusCode === 200 && json.ok && (json.app === "agintiflow" || Number(json.port) === port)));
|
|
35
|
+
} catch {
|
|
36
|
+
resolve(false);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
req.on("timeout", () => {
|
|
41
|
+
req.destroy();
|
|
42
|
+
resolve(false);
|
|
43
|
+
});
|
|
44
|
+
req.on("error", () => resolve(false));
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function canListen(host, port) {
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
const server = net.createServer();
|
|
51
|
+
server.once("error", () => resolve(false));
|
|
52
|
+
server.listen(port, host, () => {
|
|
53
|
+
server.close(() => resolve(true));
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function waitForHealth(host, port, timeoutMs = 7000) {
|
|
59
|
+
const deadline = Date.now() + timeoutMs;
|
|
60
|
+
while (Date.now() < deadline) {
|
|
61
|
+
if (await fetchHealth(host, port, 350)) return true;
|
|
62
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function findReusableOrFreeWebPort({ host = DEFAULT_HOST, preferredPort = DEFAULT_PORT, attempts = MAX_PORT_ATTEMPTS } = {}) {
|
|
68
|
+
const startPort = normalizePort(preferredPort);
|
|
69
|
+
const normalizedHost = normalizeHost(host);
|
|
70
|
+
for (let offset = 0; offset < attempts; offset += 1) {
|
|
71
|
+
const port = startPort + offset;
|
|
72
|
+
if (port >= 65536) break;
|
|
73
|
+
if (await fetchHealth(normalizedHost, port)) {
|
|
74
|
+
return { port, host: normalizedHost, url: webUrl(normalizedHost, port), reused: true, available: false };
|
|
75
|
+
}
|
|
76
|
+
if (await canListen(normalizedHost, port)) {
|
|
77
|
+
return { port, host: normalizedHost, url: webUrl(normalizedHost, port), reused: false, available: true };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { port: 0, host: normalizedHost, url: "", reused: false, available: false };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function ensureAgintiWebApp({
|
|
84
|
+
packageDir = process.cwd(),
|
|
85
|
+
cwd = process.cwd(),
|
|
86
|
+
host = DEFAULT_HOST,
|
|
87
|
+
preferredPort = DEFAULT_PORT,
|
|
88
|
+
language = "",
|
|
89
|
+
respectAutoStartDisable = true,
|
|
90
|
+
} = {}) {
|
|
91
|
+
if (respectAutoStartDisable && (process.env.AGINTI_NO_WEB_AUTO_START === "1" || process.env.AGINTIFLOW_NO_WEB_AUTO_START === "1")) {
|
|
92
|
+
return { ok: false, disabled: true, url: "" };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const candidate = await findReusableOrFreeWebPort({ host, preferredPort });
|
|
96
|
+
if (!candidate.port) {
|
|
97
|
+
return { ok: false, error: `No available AgInTiFlow web port from ${normalizePort(preferredPort)}.`, url: "" };
|
|
98
|
+
}
|
|
99
|
+
if (candidate.reused) {
|
|
100
|
+
return { ok: true, reused: true, started: false, ...candidate };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const child = spawn(process.execPath, [path.join(packageDir, "web.js")], {
|
|
104
|
+
cwd,
|
|
105
|
+
detached: true,
|
|
106
|
+
stdio: "ignore",
|
|
107
|
+
env: {
|
|
108
|
+
...process.env,
|
|
109
|
+
HOST: candidate.host,
|
|
110
|
+
PORT: String(candidate.port),
|
|
111
|
+
AGINTIFLOW_RUNTIME_DIR: cwd,
|
|
112
|
+
AGINTIFLOW_PACKAGE_DIR: packageDir,
|
|
113
|
+
...(language ? { AGINTI_LANGUAGE: language } : {}),
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
child.unref();
|
|
117
|
+
|
|
118
|
+
const healthy = await waitForHealth(candidate.host, candidate.port);
|
|
119
|
+
return healthy
|
|
120
|
+
? { ok: true, reused: false, started: true, pid: child.pid, ...candidate }
|
|
121
|
+
: { ok: false, error: `Started web process ${child.pid}, but ${candidate.url}/health did not become ready.`, url: "" };
|
|
122
|
+
}
|
package/web.js
CHANGED
|
@@ -58,7 +58,7 @@ loadProjectEnv(baseDir);
|
|
|
58
58
|
await ensureProjectSessionStorage(baseDir);
|
|
59
59
|
|
|
60
60
|
const app = express();
|
|
61
|
-
|
|
61
|
+
let port = Number(process.env.PORT || 3210);
|
|
62
62
|
const host = process.env.HOST || "127.0.0.1";
|
|
63
63
|
const runs = new Map();
|
|
64
64
|
const db = new WebDatabase(baseDir);
|
|
@@ -1337,12 +1337,39 @@ app.post("/api/runs/:sessionId/stop", async (req, res) => {
|
|
|
1337
1337
|
});
|
|
1338
1338
|
|
|
1339
1339
|
app.get("/health", (_req, res) => {
|
|
1340
|
-
res.json({ ok: true, port });
|
|
1340
|
+
res.json({ ok: true, app: "agintiflow", port, url: `http://${host}:${port}` });
|
|
1341
1341
|
});
|
|
1342
1342
|
|
|
1343
1343
|
await fs.mkdir(sessionsDir, { recursive: true });
|
|
1344
1344
|
await syncStoredSessions();
|
|
1345
1345
|
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
});
|
|
1346
|
+
function listenOnPort(candidatePort) {
|
|
1347
|
+
return new Promise((resolve, reject) => {
|
|
1348
|
+
const server = app.listen({ port: candidatePort, host });
|
|
1349
|
+
server.once("error", (error) => reject(error));
|
|
1350
|
+
server.once("listening", () => resolve(server));
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
async function listenWithFallback(preferredPort, attempts = 50) {
|
|
1355
|
+
const startPort = Number.isInteger(Number(preferredPort)) && Number(preferredPort) > 0 ? Number(preferredPort) : 3210;
|
|
1356
|
+
let lastError = null;
|
|
1357
|
+
for (let offset = 0; offset < attempts; offset += 1) {
|
|
1358
|
+
const candidatePort = startPort + offset;
|
|
1359
|
+
if (candidatePort >= 65536) break;
|
|
1360
|
+
try {
|
|
1361
|
+
return await listenOnPort(candidatePort);
|
|
1362
|
+
} catch (error) {
|
|
1363
|
+
lastError = error;
|
|
1364
|
+
if (error?.code !== "EADDRINUSE") throw error;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
const error = new Error(`No available AgInTiFlow web port from ${startPort} after ${attempts} attempts.`);
|
|
1368
|
+
error.cause = lastError;
|
|
1369
|
+
throw error;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
export const webServer = await listenWithFallback(port);
|
|
1373
|
+
port = webServer.address()?.port || port;
|
|
1374
|
+
export const webUrl = `http://${host}:${port}`;
|
|
1375
|
+
console.log(`Website control agent UI running on ${webUrl}`);
|