@lazyingart/agintiflow 0.20.129 → 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 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. After a successful `aginti update` or accepted startup auto-update, AgInTiFlow also restarts the compatible local webapp so artifact serving uses the updated package.
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.129",
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",
@@ -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/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() === "restart") {
406
+ if ((arg === "web" || arg === "--web") && ["restart", "stop"].includes(String(argv[i + 1] || "").toLowerCase())) {
407
407
  result.webapp = true;
408
- result.webAction = "restart";
408
+ result.webAction = String(argv[i + 1] || "").toLowerCase();
409
409
  i += 1;
410
410
  continue;
411
411
  }
@@ -1949,25 +1949,40 @@ export async function main(argv = process.argv.slice(2)) {
1949
1949
 
1950
1950
  if (args.webapp) {
1951
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]");
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]");
1954
1954
  process.exit(1);
1955
1955
  }
1956
- const result = await ensureAgintiWebApp({
1956
+ const webOptions = {
1957
1957
  packageDir,
1958
1958
  cwd: args.commandCwd || commandCwd,
1959
1959
  home: args.webHome || "",
1960
1960
  host: args.host || process.env.AGINTI_WEB_HOST || "127.0.0.1",
1961
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: "" }));
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: "" }));
1966
1972
  if (!result.ok) {
1967
1973
  console.error(`webapp unavailable: ${result.error || "unknown"}`);
1968
1974
  process.exit(1);
1969
1975
  }
1970
- const state = result.restarted ? "restarted" : result.reused ? "reused" : "started";
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";
1971
1986
  console.log(`webapp: ${result.url} ${state}`);
1972
1987
  console.log(`project: ${result.runtimeDir || path.resolve(args.commandCwd || commandCwd)}`);
1973
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.",
@@ -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 result = await ensureAgintiWebApp({
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
- language: state.language,
3024
- restart,
3025
- respectAutoStartDisable: false,
3026
- }).catch((error) => ({ ok: false, error: error instanceof Error ? error.message : String(error), url: "" }));
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
- state.webAppUrl = result.url;
3029
- state.webAppNotice = "";
3030
- printSystemLine(`webapp=${result.url} ${result.restarted ? "restarted" : result.reused ? "reused" : "started"}`);
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)}`;
@@ -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,