@saadnvd1/agent-os 0.3.731 → 0.3.733

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.
@@ -50,6 +50,12 @@ export function UpdateActions({ view }: { view: UpdateView }) {
50
50
  Cancel
51
51
  </Button>
52
52
  );
53
+ if (view.canUpdate.code === "no_restart")
54
+ return (
55
+ <Button size="sm" className="min-h-11 md:min-h-0" disabled>
56
+ Update now
57
+ </Button>
58
+ );
53
59
  if (!view.canUpdate.ok) return null;
54
60
 
55
61
  const start = (when: "now" | "idle") => {
@@ -50,6 +50,7 @@ const make = (over: Partial<UpdateView> = {}): UpdateView => ({
50
50
  checkError: null,
51
51
  bannerEnabled: true,
52
52
  bannerLocked: false,
53
+ canRestart: true,
53
54
  canUpdate: { ok: true },
54
55
  run: null,
55
56
  pending: null,
@@ -7,6 +7,7 @@ import { git } from "../git-exec";
7
7
  import { repoKey } from "../git-remote";
8
8
  import { IN_FLIGHT } from "../tasks/queue-rows";
9
9
  import { expandHome } from "../tasks/session";
10
+ import { isNoise } from "./overlap";
10
11
  import { MAX_FILES } from "./types";
11
12
  import {
12
13
  localLiveSessionIds,
@@ -47,7 +48,8 @@ export async function changedFiles(
47
48
  git(dir, ["ls-files", "--others", "--exclude-standard", "-z"], OPTS),
48
49
  ]);
49
50
  return [...new Set([...paths(committed), ...paths(untracked)])].filter(
50
- (p) => !p.split("/").some((seg) => seg.endsWith(".aos-partial"))
51
+ (p) =>
52
+ !p.split("/").some((seg) => seg.endsWith(".aos-partial")) && !isNoise(p)
51
53
  );
52
54
  }
53
55
 
@@ -7,8 +7,28 @@ import type { Claim, Overlap } from "./types";
7
7
  const NOISE =
8
8
  /(^|\/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|Gemfile\.lock|Cargo\.lock|go\.sum|poetry\.lock|uv\.lock|composer\.lock|CHANGELOG[^/]*|__snapshots__\/.*|migrations\.generated\.ts)$/i;
9
9
 
10
- export const isNoise = (path: string) =>
11
- NOISE.test(path) || /(^|\/)__snapshots__\//.test(path);
10
+ // Build output, caches and OS files: directory segments (never name substrings).
11
+ const NOISE_DIRS = new Set([
12
+ "__pycache__",
13
+ "node_modules",
14
+ ".next",
15
+ "dist",
16
+ "build",
17
+ "coverage",
18
+ ".pytest_cache",
19
+ ".mypy_cache",
20
+ ".ruff_cache",
21
+ ]);
22
+ const NOISE_FILE = /(\.(pyc|pyo|tsbuildinfo)|^\.DS_Store)$/;
23
+
24
+ export const isNoise = (path: string) => {
25
+ if (NOISE.test(path) || /(^|\/)__snapshots__\//.test(path)) return true;
26
+ const segs = path.split("/");
27
+ return (
28
+ segs.slice(0, -1).some((s) => NOISE_DIRS.has(s)) ||
29
+ NOISE_FILE.test(segs[segs.length - 1])
30
+ );
31
+ };
12
32
 
13
33
  // A path this many claims touch is a shared index or router, not a signal.
14
34
  export const HUB_CLAIMS = 4;
@@ -44,6 +44,7 @@ export type UpdateRefusal =
44
44
  | "diverged"
45
45
  | "up_to_date"
46
46
  | "unknown"
47
+ | "no_restart"
47
48
  | "forbidden";
48
49
 
49
50
  export interface UpdateView {
@@ -61,6 +62,8 @@ export interface UpdateView {
61
62
  checkError: string | null;
62
63
  bannerEnabled: boolean;
63
64
  bannerLocked: boolean;
65
+ /** False when the server has no command to restart itself with. */
66
+ canRestart: boolean;
64
67
  canUpdate: { ok: boolean; code?: UpdateRefusal; reason?: string };
65
68
  run: UpdateRun | null;
66
69
  pending: { requestedAt: number } | null;
@@ -197,6 +197,9 @@ export function readLog(deps: Pick<RunDeps, "logFile">, lines = 30): string[] {
197
197
  export type Refusal = { code: UpdateRefusal; error: string };
198
198
 
199
199
  /** Why this checkout can't be updated now; null when it can. No network. */
200
+ export const NO_RESTART =
201
+ "This server can't restart itself, so an update would leave the old build running on new files. Set AGENTOS_RESTART to its restart command, or start it with agent-os start.";
202
+
200
203
  export async function preflight(
201
204
  check: CheckResult | null,
202
205
  deps: RunDeps
@@ -258,6 +261,8 @@ export async function preflight(
258
261
  };
259
262
  if (!check.available)
260
263
  return { code: "up_to_date", error: "AgentOS is up to date." };
264
+ if (!(await deps.restartCommand()))
265
+ return { code: "no_restart", error: NO_RESTART };
261
266
  return null;
262
267
  }
263
268
 
@@ -339,14 +344,14 @@ async function begin(
339
344
  toVersion: result.latest!.version,
340
345
  startedAt: deps.now(),
341
346
  });
342
- const restart = await deps.restartCommand();
347
+ const restart = (await deps.restartCommand())!;
343
348
  const env = { ...deps.env } as NodeJS.ProcessEnv;
344
349
  // The restarted server must work out its own build, not inherit this one.
345
350
  delete env.AGENTOS_BUILD;
346
351
  delete env.AGENTOS_RESTART;
347
352
  env.AGENTOS_UPDATE_STATE = deps.stateFile;
348
353
  env.AGENTOS_UPDATE_ID = id;
349
- if (restart) env.AGENTOS_RESTART = restart;
354
+ env.AGENTOS_RESTART = restart;
350
355
  if (deps.env.PORT) env.PORT = deps.env.PORT;
351
356
  deps.spawnRunner(env, deps.logFile);
352
357
  return { ok: true, pending: false };
@@ -53,6 +53,7 @@ export async function updateView(
53
53
  const pending = readPending(deps);
54
54
  const active = isActivePhase(run?.phase);
55
55
  const refusal = mayUpdate ? await preflight(check, deps) : null;
56
+ const canRestart = !!(await deps.restartCommand());
56
57
  const wantsWorking = !active && !!(check?.available || pending);
57
58
  return {
58
59
  current: check?.current ?? null,
@@ -64,6 +65,7 @@ export async function updateView(
64
65
  checkError: check?.error ?? null,
65
66
  bannerEnabled: bannerEnabled(deps.env),
66
67
  bannerLocked: bannerLocked(deps.env),
68
+ canRestart,
67
69
  canUpdate: !mayUpdate
68
70
  ? { ok: false, code: "forbidden", reason: NOT_ALLOWED }
69
71
  : refusal
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saadnvd1/agent-os",
3
- "version": "0.3.731",
3
+ "version": "0.3.733",
4
4
  "description": "Self-hosted web UI for managing Claude Code sessions",
5
5
  "bin": {
6
6
  "agent-os": "scripts/agent-os"
package/scripts/redeploy CHANGED
@@ -2,6 +2,7 @@
2
2
  # Update a running checkout to origin's branch: pull, install, build beside
3
3
  # the live server, swap the build in and restart, so neither a build in
4
4
  # progress nor a failed one takes the running server down.
5
+ # Without AGENTOS_RESTART it builds and checks but swaps nothing in.
5
6
  # Restart with: AGENTOS_RESTART="<command>" scripts/redeploy
6
7
  # Put the previous build back: AGENTOS_RESTART="<command>" scripts/redeploy --rollback
7
8
  set -euo pipefail
@@ -49,6 +50,14 @@ else
49
50
  echo "::phase building"
50
51
  # A build that fails on the new dependencies puts the old ones back.
51
52
  build_beside || { [ "$swapped" = 1 ] && restore_deps; exit 1; }
53
+ if [ -z "${AGENTOS_RESTART:-}" ]; then
54
+ # Nothing can restart the server, and a new build or node_modules under
55
+ # the old process breaks its pages: leave the live tree as it was.
56
+ [ "$swapped" = 1 ] && restore_deps
57
+ echo "Built and verified in $NEXT_BUILD, but not swapped in: nothing here can restart the server."
58
+ echo "Set AGENTOS_RESTART to its restart command and run scripts/redeploy again to swap and restart."
59
+ exit 0
60
+ fi
52
61
  swap_build
53
62
  fi
54
63
 
@@ -68,4 +77,4 @@ if [ -n "${AGENTOS_RESTART:-}" ]; then
68
77
  echo "AgentOS did not come back on $port within ${AGENTOS_UP_WAIT:-600}s; scripts/redeploy --rollback puts the last build back" >&2
69
78
  exit 1
70
79
  fi
71
- echo "Built. Restart your AgentOS server to run it."
80
+ echo "Rolled back. Restart your AgentOS server to run it."
@@ -0,0 +1,77 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import {
3
+ chmodSync,
4
+ copyFileSync,
5
+ existsSync,
6
+ mkdirSync,
7
+ readFileSync,
8
+ writeFileSync,
9
+ } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { beforeEach, describe, expect, it } from "vitest";
12
+ import { tempDir } from "@/lib/test-temp";
13
+
14
+ // Runs the real scripts/redeploy (--pulled, so no git) in a scratch tree with
15
+ // stub install, build and pnpm.
16
+ let dir: string;
17
+ beforeEach(() => {
18
+ dir = tempDir("aos-redeploy-");
19
+ mkdirSync(join(dir, "scripts/lib"), { recursive: true });
20
+ for (const f of [
21
+ "redeploy",
22
+ "lib/build-swap.sh",
23
+ "lib/pnpm.sh",
24
+ "lib/deps-swap.sh",
25
+ ])
26
+ copyFileSync(join(import.meta.dirname, f), join(dir, "scripts", f));
27
+ chmodSync(join(dir, "scripts/redeploy"), 0o755);
28
+ mkdirSync(join(dir, ".next"));
29
+ writeFileSync(join(dir, ".next/BUILD_ID"), "old");
30
+ mkdirSync(join(dir, "bin"));
31
+ for (const name of ["pnpm"])
32
+ writeFileSync(join(dir, "bin", name), "#!/bin/sh\nexit 0\n", {
33
+ mode: 0o755,
34
+ });
35
+ for (const f of ["package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml"])
36
+ writeFileSync(join(dir, f), "{}");
37
+ });
38
+
39
+ const redeploy = (restart?: string) => {
40
+ const env: NodeJS.ProcessEnv = {
41
+ ...process.env,
42
+ HOME: dir,
43
+ PATH: `${join(dir, "bin")}:${process.env.PATH}`,
44
+ AGENTOS_INSTALL_CMD: "mkdir -p node_modules",
45
+ AGENTOS_NATIVE_CHECK: "true",
46
+ AGENTOS_BUILD_CMD:
47
+ 'mkdir -p "$AGENTOS_DIST_DIR" && echo new > "$AGENTOS_DIST_DIR/BUILD_ID"',
48
+ AGENTOS_UP_WAIT: "1",
49
+ PORT: "1",
50
+ };
51
+ delete env.AGENTOS_RESTART;
52
+ if (restart) env.AGENTOS_RESTART = restart;
53
+ return spawnSync("bash", [join(dir, "scripts/redeploy"), "--pulled"], {
54
+ cwd: dir,
55
+ encoding: "utf8",
56
+ env,
57
+ });
58
+ };
59
+ const liveBuild = () =>
60
+ readFileSync(join(dir, ".next/BUILD_ID"), "utf8").trim();
61
+
62
+ describe("scripts/redeploy", () => {
63
+ it("without a restart command builds but leaves the live build in place", () => {
64
+ const r = redeploy();
65
+ expect(r.status).toBe(0);
66
+ expect(r.stdout).toContain("not swapped in");
67
+ expect(liveBuild()).toBe("old");
68
+ expect(existsSync(join(dir, ".next-prev"))).toBe(false);
69
+ });
70
+
71
+ it("with a restart command swaps the build in and restarts", () => {
72
+ const r = redeploy(`touch ${join(dir, "restarted")}`);
73
+ expect(liveBuild()).toBe("new");
74
+ expect(existsSync(join(dir, "restarted"))).toBe(true);
75
+ expect(r.stdout).toContain("::phase restarting");
76
+ });
77
+ });