@lazyingart/agintiflow 0.20.127 → 0.20.129
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 +4 -2
- package/package.json +1 -1
- package/public/app.js +14 -10
- package/scripts/smoke-auto-update.js +48 -0
- package/scripts/smoke-canvas-artifacts.js +36 -0
- package/scripts/smoke-capabilities.js +2 -0
- package/scripts/smoke-cli-chat.js +2 -0
- package/scripts/smoke-web-autostart.js +68 -0
- package/scripts/smoke-webapp-command.js +7 -1
- package/src/auto-update.js +40 -4
- package/src/cli.js +95 -1
- package/src/i18n.js +1 -1
- package/src/interactive-cli.js +8 -4
- package/src/web-autostart.js +166 -15
- package/web.js +15 -1
package/README.md
CHANGED
|
@@ -103,13 +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
|
-
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.
|
|
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 by another project. The active URL is shown in the CLI launch header. If startup is blocked, stale, or unavailable, the same header row shows the recovery hint; run `/webapp [port]` inside the CLI to retry, or `/webapp restart [port]` to stop and relaunch the local webapp with the current project and canonical `~/.agintiflow` session home. After a successful `aginti update` or accepted startup auto-update, AgInTiFlow also restarts the compatible local webapp so artifact serving uses the updated package.
|
|
107
107
|
|
|
108
108
|
Package installation also makes a best-effort, non-blocking webapp initialization. Install never fails because the optional local webapp could not start.
|
|
109
109
|
|
|
110
110
|
Launch the web UI explicitly when you want a foreground web server:
|
|
111
111
|
|
|
112
112
|
```bash
|
|
113
|
+
aginti webapp
|
|
114
|
+
aginti webapp restart
|
|
113
115
|
aginti web --port 3210
|
|
114
116
|
# opens http://127.0.0.1:3210, or the next available port
|
|
115
117
|
```
|
|
@@ -143,7 +145,7 @@ aginti --language de
|
|
|
143
145
|
| Goal | Command |
|
|
144
146
|
| --- | --- |
|
|
145
147
|
| Start interactive chat | `aginti` or `aginti chat` |
|
|
146
|
-
| Start local web app | Auto-starts with `aginti`; foreground mode is `aginti web --port 3210` |
|
|
148
|
+
| Start local web app | Auto-starts with `aginti`; detached command is `aginti webapp`; restart with `aginti webapp restart`; foreground mode is `aginti web --port 3210` |
|
|
147
149
|
| Save provider keys | `aginti auth`, `/auth`, `/login` |
|
|
148
150
|
| Review current repo | `/review [focus]` |
|
|
149
151
|
| 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.129",
|
|
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",
|
package/public/app.js
CHANGED
|
@@ -2083,7 +2083,7 @@ function renderArtifactContent(content) {
|
|
|
2083
2083
|
artifactViewerKindEl.textContent = content.kind || item?.kind || "";
|
|
2084
2084
|
const streamedUrl = content.url || (content.id ? artifactRawUrl(content.id) : "");
|
|
2085
2085
|
const downloadUrl = content.downloadUrl || (content.id ? artifactRawUrl(content.id, { download: true }) : "");
|
|
2086
|
-
const renderUrl = content.dataUrl
|
|
2086
|
+
const renderUrl = streamedUrl || content.dataUrl;
|
|
2087
2087
|
|
|
2088
2088
|
if (renderUrl && (content.kind === "pdf" || content.mime === "application/pdf")) {
|
|
2089
2089
|
artifactViewerBodyEl.innerHTML = `
|
|
@@ -2133,15 +2133,19 @@ function artifactDownloadName(item, content) {
|
|
|
2133
2133
|
? ".pdf"
|
|
2134
2134
|
: content.mime?.startsWith("image/png")
|
|
2135
2135
|
? ".png"
|
|
2136
|
-
: content.mime?.startsWith("image/
|
|
2137
|
-
? ".
|
|
2138
|
-
: content.
|
|
2139
|
-
? ".
|
|
2140
|
-
: content.
|
|
2141
|
-
? ".
|
|
2142
|
-
: content.kind === "
|
|
2143
|
-
? ".
|
|
2144
|
-
:
|
|
2136
|
+
: content.mime?.startsWith("image/jpeg")
|
|
2137
|
+
? ".jpg"
|
|
2138
|
+
: content.mime?.startsWith("image/webp")
|
|
2139
|
+
? ".webp"
|
|
2140
|
+
: content.mime?.startsWith("image/svg")
|
|
2141
|
+
? ".svg"
|
|
2142
|
+
: content.kind === "json"
|
|
2143
|
+
? ".json"
|
|
2144
|
+
: content.kind === "diff"
|
|
2145
|
+
? ".diff"
|
|
2146
|
+
: content.kind === "markdown"
|
|
2147
|
+
? ".md"
|
|
2148
|
+
: ".txt";
|
|
2145
2149
|
const base = sourceName
|
|
2146
2150
|
.split("/")
|
|
2147
2151
|
.filter(Boolean)
|
|
@@ -101,4 +101,52 @@ else process.env.AGINTIFLOW_AUTO_UPDATE_STARTUP_INTERVAL_MS = previousStartupInt
|
|
|
101
101
|
if (previousCi === undefined) delete process.env.CI;
|
|
102
102
|
else process.env.CI = previousCi;
|
|
103
103
|
|
|
104
|
+
const updateHome = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-auto-update-install-"));
|
|
105
|
+
const previousInstallHome = process.env.AGINTIFLOW_HOME;
|
|
106
|
+
process.env.AGINTIFLOW_HOME = updateHome;
|
|
107
|
+
await fs.writeFile(
|
|
108
|
+
path.join(updateHome, "update-check.json"),
|
|
109
|
+
`${JSON.stringify({ checkedAt: Date.now(), latest: "0.20.99" })}\n`,
|
|
110
|
+
"utf8"
|
|
111
|
+
);
|
|
112
|
+
const hookEvents = [];
|
|
113
|
+
const fakeInstallWrites = [];
|
|
114
|
+
const fakeInstallStdout = {
|
|
115
|
+
isTTY: true,
|
|
116
|
+
write(value) {
|
|
117
|
+
fakeInstallWrites.push(String(value));
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
const fakeInstallStderr = {
|
|
121
|
+
write(value) {
|
|
122
|
+
fakeInstallWrites.push(String(value));
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
const installed = await maybeAutoUpdate({
|
|
126
|
+
argv: ["update"],
|
|
127
|
+
manual: true,
|
|
128
|
+
packageDir: scopedGlobalPath,
|
|
129
|
+
packageName: "@lazyingart/agintiflow",
|
|
130
|
+
packageVersion: "0.20.38",
|
|
131
|
+
restart: false,
|
|
132
|
+
stdout: fakeInstallStdout,
|
|
133
|
+
stderr: fakeInstallStderr,
|
|
134
|
+
installPackage: async (packageName) => {
|
|
135
|
+
hookEvents.push(["install", packageName]);
|
|
136
|
+
return { ok: true, code: 0 };
|
|
137
|
+
},
|
|
138
|
+
afterUpdate: async (context) => {
|
|
139
|
+
hookEvents.push(["afterUpdate", context.latest]);
|
|
140
|
+
return { ok: true, restarted: true, url: "http://127.0.0.1:3210" };
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
assert(installed.updated === true, "fake install did not report update success");
|
|
144
|
+
assert(installed.webappRestart?.ok === true && installed.webappRestart.restarted === true, "after-update webapp restart result missing");
|
|
145
|
+
assert(hookEvents.some((event) => event[0] === "install"), "install hook was not called");
|
|
146
|
+
assert(hookEvents.some((event) => event[0] === "afterUpdate"), "after-update hook was not called");
|
|
147
|
+
assert(fakeInstallWrites.join("").includes("webapp restarted after update"), "after-update webapp restart was not reported");
|
|
148
|
+
if (previousInstallHome === undefined) delete process.env.AGINTIFLOW_HOME;
|
|
149
|
+
else process.env.AGINTIFLOW_HOME = previousInstallHome;
|
|
150
|
+
await fs.rm(updateHome, { recursive: true, force: true });
|
|
151
|
+
|
|
104
152
|
console.log("auto-update smoke ok");
|
|
@@ -99,6 +99,42 @@ async function main() {
|
|
|
99
99
|
throw new Error("large artifact file resolver returned invalid metadata");
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
const largePdfPath = path.join(workspace, "compiled-paper.pdf");
|
|
103
|
+
await fs.writeFile(largePdfPath, Buffer.concat([Buffer.from("%PDF-1.7\n"), Buffer.alloc(4_200_000)]));
|
|
104
|
+
const pdfNormalized = normalizeCanvasPayload(
|
|
105
|
+
{
|
|
106
|
+
title: "Compiled paper",
|
|
107
|
+
kind: "pdf",
|
|
108
|
+
path: "compiled-paper.pdf",
|
|
109
|
+
selected: true,
|
|
110
|
+
},
|
|
111
|
+
config
|
|
112
|
+
);
|
|
113
|
+
if (!pdfNormalized.ok) throw new Error(pdfNormalized.reason || "large PDF canvas payload normalization failed");
|
|
114
|
+
const pdfPersisted = await persistCanvasPayloadFile(pdfNormalized.payload, { config, store });
|
|
115
|
+
if (!pdfPersisted.ok) throw new Error(pdfPersisted.reason || "large PDF canvas artifact persistence failed");
|
|
116
|
+
const { items: pdfItems } = buildArtifacts({
|
|
117
|
+
sessionId: store.sessionId,
|
|
118
|
+
events: [
|
|
119
|
+
{
|
|
120
|
+
timestamp: new Date().toISOString(),
|
|
121
|
+
type: "canvas.item",
|
|
122
|
+
data: {
|
|
123
|
+
...pdfPersisted.payload,
|
|
124
|
+
commandCwd: workspace,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
store,
|
|
129
|
+
});
|
|
130
|
+
const pdfContent = await readArtifactContent(pdfItems[0], { store, config });
|
|
131
|
+
if (!pdfContent.ok || pdfContent.kind !== "pdf" || pdfContent.mime !== "application/pdf") {
|
|
132
|
+
throw new Error(`large PDF artifact did not expose PDF metadata: ${JSON.stringify(pdfContent)}`);
|
|
133
|
+
}
|
|
134
|
+
if (!pdfContent.tooLargeForInline || !pdfContent.url || !pdfContent.downloadUrl || pdfContent.dataUrl) {
|
|
135
|
+
throw new Error("large PDF artifact should stream through preview/download URLs instead of inline data");
|
|
136
|
+
}
|
|
137
|
+
|
|
102
138
|
const missing = await persistCanvasPayloadFile({ ...normalized.payload, path: "missing.png" }, { config, store });
|
|
103
139
|
if (missing.ok) throw new Error("missing canvas path should fail persistence");
|
|
104
140
|
|
|
@@ -26,6 +26,7 @@ async function runCli(args, envOverrides = {}) {
|
|
|
26
26
|
...process.env,
|
|
27
27
|
AGINTIFLOW_RUNTIME_DIR: "",
|
|
28
28
|
AGINTIFLOW_HOME: agintiflowHome,
|
|
29
|
+
AGINTIFLOW_NO_WEB_AUTO_START: "1",
|
|
29
30
|
...envOverrides,
|
|
30
31
|
},
|
|
31
32
|
});
|
|
@@ -41,6 +42,7 @@ async function runCliIn(cwd, args, envOverrides = {}) {
|
|
|
41
42
|
...process.env,
|
|
42
43
|
AGINTIFLOW_RUNTIME_DIR: "",
|
|
43
44
|
AGINTIFLOW_HOME: agintiflowHome,
|
|
45
|
+
AGINTIFLOW_NO_WEB_AUTO_START: "1",
|
|
44
46
|
...envOverrides,
|
|
45
47
|
},
|
|
46
48
|
});
|
|
@@ -78,6 +78,7 @@ function runCli(args, inputText) {
|
|
|
78
78
|
...process.env,
|
|
79
79
|
AGINTIFLOW_RUNTIME_DIR: "",
|
|
80
80
|
AGINTIFLOW_HOME: agintiflowHome,
|
|
81
|
+
AGINTIFLOW_NO_WEB_AUTO_START: "1",
|
|
81
82
|
AGINTIFLOW_PREVIEW_TTL_MS: "1000",
|
|
82
83
|
AGINTI_LANGUAGE: "en",
|
|
83
84
|
},
|
|
@@ -146,6 +147,7 @@ async function runTmuxInterruptSmoke({ key, expected }) {
|
|
|
146
147
|
"env",
|
|
147
148
|
`AGINTIFLOW_HOME=${shellQuote(agintiflowHome)}`,
|
|
148
149
|
"AGINTIFLOW_RUNTIME_DIR=",
|
|
150
|
+
"AGINTIFLOW_NO_WEB_AUTO_START=1",
|
|
149
151
|
"AGINTI_LANGUAGE=en",
|
|
150
152
|
"AGINTI_INTERRUPT_FORCE_EXIT_MS=2500",
|
|
151
153
|
shellQuote(process.execPath),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
|
+
import net from "node:net";
|
|
3
4
|
import os from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -7,13 +8,26 @@ import { ensureAgintiWebApp } from "../src/web-autostart.js";
|
|
|
7
8
|
|
|
8
9
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
10
|
const runtimeDir = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-web-autostart-"));
|
|
11
|
+
const homeDir = path.join(runtimeDir, "stable-web-home");
|
|
12
|
+
const inheritedHome = path.join(runtimeDir, "leaked-cli-home");
|
|
10
13
|
const preferredPort = 44500 + Math.floor(Math.random() * 1000);
|
|
11
14
|
let childPid = 0;
|
|
15
|
+
const originalHome = process.env.AGINTIFLOW_HOME;
|
|
16
|
+
|
|
17
|
+
function listenOccupier(port) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const server = net.createServer();
|
|
20
|
+
server.once("error", reject);
|
|
21
|
+
server.listen(port, "127.0.0.1", () => resolve(server));
|
|
22
|
+
});
|
|
23
|
+
}
|
|
12
24
|
|
|
13
25
|
try {
|
|
26
|
+
process.env.AGINTIFLOW_HOME = inheritedHome;
|
|
14
27
|
const first = await ensureAgintiWebApp({
|
|
15
28
|
packageDir: repoRoot,
|
|
16
29
|
cwd: runtimeDir,
|
|
30
|
+
home: homeDir,
|
|
17
31
|
preferredPort,
|
|
18
32
|
host: "127.0.0.1",
|
|
19
33
|
});
|
|
@@ -25,17 +39,71 @@ try {
|
|
|
25
39
|
if (!health.ok || Number(health.port) !== preferredPort) {
|
|
26
40
|
throw new Error(`auto-started web health was invalid: ${JSON.stringify(health)}`);
|
|
27
41
|
}
|
|
42
|
+
if (path.resolve(health.agintiflowHome) !== path.resolve(homeDir) || path.resolve(health.runtimeDir) !== path.resolve(runtimeDir)) {
|
|
43
|
+
throw new Error(`auto-started webapp inherited the wrong context: ${JSON.stringify(health)}`);
|
|
44
|
+
}
|
|
28
45
|
const second = await ensureAgintiWebApp({
|
|
29
46
|
packageDir: repoRoot,
|
|
30
47
|
cwd: runtimeDir,
|
|
48
|
+
home: homeDir,
|
|
31
49
|
preferredPort,
|
|
32
50
|
host: "127.0.0.1",
|
|
33
51
|
});
|
|
34
52
|
if (!second.ok || !second.reused || second.url !== first.url) {
|
|
35
53
|
throw new Error(`expected second auto-start call to reuse existing webapp, got ${JSON.stringify(second)}`);
|
|
36
54
|
}
|
|
55
|
+
const restarted = await ensureAgintiWebApp({
|
|
56
|
+
packageDir: repoRoot,
|
|
57
|
+
cwd: runtimeDir,
|
|
58
|
+
home: homeDir,
|
|
59
|
+
preferredPort,
|
|
60
|
+
host: "127.0.0.1",
|
|
61
|
+
restart: true,
|
|
62
|
+
});
|
|
63
|
+
if (!restarted.ok || !restarted.restarted || restarted.url !== first.url || Number(restarted.pid) === childPid) {
|
|
64
|
+
throw new Error(`expected restart on same URL with a new pid, got ${JSON.stringify(restarted)}`);
|
|
65
|
+
}
|
|
66
|
+
childPid = Number(restarted.pid) || childPid;
|
|
67
|
+
try {
|
|
68
|
+
process.kill(childPid, "SIGTERM");
|
|
69
|
+
} catch {
|
|
70
|
+
// The primary restart target may have already exited.
|
|
71
|
+
}
|
|
72
|
+
childPid = 0;
|
|
73
|
+
|
|
74
|
+
const occupiedPort = preferredPort + 103;
|
|
75
|
+
const occupier = await listenOccupier(occupiedPort);
|
|
76
|
+
try {
|
|
77
|
+
const fallback = await ensureAgintiWebApp({
|
|
78
|
+
packageDir: repoRoot,
|
|
79
|
+
cwd: runtimeDir,
|
|
80
|
+
home: homeDir,
|
|
81
|
+
preferredPort: occupiedPort,
|
|
82
|
+
host: "127.0.0.1",
|
|
83
|
+
});
|
|
84
|
+
if (!fallback.ok || !fallback.started || Number(fallback.port) <= occupiedPort) {
|
|
85
|
+
throw new Error(`expected fallback webapp above occupied port ${occupiedPort}, got ${JSON.stringify(fallback)}`);
|
|
86
|
+
}
|
|
87
|
+
const fallbackPid = Number(fallback.pid) || 0;
|
|
88
|
+
const fallbackRestarted = await ensureAgintiWebApp({
|
|
89
|
+
packageDir: repoRoot,
|
|
90
|
+
cwd: runtimeDir,
|
|
91
|
+
home: homeDir,
|
|
92
|
+
preferredPort: occupiedPort,
|
|
93
|
+
host: "127.0.0.1",
|
|
94
|
+
restart: true,
|
|
95
|
+
});
|
|
96
|
+
if (!fallbackRestarted.ok || !fallbackRestarted.restarted || fallbackRestarted.url !== fallback.url || Number(fallbackRestarted.pid) === fallbackPid) {
|
|
97
|
+
throw new Error(`expected restart of compatible fallback-port webapp, got ${JSON.stringify(fallbackRestarted)}`);
|
|
98
|
+
}
|
|
99
|
+
childPid = Number(fallbackRestarted.pid) || 0;
|
|
100
|
+
} finally {
|
|
101
|
+
occupier.close();
|
|
102
|
+
}
|
|
37
103
|
console.log(`web auto-start smoke passed: ${first.url}`);
|
|
38
104
|
} finally {
|
|
105
|
+
if (originalHome === undefined) delete process.env.AGINTIFLOW_HOME;
|
|
106
|
+
else process.env.AGINTIFLOW_HOME = originalHome;
|
|
39
107
|
if (childPid) {
|
|
40
108
|
try {
|
|
41
109
|
process.kill(childPid, "SIGTERM");
|
|
@@ -47,7 +47,8 @@ async function runCase({ port, env = {}, expectHeader, label }) {
|
|
|
47
47
|
env: {
|
|
48
48
|
...process.env,
|
|
49
49
|
AGINTIFLOW_NO_ANIMATION: "1",
|
|
50
|
-
AGINTIFLOW_HOME: path.join(runtimeDir, `.
|
|
50
|
+
AGINTIFLOW_HOME: path.join(runtimeDir, `.ignored-cli-home-${label}`),
|
|
51
|
+
AGINTIFLOW_WEB_HOME: path.join(runtimeDir, `.agintiflow-web-home-${label}`),
|
|
51
52
|
...env,
|
|
52
53
|
},
|
|
53
54
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -64,10 +65,15 @@ async function runCase({ port, env = {}, expectHeader, label }) {
|
|
|
64
65
|
await waitFor(() => output.stdout.includes(expectHeader), child, `${label} launch header`, output);
|
|
65
66
|
child.stdin.write(`/webapp ${port}\n`);
|
|
66
67
|
await waitFor(() => output.stdout.includes(`webapp=http://127.0.0.1:${port}`), child, `${label} /webapp command`, output);
|
|
68
|
+
child.stdin.write(`/webapp restart ${port}\n`);
|
|
69
|
+
await waitFor(() => output.stdout.includes(`webapp=http://127.0.0.1:${port} restarted`), child, `${label} /webapp restart command`, output);
|
|
67
70
|
const health = await fetch(`http://127.0.0.1:${port}/health`).then((response) => response.json());
|
|
68
71
|
if (!health.ok || health.app !== "agintiflow" || Number(health.port) !== port) {
|
|
69
72
|
throw new Error(`invalid /webapp health response for ${label}: ${JSON.stringify(health)}`);
|
|
70
73
|
}
|
|
74
|
+
if (path.resolve(health.agintiflowHome) !== path.resolve(path.join(runtimeDir, `.agintiflow-web-home-${label}`))) {
|
|
75
|
+
throw new Error(`webapp command inherited the wrong home for ${label}: ${JSON.stringify(health)}`);
|
|
76
|
+
}
|
|
71
77
|
} finally {
|
|
72
78
|
child.kill("SIGTERM");
|
|
73
79
|
await killPort(port);
|
package/src/auto-update.js
CHANGED
|
@@ -189,6 +189,24 @@ async function restartCurrentProcess() {
|
|
|
189
189
|
});
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
async function runAfterUpdateHook(afterUpdate, context, stdout, stderr) {
|
|
193
|
+
if (typeof afterUpdate !== "function") return null;
|
|
194
|
+
try {
|
|
195
|
+
const result = (await afterUpdate(context)) || { ok: true };
|
|
196
|
+
if (result.ok && result.url) {
|
|
197
|
+
const state = result.restarted ? "restarted" : result.reused ? "reused" : result.started ? "started" : "ready";
|
|
198
|
+
stdout.write(`AgInTiFlow webapp ${state} after update: ${result.url}\n`);
|
|
199
|
+
} else if (result.ok === false) {
|
|
200
|
+
stderr.write(`AgInTiFlow webapp restart after update failed: ${result.error || "unknown error"}\n`);
|
|
201
|
+
}
|
|
202
|
+
return result;
|
|
203
|
+
} catch (error) {
|
|
204
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
205
|
+
stderr.write(`AgInTiFlow webapp restart after update failed: ${message}\n`);
|
|
206
|
+
return { ok: false, error: message };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
192
210
|
function shouldSkipFailedInstall(cache, currentMs, force) {
|
|
193
211
|
if (force) return false;
|
|
194
212
|
const failedAt = Number(cache.lastInstallFailedAt || 0);
|
|
@@ -289,6 +307,9 @@ export async function maybeAutoUpdate({
|
|
|
289
307
|
packageVersion = "",
|
|
290
308
|
restart = false,
|
|
291
309
|
selectUpdateAction = promptUpdateChoice,
|
|
310
|
+
installPackage = installLatest,
|
|
311
|
+
restartProcess = restartCurrentProcess,
|
|
312
|
+
afterUpdate = null,
|
|
292
313
|
stdout = process.stdout,
|
|
293
314
|
stderr = process.stderr,
|
|
294
315
|
} = {}) {
|
|
@@ -369,7 +390,7 @@ export async function maybeAutoUpdate({
|
|
|
369
390
|
|
|
370
391
|
stdout.write(`AgInTiFlow update available: ${packageVersion} -> ${latest}\n`);
|
|
371
392
|
stdout.write(`Running: npm install -g ${packageName}@latest\n`);
|
|
372
|
-
const install = await
|
|
393
|
+
const install = await installPackage(packageName);
|
|
373
394
|
if (!install.ok) {
|
|
374
395
|
await writeCache({
|
|
375
396
|
...cache,
|
|
@@ -394,12 +415,27 @@ export async function maybeAutoUpdate({
|
|
|
394
415
|
packageName,
|
|
395
416
|
});
|
|
396
417
|
stdout.write(`AgInTiFlow updated to ${latest}.\n`);
|
|
418
|
+
const webappRestart = await runAfterUpdateHook(
|
|
419
|
+
afterUpdate,
|
|
420
|
+
{ current: packageVersion, latest, packageName },
|
|
421
|
+
stdout,
|
|
422
|
+
stderr
|
|
423
|
+
);
|
|
397
424
|
|
|
398
425
|
if (restart) {
|
|
399
426
|
stdout.write("Restarting AgInTiFlow with the updated package...\n");
|
|
400
|
-
const restarted = await
|
|
401
|
-
return {
|
|
427
|
+
const restarted = await restartProcess();
|
|
428
|
+
return {
|
|
429
|
+
checked: true,
|
|
430
|
+
latest,
|
|
431
|
+
current: packageVersion,
|
|
432
|
+
updated: true,
|
|
433
|
+
restarted: true,
|
|
434
|
+
exitCode: restarted.exitCode,
|
|
435
|
+
error: restarted.error,
|
|
436
|
+
webappRestart,
|
|
437
|
+
};
|
|
402
438
|
}
|
|
403
439
|
|
|
404
|
-
return { checked: true, latest, current: packageVersion, updated: true };
|
|
440
|
+
return { checked: true, latest, current: packageVersion, updated: true, webappRestart };
|
|
405
441
|
}
|
package/src/cli.js
CHANGED
|
@@ -95,11 +95,12 @@ function printUnknownCliOptions(options = []) {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
async function maybeEnsureDefaultWebApp(args = {}, { commandCwd = process.cwd() } = {}) {
|
|
98
|
-
if (args.web) return { ok: false, url: "" };
|
|
98
|
+
if (args.web || args.webapp) return { ok: false, url: "" };
|
|
99
99
|
try {
|
|
100
100
|
return await ensureAgintiWebApp({
|
|
101
101
|
packageDir,
|
|
102
102
|
cwd: args.commandCwd || commandCwd || process.cwd(),
|
|
103
|
+
home: args.webHome || "",
|
|
103
104
|
host: args.host || process.env.AGINTI_WEB_HOST || "127.0.0.1",
|
|
104
105
|
preferredPort: args.port || process.env.AGINTI_WEB_PORT || 3210,
|
|
105
106
|
language: args.language ? resolveLanguage(args.language) : "",
|
|
@@ -379,6 +380,9 @@ export function parseArgs(argv) {
|
|
|
379
380
|
sandboxStatus: false,
|
|
380
381
|
sandboxPreflight: false,
|
|
381
382
|
web: false,
|
|
383
|
+
webapp: false,
|
|
384
|
+
webAction: "",
|
|
385
|
+
webHome: "",
|
|
382
386
|
interactive: false,
|
|
383
387
|
port: "",
|
|
384
388
|
host: "",
|
|
@@ -399,6 +403,21 @@ export function parseArgs(argv) {
|
|
|
399
403
|
parts.push(...argv.slice(i + 1));
|
|
400
404
|
break;
|
|
401
405
|
}
|
|
406
|
+
if ((arg === "web" || arg === "--web") && String(argv[i + 1] || "").toLowerCase() === "restart") {
|
|
407
|
+
result.webapp = true;
|
|
408
|
+
result.webAction = "restart";
|
|
409
|
+
i += 1;
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (arg === "webapp" || arg === "--webapp" || arg === "web-ui" || arg === "--web-ui") {
|
|
413
|
+
result.webapp = true;
|
|
414
|
+
const next = String(argv[i + 1] || "");
|
|
415
|
+
if (next && !next.startsWith("--")) {
|
|
416
|
+
result.webAction = next.toLowerCase();
|
|
417
|
+
i += 1;
|
|
418
|
+
}
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
402
421
|
if (arg === "web" || arg === "--web") {
|
|
403
422
|
result.web = true;
|
|
404
423
|
continue;
|
|
@@ -417,6 +436,11 @@ export function parseArgs(argv) {
|
|
|
417
436
|
i += 1;
|
|
418
437
|
continue;
|
|
419
438
|
}
|
|
439
|
+
if (arg === "--web-home") {
|
|
440
|
+
result.webHome = readOption(argv, i);
|
|
441
|
+
i += 1;
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
420
444
|
if (arg === "--language" || arg === "--lang" || arg === "-L") {
|
|
421
445
|
const first = readOption(argv, i);
|
|
422
446
|
const second = argv[i + 2] && !String(argv[i + 2]).startsWith("--") ? argv[i + 2] : "";
|
|
@@ -845,6 +869,43 @@ function stripLeadingGlobalOptions(argv = []) {
|
|
|
845
869
|
};
|
|
846
870
|
}
|
|
847
871
|
|
|
872
|
+
function commandCwdFromArgv(argv = []) {
|
|
873
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
874
|
+
if (argv[index] === "--cwd") {
|
|
875
|
+
const cwd = readOption(argv, index);
|
|
876
|
+
if (cwd) return path.resolve(cwd);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return process.cwd();
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function languageFromArgv(argv = []) {
|
|
883
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
884
|
+
const arg = argv[index];
|
|
885
|
+
if (arg === "--language" || arg === "--lang" || arg === "-L") {
|
|
886
|
+
const first = readOption(argv, index);
|
|
887
|
+
const second = argv[index + 2] && !String(argv[index + 2]).startsWith("--") ? argv[index + 2] : "";
|
|
888
|
+
if (["cn", "zh"].includes(String(first || "").toLowerCase()) && ["s", "t"].includes(String(second || "").toLowerCase())) {
|
|
889
|
+
return resolveLanguage(`${first}-${second}`);
|
|
890
|
+
}
|
|
891
|
+
return resolveLanguage(first);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
return "";
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
async function restartWebAppAfterUpdate({ commandCwd = process.cwd(), language = "" } = {}) {
|
|
898
|
+
return await ensureAgintiWebApp({
|
|
899
|
+
packageDir,
|
|
900
|
+
cwd: commandCwd,
|
|
901
|
+
host: process.env.AGINTI_WEB_HOST || process.env.HOST || "127.0.0.1",
|
|
902
|
+
preferredPort: process.env.AGINTI_WEB_PORT || process.env.PORT || 3210,
|
|
903
|
+
language,
|
|
904
|
+
restart: true,
|
|
905
|
+
respectAutoStartDisable: false,
|
|
906
|
+
}).catch((error) => ({ ok: false, error: error instanceof Error ? error.message : String(error), url: "" }));
|
|
907
|
+
}
|
|
908
|
+
|
|
848
909
|
function providerLabel(provider) {
|
|
849
910
|
const normalized = String(provider || "").toLowerCase();
|
|
850
911
|
if (normalized === "openai") return "OpenAI";
|
|
@@ -1663,6 +1724,8 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1663
1724
|
}
|
|
1664
1725
|
|
|
1665
1726
|
if (argv[0] === "update" || argv[0] === "upgrade") {
|
|
1727
|
+
const updateCommandCwd = commandCwdFromArgv(argv);
|
|
1728
|
+
const updateLanguage = languageFromArgv(argv);
|
|
1666
1729
|
const updateResult = await maybeAutoUpdate({
|
|
1667
1730
|
argv,
|
|
1668
1731
|
force: true,
|
|
@@ -1671,11 +1734,14 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1671
1734
|
packageName: packageJson.name,
|
|
1672
1735
|
packageVersion: packageJson.version,
|
|
1673
1736
|
restart: false,
|
|
1737
|
+
afterUpdate: () => restartWebAppAfterUpdate({ commandCwd: updateCommandCwd, language: updateLanguage }),
|
|
1674
1738
|
});
|
|
1675
1739
|
if (updateResult.error) process.exit(1);
|
|
1676
1740
|
return;
|
|
1677
1741
|
}
|
|
1678
1742
|
|
|
1743
|
+
const updateCommandCwd = commandCwdFromArgv(argv);
|
|
1744
|
+
const updateLanguage = languageFromArgv(argv);
|
|
1679
1745
|
const autoUpdateResult = await maybeAutoUpdate({
|
|
1680
1746
|
argv,
|
|
1681
1747
|
force: argv.includes("--auto-update"),
|
|
@@ -1683,6 +1749,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1683
1749
|
packageName: packageJson.name,
|
|
1684
1750
|
packageVersion: packageJson.version,
|
|
1685
1751
|
restart: true,
|
|
1752
|
+
afterUpdate: () => restartWebAppAfterUpdate({ commandCwd: updateCommandCwd, language: updateLanguage }),
|
|
1686
1753
|
});
|
|
1687
1754
|
if (autoUpdateResult.restarted) process.exit(autoUpdateResult.exitCode ?? 0);
|
|
1688
1755
|
|
|
@@ -1880,6 +1947,33 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1880
1947
|
const args = { ...parsedArgs, commandCwd: parsedArgs.commandCwd || commandCwd };
|
|
1881
1948
|
exitOnUnknownOptions(args);
|
|
1882
1949
|
|
|
1950
|
+
if (args.webapp) {
|
|
1951
|
+
const action = String(args.webAction || "start").toLowerCase();
|
|
1952
|
+
if (!["start", "restart", "reuse"].includes(action)) {
|
|
1953
|
+
console.error("Usage: aginti webapp [start|restart] [--port 3210] [--host 127.0.0.1]");
|
|
1954
|
+
process.exit(1);
|
|
1955
|
+
}
|
|
1956
|
+
const result = await ensureAgintiWebApp({
|
|
1957
|
+
packageDir,
|
|
1958
|
+
cwd: args.commandCwd || commandCwd,
|
|
1959
|
+
home: args.webHome || "",
|
|
1960
|
+
host: args.host || process.env.AGINTI_WEB_HOST || "127.0.0.1",
|
|
1961
|
+
preferredPort: args.port || process.env.AGINTI_WEB_PORT || 3210,
|
|
1962
|
+
language: args.language ? resolveLanguage(args.language) : "",
|
|
1963
|
+
restart: action === "restart",
|
|
1964
|
+
respectAutoStartDisable: false,
|
|
1965
|
+
}).catch((error) => ({ ok: false, error: error instanceof Error ? error.message : String(error), url: "" }));
|
|
1966
|
+
if (!result.ok) {
|
|
1967
|
+
console.error(`webapp unavailable: ${result.error || "unknown"}`);
|
|
1968
|
+
process.exit(1);
|
|
1969
|
+
}
|
|
1970
|
+
const state = result.restarted ? "restarted" : result.reused ? "reused" : "started";
|
|
1971
|
+
console.log(`webapp: ${result.url} ${state}`);
|
|
1972
|
+
console.log(`project: ${result.runtimeDir || path.resolve(args.commandCwd || commandCwd)}`);
|
|
1973
|
+
console.log(`home: ${result.agintiflowHome || ""}`);
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1883
1977
|
if (args.web) {
|
|
1884
1978
|
if (args.port) process.env.PORT = String(args.port);
|
|
1885
1979
|
if (args.host) process.env.HOST = String(args.host);
|
package/src/i18n.js
CHANGED
|
@@ -139,7 +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
|
|
142
|
+
helpWebapp: "Start, reuse, or restart the local webapp and print its URL.",
|
|
143
143
|
helpWebSearch: "Enable or disable the web_search tool.",
|
|
144
144
|
helpEnableScs: "Toggle Student-Committee-Supervisor gated execution.",
|
|
145
145
|
helpScouts: "Enable parallel DeepSeek scouts and set scout count.",
|
package/src/interactive-cli.js
CHANGED
|
@@ -881,7 +881,7 @@ function printHelp() {
|
|
|
881
881
|
` ${command("/skills [query]", "List Markdown skills selected for a topic.", "helpSkills")}`,
|
|
882
882
|
` ${command("/skillmesh [status|off|record|share|sync]", "Manage strict reviewed skill sharing.", "helpSkillMesh")}`,
|
|
883
883
|
` ${command("/profile <name>", "Set task profile, e.g. code, website, latex, maintenance.", "helpProfile")}`,
|
|
884
|
-
` ${command("/webapp [port]", "Start or
|
|
884
|
+
` ${command("/webapp [port|restart]", "Start, reuse, or restart the local webapp and print its URL.", "helpWebapp")}`,
|
|
885
885
|
` ${command("/web-search on|off", "Enable or disable the web_search tool.", "helpWebSearch")}`,
|
|
886
886
|
` ${command("/web-research <query>", "Run a sourced web_research turn with persisted evidence.", "helpWebSearch")}`,
|
|
887
887
|
` ${command("/image-read <path> [question]", "Run read_image on a workspace screenshot/image.", "helpWebSearch")}`,
|
|
@@ -3010,20 +3010,24 @@ async function handleCommand(line, state, packageDir) {
|
|
|
3010
3010
|
return true;
|
|
3011
3011
|
}
|
|
3012
3012
|
if (command === "webapp" || command === "web") {
|
|
3013
|
-
const
|
|
3014
|
-
|
|
3013
|
+
const words = value.split(/\s+/).filter(Boolean);
|
|
3014
|
+
const restart = words.some((word) => word.toLowerCase() === "restart");
|
|
3015
|
+
const portValue = words.find((word) => /^\d+$/.test(word));
|
|
3016
|
+
const port = Number(portValue) || Number(process.env.AGINTI_WEB_PORT || process.env.PORT || 3210);
|
|
3017
|
+
printSystemLine(restart ? "webapp=restarting" : "webapp=starting");
|
|
3015
3018
|
const result = await ensureAgintiWebApp({
|
|
3016
3019
|
packageDir,
|
|
3017
3020
|
cwd: state.commandCwd || process.cwd(),
|
|
3018
3021
|
host: process.env.AGINTI_WEB_HOST || process.env.HOST || "127.0.0.1",
|
|
3019
3022
|
preferredPort: port,
|
|
3020
3023
|
language: state.language,
|
|
3024
|
+
restart,
|
|
3021
3025
|
respectAutoStartDisable: false,
|
|
3022
3026
|
}).catch((error) => ({ ok: false, error: error instanceof Error ? error.message : String(error), url: "" }));
|
|
3023
3027
|
if (result.ok) {
|
|
3024
3028
|
state.webAppUrl = result.url;
|
|
3025
3029
|
state.webAppNotice = "";
|
|
3026
|
-
printSystemLine(`webapp=${result.url} ${result.reused ? "reused" : "started"}`);
|
|
3030
|
+
printSystemLine(`webapp=${result.url} ${result.restarted ? "restarted" : result.reused ? "reused" : "started"}`);
|
|
3027
3031
|
} else {
|
|
3028
3032
|
state.webAppUrl = "";
|
|
3029
3033
|
state.webAppNotice = `webapp unavailable - use /webapp to retry; error: ${compactLine(result.error || "unknown", 72)}`;
|
package/src/web-autostart.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
2
|
import http from "node:http";
|
|
3
3
|
import net from "node:net";
|
|
4
|
+
import os from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
|
|
6
7
|
const DEFAULT_HOST = "127.0.0.1";
|
|
@@ -20,7 +21,40 @@ function webUrl(host, port) {
|
|
|
20
21
|
return `http://${host}:${port}`;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
function
|
|
24
|
+
function isInside(root, target) {
|
|
25
|
+
const relative = path.relative(root, target);
|
|
26
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function samePath(left = "", right = "") {
|
|
30
|
+
if (!left || !right) return false;
|
|
31
|
+
return path.resolve(left) === path.resolve(right);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function defaultAgintiflowHome() {
|
|
35
|
+
return path.join(os.homedir(), ".agintiflow");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isLikelyTransientPath(value = "") {
|
|
39
|
+
if (!value) return false;
|
|
40
|
+
const resolved = path.resolve(value);
|
|
41
|
+
const tmp = path.resolve(os.tmpdir());
|
|
42
|
+
return isInside(tmp, resolved) || /agintiflow-(cli-chat|webapp-command|web-autostart|web-port|smoke|test)-/i.test(resolved);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resolveWebHome(home = "") {
|
|
46
|
+
const explicit = home || process.env.AGINTIFLOW_WEB_HOME || process.env.AGINTI_WEB_HOME || "";
|
|
47
|
+
if (explicit) return path.resolve(explicit);
|
|
48
|
+
const inherited = process.env.AGINTIFLOW_HOME || "";
|
|
49
|
+
if (inherited && !isLikelyTransientPath(inherited)) return path.resolve(inherited);
|
|
50
|
+
return defaultAgintiflowHome();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveRuntimeDir(cwd = "") {
|
|
54
|
+
return path.resolve(process.env.AGINTIFLOW_WEB_RUNTIME_DIR || cwd || process.cwd());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function fetchHealthDetails(host, port, timeoutMs = 450) {
|
|
24
58
|
return new Promise((resolve) => {
|
|
25
59
|
const req = http.get(`${webUrl(host, port)}/health`, { timeout: timeoutMs }, (res) => {
|
|
26
60
|
let body = "";
|
|
@@ -31,20 +65,28 @@ function fetchHealth(host, port, timeoutMs = 450) {
|
|
|
31
65
|
res.on("end", () => {
|
|
32
66
|
try {
|
|
33
67
|
const json = JSON.parse(body || "{}");
|
|
34
|
-
resolve(
|
|
68
|
+
resolve({
|
|
69
|
+
ok: Boolean(res.statusCode === 200 && json.ok && (json.app === "agintiflow" || Number(json.port) === port)),
|
|
70
|
+
statusCode: res.statusCode,
|
|
71
|
+
...json,
|
|
72
|
+
});
|
|
35
73
|
} catch {
|
|
36
|
-
resolve(false);
|
|
74
|
+
resolve({ ok: false });
|
|
37
75
|
}
|
|
38
76
|
});
|
|
39
77
|
});
|
|
40
78
|
req.on("timeout", () => {
|
|
41
79
|
req.destroy();
|
|
42
|
-
resolve(false);
|
|
80
|
+
resolve({ ok: false });
|
|
43
81
|
});
|
|
44
|
-
req.on("error", () => resolve(false));
|
|
82
|
+
req.on("error", () => resolve({ ok: false }));
|
|
45
83
|
});
|
|
46
84
|
}
|
|
47
85
|
|
|
86
|
+
async function fetchHealth(host, port, timeoutMs = 450) {
|
|
87
|
+
return (await fetchHealthDetails(host, port, timeoutMs)).ok;
|
|
88
|
+
}
|
|
89
|
+
|
|
48
90
|
function canListen(host, port) {
|
|
49
91
|
return new Promise((resolve) => {
|
|
50
92
|
const server = net.createServer();
|
|
@@ -64,14 +106,105 @@ async function waitForHealth(host, port, timeoutMs = 7000) {
|
|
|
64
106
|
return false;
|
|
65
107
|
}
|
|
66
108
|
|
|
67
|
-
|
|
109
|
+
function compatibleHealth(health = {}, { cwd = "", home = "", packageDir = "" } = {}) {
|
|
110
|
+
if (!health.ok || health.app !== "agintiflow") return false;
|
|
111
|
+
if (!health.runtimeDir || !health.agintiflowHome) return false;
|
|
112
|
+
if (!samePath(health.runtimeDir, cwd)) return false;
|
|
113
|
+
if (!samePath(health.agintiflowHome, home)) return false;
|
|
114
|
+
if (health.packageDir && packageDir && !samePath(health.packageDir, packageDir)) return false;
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function listenerPids(port) {
|
|
119
|
+
return new Promise((resolve) => {
|
|
120
|
+
execFile("lsof", [`-tiTCP:${port}`, "-sTCP:LISTEN"], { encoding: "utf8" }, (error, stdout) => {
|
|
121
|
+
if (error) {
|
|
122
|
+
resolve([]);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
resolve(
|
|
126
|
+
String(stdout || "")
|
|
127
|
+
.split(/\s+/)
|
|
128
|
+
.map((value) => Number(value))
|
|
129
|
+
.filter((pid) => Number.isInteger(pid) && pid > 0 && pid !== process.pid)
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function waitForPortRelease(host, port, timeoutMs = 5000) {
|
|
136
|
+
const deadline = Date.now() + timeoutMs;
|
|
137
|
+
while (Date.now() < deadline) {
|
|
138
|
+
if (!(await fetchHealth(host, port, 220)) && (await canListen(host, port))) return true;
|
|
139
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
140
|
+
}
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function stopWebAppOnPort({ host, port, health = {} } = {}) {
|
|
145
|
+
const pids = new Set();
|
|
146
|
+
if (Number.isInteger(Number(health.pid)) && Number(health.pid) > 0) pids.add(Number(health.pid));
|
|
147
|
+
for (const pid of await listenerPids(port)) pids.add(pid);
|
|
148
|
+
if (pids.size === 0) return { ok: false, error: `Could not identify AgInTiFlow webapp process on ${host}:${port}.` };
|
|
149
|
+
|
|
150
|
+
for (const pid of pids) {
|
|
151
|
+
try {
|
|
152
|
+
process.kill(pid, "SIGTERM");
|
|
153
|
+
} catch {
|
|
154
|
+
// Already stopped or not owned by this user.
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (await waitForPortRelease(host, port, 3500)) return { ok: true, pids: [...pids], forced: false };
|
|
158
|
+
|
|
159
|
+
for (const pid of pids) {
|
|
160
|
+
try {
|
|
161
|
+
process.kill(pid, "SIGKILL");
|
|
162
|
+
} catch {
|
|
163
|
+
// Ignore.
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const released = await waitForPortRelease(host, port, 2000);
|
|
167
|
+
return released
|
|
168
|
+
? { ok: true, pids: [...pids], forced: true }
|
|
169
|
+
: { ok: false, pids: [...pids], error: `AgInTiFlow webapp on ${host}:${port} did not stop.` };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function findReusableOrFreeWebPort({
|
|
173
|
+
host = DEFAULT_HOST,
|
|
174
|
+
preferredPort = DEFAULT_PORT,
|
|
175
|
+
attempts = MAX_PORT_ATTEMPTS,
|
|
176
|
+
cwd = process.cwd(),
|
|
177
|
+
home = "",
|
|
178
|
+
packageDir = process.cwd(),
|
|
179
|
+
restart = false,
|
|
180
|
+
} = {}) {
|
|
68
181
|
const startPort = normalizePort(preferredPort);
|
|
69
182
|
const normalizedHost = normalizeHost(host);
|
|
183
|
+
const runtimeDir = resolveRuntimeDir(cwd);
|
|
184
|
+
const homeDir = resolveWebHome(home);
|
|
70
185
|
for (let offset = 0; offset < attempts; offset += 1) {
|
|
71
186
|
const port = startPort + offset;
|
|
72
187
|
if (port >= 65536) break;
|
|
73
|
-
|
|
74
|
-
|
|
188
|
+
const health = await fetchHealthDetails(normalizedHost, port);
|
|
189
|
+
if (health.ok) {
|
|
190
|
+
const compatible = compatibleHealth(health, { cwd: runtimeDir, home: homeDir, packageDir });
|
|
191
|
+
if (restart && compatible) {
|
|
192
|
+
const stopped = await stopWebAppOnPort({ host: normalizedHost, port, health });
|
|
193
|
+
if (!stopped.ok) return { port, host: normalizedHost, url: "", reused: false, available: false, stopped, error: stopped.error };
|
|
194
|
+
return {
|
|
195
|
+
port,
|
|
196
|
+
host: normalizedHost,
|
|
197
|
+
url: webUrl(normalizedHost, port),
|
|
198
|
+
reused: false,
|
|
199
|
+
available: true,
|
|
200
|
+
restarted: true,
|
|
201
|
+
stopped,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (compatible) {
|
|
205
|
+
return { port, host: normalizedHost, url: webUrl(normalizedHost, port), reused: true, available: false, health };
|
|
206
|
+
}
|
|
207
|
+
continue;
|
|
75
208
|
}
|
|
76
209
|
if (await canListen(normalizedHost, port)) {
|
|
77
210
|
return { port, host: normalizedHost, url: webUrl(normalizedHost, port), reused: false, available: true };
|
|
@@ -83,32 +216,40 @@ export async function findReusableOrFreeWebPort({ host = DEFAULT_HOST, preferred
|
|
|
83
216
|
export async function ensureAgintiWebApp({
|
|
84
217
|
packageDir = process.cwd(),
|
|
85
218
|
cwd = process.cwd(),
|
|
219
|
+
home = "",
|
|
86
220
|
host = DEFAULT_HOST,
|
|
87
221
|
preferredPort = DEFAULT_PORT,
|
|
88
222
|
language = "",
|
|
223
|
+
restart = false,
|
|
89
224
|
respectAutoStartDisable = true,
|
|
90
225
|
} = {}) {
|
|
91
226
|
if (respectAutoStartDisable && (process.env.AGINTI_NO_WEB_AUTO_START === "1" || process.env.AGINTIFLOW_NO_WEB_AUTO_START === "1")) {
|
|
92
227
|
return { ok: false, disabled: true, url: "" };
|
|
93
228
|
}
|
|
94
229
|
|
|
95
|
-
const
|
|
230
|
+
const runtimeDir = resolveRuntimeDir(cwd);
|
|
231
|
+
const homeDir = resolveWebHome(home);
|
|
232
|
+
const candidate = await findReusableOrFreeWebPort({ host, preferredPort, cwd: runtimeDir, home: homeDir, packageDir, restart });
|
|
96
233
|
if (!candidate.port) {
|
|
97
|
-
return { ok: false, error: `No available AgInTiFlow web port from ${normalizePort(preferredPort)}.`, url: "" };
|
|
234
|
+
return { ok: false, error: candidate.error || `No available AgInTiFlow web port from ${normalizePort(preferredPort)}.`, url: "" };
|
|
235
|
+
}
|
|
236
|
+
if (candidate.error || (!candidate.available && !candidate.reused)) {
|
|
237
|
+
return { ok: false, error: candidate.error || `No reusable or free AgInTiFlow web port from ${normalizePort(preferredPort)}.`, url: "" };
|
|
98
238
|
}
|
|
99
239
|
if (candidate.reused) {
|
|
100
|
-
return { ok: true, reused: true, started: false, ...candidate };
|
|
240
|
+
return { ok: true, reused: true, started: false, runtimeDir, agintiflowHome: homeDir, ...candidate };
|
|
101
241
|
}
|
|
102
242
|
|
|
103
243
|
const child = spawn(process.execPath, [path.join(packageDir, "web.js")], {
|
|
104
|
-
cwd,
|
|
244
|
+
cwd: runtimeDir,
|
|
105
245
|
detached: true,
|
|
106
246
|
stdio: "ignore",
|
|
107
247
|
env: {
|
|
108
248
|
...process.env,
|
|
109
249
|
HOST: candidate.host,
|
|
110
250
|
PORT: String(candidate.port),
|
|
111
|
-
AGINTIFLOW_RUNTIME_DIR:
|
|
251
|
+
AGINTIFLOW_RUNTIME_DIR: runtimeDir,
|
|
252
|
+
AGINTIFLOW_HOME: homeDir,
|
|
112
253
|
AGINTIFLOW_PACKAGE_DIR: packageDir,
|
|
113
254
|
...(language ? { AGINTI_LANGUAGE: language } : {}),
|
|
114
255
|
},
|
|
@@ -117,6 +258,16 @@ export async function ensureAgintiWebApp({
|
|
|
117
258
|
|
|
118
259
|
const healthy = await waitForHealth(candidate.host, candidate.port);
|
|
119
260
|
return healthy
|
|
120
|
-
? {
|
|
261
|
+
? {
|
|
262
|
+
ok: true,
|
|
263
|
+
reused: false,
|
|
264
|
+
started: true,
|
|
265
|
+
restarted: Boolean(candidate.restarted),
|
|
266
|
+
stopped: candidate.stopped,
|
|
267
|
+
pid: child.pid,
|
|
268
|
+
runtimeDir,
|
|
269
|
+
agintiflowHome: homeDir,
|
|
270
|
+
...candidate,
|
|
271
|
+
}
|
|
121
272
|
: { ok: false, error: `Started web process ${child.pid}, but ${candidate.url}/health did not become ready.`, url: "" };
|
|
122
273
|
}
|
package/web.js
CHANGED
|
@@ -1383,7 +1383,21 @@ app.post("/api/runs/:sessionId/stop", async (req, res) => {
|
|
|
1383
1383
|
});
|
|
1384
1384
|
|
|
1385
1385
|
app.get("/health", (_req, res) => {
|
|
1386
|
-
res.json({
|
|
1386
|
+
res.json({
|
|
1387
|
+
ok: true,
|
|
1388
|
+
app: "agintiflow",
|
|
1389
|
+
version: packageJson.version,
|
|
1390
|
+
pid: process.pid,
|
|
1391
|
+
host,
|
|
1392
|
+
port,
|
|
1393
|
+
url: `http://${host}:${port}`,
|
|
1394
|
+
runtimeDir: baseDir,
|
|
1395
|
+
projectRoot: baseDir,
|
|
1396
|
+
agintiflowHome: storagePaths.agintiflowHome,
|
|
1397
|
+
sessionsDir,
|
|
1398
|
+
projectSessionsDir,
|
|
1399
|
+
packageDir,
|
|
1400
|
+
});
|
|
1387
1401
|
});
|
|
1388
1402
|
|
|
1389
1403
|
await fs.mkdir(sessionsDir, { recursive: true });
|