@lazyingart/agintiflow 0.20.128 → 0.20.130
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 +3 -2
- package/package.json +1 -1
- package/references/agintiflow-aaps-slogan-subtext-v3.md +1 -1
- package/scripts/smoke-auto-update.js +48 -0
- package/scripts/smoke-web-autostart.js +45 -0
- package/scripts/smoke-webapp-command.js +13 -0
- package/src/auto-update.js +40 -4
- package/src/cli.js +69 -11
- package/src/i18n.js +1 -1
- package/src/interactive-cli.js +31 -11
- package/src/web-autostart.js +54 -2
package/README.md
CHANGED
|
@@ -103,7 +103,7 @@ 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 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.
|
|
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, `/webapp stop [port]` to stop the compatible local webapp, 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
|
|
|
@@ -111,6 +111,7 @@ Launch the web UI explicitly when you want a foreground web server:
|
|
|
111
111
|
|
|
112
112
|
```bash
|
|
113
113
|
aginti webapp
|
|
114
|
+
aginti webapp stop
|
|
114
115
|
aginti webapp restart
|
|
115
116
|
aginti web --port 3210
|
|
116
117
|
# opens http://127.0.0.1:3210, or the next available port
|
|
@@ -145,7 +146,7 @@ aginti --language de
|
|
|
145
146
|
| Goal | Command |
|
|
146
147
|
| --- | --- |
|
|
147
148
|
| Start interactive chat | `aginti` or `aginti chat` |
|
|
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` |
|
|
149
|
+
| Start local web app | Auto-starts with `aginti`; detached command is `aginti webapp`; stop with `aginti webapp stop`; restart with `aginti webapp restart`; foreground mode is `aginti web --port 3210` |
|
|
149
150
|
| Save provider keys | `aginti auth`, `/auth`, `/login` |
|
|
150
151
|
| Review current repo | `/review [focus]` |
|
|
151
152
|
| 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.130",
|
|
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",
|
|
@@ -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
|
|
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.
|
|
18
18
|
|
|
19
19
|
## Shorter Version
|
|
20
20
|
|
|
@@ -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");
|
|
@@ -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";
|
|
@@ -13,6 +14,14 @@ const preferredPort = 44500 + Math.floor(Math.random() * 1000);
|
|
|
13
14
|
let childPid = 0;
|
|
14
15
|
const originalHome = process.env.AGINTIFLOW_HOME;
|
|
15
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
|
+
}
|
|
24
|
+
|
|
16
25
|
try {
|
|
17
26
|
process.env.AGINTIFLOW_HOME = inheritedHome;
|
|
18
27
|
const first = await ensureAgintiWebApp({
|
|
@@ -55,6 +64,42 @@ try {
|
|
|
55
64
|
throw new Error(`expected restart on same URL with a new pid, got ${JSON.stringify(restarted)}`);
|
|
56
65
|
}
|
|
57
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
|
+
}
|
|
58
103
|
console.log(`web auto-start smoke passed: ${first.url}`);
|
|
59
104
|
} finally {
|
|
60
105
|
if (originalHome === undefined) delete process.env.AGINTIFLOW_HOME;
|
|
@@ -74,6 +74,19 @@ async function runCase({ port, env = {}, expectHeader, label }) {
|
|
|
74
74
|
if (path.resolve(health.agintiflowHome) !== path.resolve(path.join(runtimeDir, `.agintiflow-web-home-${label}`))) {
|
|
75
75
|
throw new Error(`webapp command inherited the wrong home for ${label}: ${JSON.stringify(health)}`);
|
|
76
76
|
}
|
|
77
|
+
child.stdin.write(`/webapp stop ${port}\n`);
|
|
78
|
+
await waitFor(() => output.stdout.includes(`webapp=http://127.0.0.1:${port} stopped`), child, `${label} /webapp stop command`, output);
|
|
79
|
+
let stopped = false;
|
|
80
|
+
try {
|
|
81
|
+
await fetch(`http://127.0.0.1:${port}/health`);
|
|
82
|
+
} catch {
|
|
83
|
+
stopped = true;
|
|
84
|
+
}
|
|
85
|
+
if (!stopped) {
|
|
86
|
+
throw new Error(`webapp still responded after /webapp stop for ${label}`);
|
|
87
|
+
}
|
|
88
|
+
child.stdin.write(`/webapp ${port}\n`);
|
|
89
|
+
await waitFor(() => output.stdout.includes(`webapp=http://127.0.0.1:${port} started`), child, `${label} /webapp restart after stop`, output);
|
|
77
90
|
} finally {
|
|
78
91
|
child.kill("SIGTERM");
|
|
79
92
|
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
|
@@ -37,7 +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
|
+
import { ensureAgintiWebApp, stopAgintiWebApp } from "./web-autostart.js";
|
|
41
41
|
import { dockerHostInstallPlan, formatDockerSetupText, summarizeDockerSetup } from "./docker-setup.js";
|
|
42
42
|
import fs from "node:fs/promises";
|
|
43
43
|
import path from "node:path";
|
|
@@ -403,9 +403,9 @@ export function parseArgs(argv) {
|
|
|
403
403
|
parts.push(...argv.slice(i + 1));
|
|
404
404
|
break;
|
|
405
405
|
}
|
|
406
|
-
if ((arg === "web" || arg === "--web") && String(argv[i + 1] || "").toLowerCase()
|
|
406
|
+
if ((arg === "web" || arg === "--web") && ["restart", "stop"].includes(String(argv[i + 1] || "").toLowerCase())) {
|
|
407
407
|
result.webapp = true;
|
|
408
|
-
result.webAction = "
|
|
408
|
+
result.webAction = String(argv[i + 1] || "").toLowerCase();
|
|
409
409
|
i += 1;
|
|
410
410
|
continue;
|
|
411
411
|
}
|
|
@@ -869,6 +869,43 @@ function stripLeadingGlobalOptions(argv = []) {
|
|
|
869
869
|
};
|
|
870
870
|
}
|
|
871
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
|
+
|
|
872
909
|
function providerLabel(provider) {
|
|
873
910
|
const normalized = String(provider || "").toLowerCase();
|
|
874
911
|
if (normalized === "openai") return "OpenAI";
|
|
@@ -1687,6 +1724,8 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1687
1724
|
}
|
|
1688
1725
|
|
|
1689
1726
|
if (argv[0] === "update" || argv[0] === "upgrade") {
|
|
1727
|
+
const updateCommandCwd = commandCwdFromArgv(argv);
|
|
1728
|
+
const updateLanguage = languageFromArgv(argv);
|
|
1690
1729
|
const updateResult = await maybeAutoUpdate({
|
|
1691
1730
|
argv,
|
|
1692
1731
|
force: true,
|
|
@@ -1695,11 +1734,14 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1695
1734
|
packageName: packageJson.name,
|
|
1696
1735
|
packageVersion: packageJson.version,
|
|
1697
1736
|
restart: false,
|
|
1737
|
+
afterUpdate: () => restartWebAppAfterUpdate({ commandCwd: updateCommandCwd, language: updateLanguage }),
|
|
1698
1738
|
});
|
|
1699
1739
|
if (updateResult.error) process.exit(1);
|
|
1700
1740
|
return;
|
|
1701
1741
|
}
|
|
1702
1742
|
|
|
1743
|
+
const updateCommandCwd = commandCwdFromArgv(argv);
|
|
1744
|
+
const updateLanguage = languageFromArgv(argv);
|
|
1703
1745
|
const autoUpdateResult = await maybeAutoUpdate({
|
|
1704
1746
|
argv,
|
|
1705
1747
|
force: argv.includes("--auto-update"),
|
|
@@ -1707,6 +1749,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1707
1749
|
packageName: packageJson.name,
|
|
1708
1750
|
packageVersion: packageJson.version,
|
|
1709
1751
|
restart: true,
|
|
1752
|
+
afterUpdate: () => restartWebAppAfterUpdate({ commandCwd: updateCommandCwd, language: updateLanguage }),
|
|
1710
1753
|
});
|
|
1711
1754
|
if (autoUpdateResult.restarted) process.exit(autoUpdateResult.exitCode ?? 0);
|
|
1712
1755
|
|
|
@@ -1906,25 +1949,40 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1906
1949
|
|
|
1907
1950
|
if (args.webapp) {
|
|
1908
1951
|
const action = String(args.webAction || "start").toLowerCase();
|
|
1909
|
-
if (!["start", "restart", "reuse"].includes(action)) {
|
|
1910
|
-
console.error("Usage: aginti webapp [start|restart] [--port 3210] [--host 127.0.0.1]");
|
|
1952
|
+
if (!["start", "stop", "restart", "reuse"].includes(action)) {
|
|
1953
|
+
console.error("Usage: aginti webapp [start|stop|restart|reuse] [--port 3210] [--host 127.0.0.1]");
|
|
1911
1954
|
process.exit(1);
|
|
1912
1955
|
}
|
|
1913
|
-
const
|
|
1956
|
+
const webOptions = {
|
|
1914
1957
|
packageDir,
|
|
1915
1958
|
cwd: args.commandCwd || commandCwd,
|
|
1916
1959
|
home: args.webHome || "",
|
|
1917
1960
|
host: args.host || process.env.AGINTI_WEB_HOST || "127.0.0.1",
|
|
1918
1961
|
preferredPort: args.port || process.env.AGINTI_WEB_PORT || 3210,
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1962
|
+
};
|
|
1963
|
+
const result = await (action === "stop"
|
|
1964
|
+
? stopAgintiWebApp(webOptions)
|
|
1965
|
+
: ensureAgintiWebApp({
|
|
1966
|
+
...webOptions,
|
|
1967
|
+
language: args.language ? resolveLanguage(args.language) : "",
|
|
1968
|
+
restart: action === "restart",
|
|
1969
|
+
respectAutoStartDisable: false,
|
|
1970
|
+
})
|
|
1971
|
+
).catch((error) => ({ ok: false, error: error instanceof Error ? error.message : String(error), url: "" }));
|
|
1923
1972
|
if (!result.ok) {
|
|
1924
1973
|
console.error(`webapp unavailable: ${result.error || "unknown"}`);
|
|
1925
1974
|
process.exit(1);
|
|
1926
1975
|
}
|
|
1927
|
-
const state =
|
|
1976
|
+
const state =
|
|
1977
|
+
action === "stop" && result.stopped
|
|
1978
|
+
? "stopped"
|
|
1979
|
+
: action === "stop" && result.alreadyStopped
|
|
1980
|
+
? "already stopped"
|
|
1981
|
+
: result.restarted
|
|
1982
|
+
? "restarted"
|
|
1983
|
+
: result.reused
|
|
1984
|
+
? "reused"
|
|
1985
|
+
: "started";
|
|
1928
1986
|
console.log(`webapp: ${result.url} ${state}`);
|
|
1929
1987
|
console.log(`project: ${result.runtimeDir || path.resolve(args.commandCwd || commandCwd)}`);
|
|
1930
1988
|
console.log(`home: ${result.agintiflowHome || ""}`);
|
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, reuse, or restart the local webapp and print its URL.",
|
|
142
|
+
helpWebapp: "Start, reuse, stop, 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
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
permissionModeForApprovalCategory,
|
|
46
46
|
permissionModeLabel,
|
|
47
47
|
} from "./permission-modes.js";
|
|
48
|
-
import { ensureAgintiWebApp } from "./web-autostart.js";
|
|
48
|
+
import { ensureAgintiWebApp, stopAgintiWebApp } from "./web-autostart.js";
|
|
49
49
|
|
|
50
50
|
const useColor = Boolean(input.isTTY && output.isTTY && process.env.AGINTIFLOW_NO_COLOR !== "1");
|
|
51
51
|
const ansi = {
|
|
@@ -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|restart]", "Start, reuse, or restart the local webapp and print its URL.", "helpWebapp")}`,
|
|
884
|
+
` ${command("/webapp [port|start|stop|restart|reuse]", "Start, reuse, stop, 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")}`,
|
|
@@ -3011,23 +3011,43 @@ async function handleCommand(line, state, packageDir) {
|
|
|
3011
3011
|
}
|
|
3012
3012
|
if (command === "webapp" || command === "web") {
|
|
3013
3013
|
const words = value.split(/\s+/).filter(Boolean);
|
|
3014
|
+
const action = words.find((word) => ["start", "stop", "restart", "reuse"].includes(word.toLowerCase()))?.toLowerCase() || "start";
|
|
3014
3015
|
const restart = words.some((word) => word.toLowerCase() === "restart");
|
|
3015
3016
|
const portValue = words.find((word) => /^\d+$/.test(word));
|
|
3016
3017
|
const port = Number(portValue) || Number(process.env.AGINTI_WEB_PORT || process.env.PORT || 3210);
|
|
3017
|
-
printSystemLine(restart ? "webapp=restarting" : "webapp=starting");
|
|
3018
|
-
const
|
|
3018
|
+
printSystemLine(action === "stop" ? "webapp=stopping" : restart ? "webapp=restarting" : "webapp=starting");
|
|
3019
|
+
const webOptions = {
|
|
3019
3020
|
packageDir,
|
|
3020
3021
|
cwd: state.commandCwd || process.cwd(),
|
|
3021
3022
|
host: process.env.AGINTI_WEB_HOST || process.env.HOST || "127.0.0.1",
|
|
3022
3023
|
preferredPort: port,
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3024
|
+
};
|
|
3025
|
+
const result = await (action === "stop"
|
|
3026
|
+
? stopAgintiWebApp(webOptions)
|
|
3027
|
+
: ensureAgintiWebApp({
|
|
3028
|
+
...webOptions,
|
|
3029
|
+
language: state.language,
|
|
3030
|
+
restart,
|
|
3031
|
+
respectAutoStartDisable: false,
|
|
3032
|
+
})
|
|
3033
|
+
).catch((error) => ({ ok: false, error: error instanceof Error ? error.message : String(error), url: "" }));
|
|
3027
3034
|
if (result.ok) {
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3035
|
+
const stateLabel = result.stopped
|
|
3036
|
+
? action === "stop"
|
|
3037
|
+
? "stopped"
|
|
3038
|
+
: result.restarted
|
|
3039
|
+
? "restarted"
|
|
3040
|
+
: "started"
|
|
3041
|
+
: action === "stop" && result.alreadyStopped
|
|
3042
|
+
? "already stopped"
|
|
3043
|
+
: result.restarted
|
|
3044
|
+
? "restarted"
|
|
3045
|
+
: result.reused
|
|
3046
|
+
? "reused"
|
|
3047
|
+
: "started";
|
|
3048
|
+
state.webAppUrl = action === "stop" ? "" : result.url;
|
|
3049
|
+
state.webAppNotice = action === "stop" ? "webapp stopped - use /webapp to start it again" : "";
|
|
3050
|
+
printSystemLine(`webapp=${result.url} ${stateLabel}`);
|
|
3031
3051
|
} else {
|
|
3032
3052
|
state.webAppUrl = "";
|
|
3033
3053
|
state.webAppNotice = `webapp unavailable - use /webapp to retry; error: ${compactLine(result.error || "unknown", 72)}`;
|
package/src/web-autostart.js
CHANGED
|
@@ -169,6 +169,57 @@ async function stopWebAppOnPort({ host, port, health = {} } = {}) {
|
|
|
169
169
|
: { ok: false, pids: [...pids], error: `AgInTiFlow webapp on ${host}:${port} did not stop.` };
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
export async function stopAgintiWebApp({
|
|
173
|
+
packageDir = process.cwd(),
|
|
174
|
+
cwd = process.cwd(),
|
|
175
|
+
home = "",
|
|
176
|
+
host = DEFAULT_HOST,
|
|
177
|
+
preferredPort = DEFAULT_PORT,
|
|
178
|
+
force = false,
|
|
179
|
+
} = {}) {
|
|
180
|
+
const normalizedHost = normalizeHost(host);
|
|
181
|
+
const port = normalizePort(preferredPort);
|
|
182
|
+
const url = webUrl(normalizedHost, port);
|
|
183
|
+
const runtimeDir = resolveRuntimeDir(cwd);
|
|
184
|
+
const homeDir = resolveWebHome(home);
|
|
185
|
+
const health = await fetchHealthDetails(normalizedHost, port);
|
|
186
|
+
|
|
187
|
+
if (!health.ok) {
|
|
188
|
+
if (await canListen(normalizedHost, port)) {
|
|
189
|
+
return { ok: true, stopped: false, alreadyStopped: true, host: normalizedHost, port, url, runtimeDir, agintiflowHome: homeDir };
|
|
190
|
+
}
|
|
191
|
+
return { ok: false, stopped: false, host: normalizedHost, port, url, error: `No AgInTiFlow health endpoint responded on ${url}.` };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!force && !compatibleHealth(health, { cwd: runtimeDir, home: homeDir, packageDir })) {
|
|
195
|
+
return {
|
|
196
|
+
ok: false,
|
|
197
|
+
stopped: false,
|
|
198
|
+
host: normalizedHost,
|
|
199
|
+
port,
|
|
200
|
+
url,
|
|
201
|
+
runtimeDir,
|
|
202
|
+
agintiflowHome: homeDir,
|
|
203
|
+
health,
|
|
204
|
+
error: `Refusing to stop AgInTiFlow webapp on ${url} because it belongs to a different project or home.`,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const stopped = await stopWebAppOnPort({ host: normalizedHost, port, health });
|
|
209
|
+
return {
|
|
210
|
+
ok: Boolean(stopped.ok),
|
|
211
|
+
stopped: Boolean(stopped.ok),
|
|
212
|
+
alreadyStopped: false,
|
|
213
|
+
host: normalizedHost,
|
|
214
|
+
port,
|
|
215
|
+
url,
|
|
216
|
+
runtimeDir,
|
|
217
|
+
agintiflowHome: homeDir,
|
|
218
|
+
health,
|
|
219
|
+
...stopped,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
172
223
|
export async function findReusableOrFreeWebPort({
|
|
173
224
|
host = DEFAULT_HOST,
|
|
174
225
|
preferredPort = DEFAULT_PORT,
|
|
@@ -187,7 +238,8 @@ export async function findReusableOrFreeWebPort({
|
|
|
187
238
|
if (port >= 65536) break;
|
|
188
239
|
const health = await fetchHealthDetails(normalizedHost, port);
|
|
189
240
|
if (health.ok) {
|
|
190
|
-
|
|
241
|
+
const compatible = compatibleHealth(health, { cwd: runtimeDir, home: homeDir, packageDir });
|
|
242
|
+
if (restart && compatible) {
|
|
191
243
|
const stopped = await stopWebAppOnPort({ host: normalizedHost, port, health });
|
|
192
244
|
if (!stopped.ok) return { port, host: normalizedHost, url: "", reused: false, available: false, stopped, error: stopped.error };
|
|
193
245
|
return {
|
|
@@ -200,7 +252,7 @@ export async function findReusableOrFreeWebPort({
|
|
|
200
252
|
stopped,
|
|
201
253
|
};
|
|
202
254
|
}
|
|
203
|
-
if (
|
|
255
|
+
if (compatible) {
|
|
204
256
|
return { port, host: normalizedHost, url: webUrl(normalizedHost, port), reused: true, available: false, health };
|
|
205
257
|
}
|
|
206
258
|
continue;
|