@saadnvd1/agent-os 0.3.826 → 0.3.828
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/content/whats-new.json +8 -0
- package/lib/updates/generated.ts +29 -0
- package/lib/updates/run.ts +16 -6
- package/package.json +1 -1
- package/scripts/autodeploy +4 -0
- package/scripts/autodeploy.test.ts +6 -1
- package/scripts/generated-files.txt +3 -0
- package/scripts/generated.test.ts +77 -0
- package/scripts/lib/build-swap.sh +4 -0
- package/scripts/lib/generated.sh +18 -0
- package/scripts/redeploy +1 -0
- package/scripts/redeploy.test.ts +1 -0
package/content/whats-new.json
CHANGED
|
@@ -1937,6 +1937,14 @@
|
|
|
1937
1937
|
"area": "Chat",
|
|
1938
1938
|
"text": "Cancelled questions from GitHub Copilot chats now clear right away instead of staying on screen.",
|
|
1939
1939
|
"url": "https://github.com/saadnvd1/agent-os/pull/552"
|
|
1940
|
+
},
|
|
1941
|
+
{
|
|
1942
|
+
"id": 555,
|
|
1943
|
+
"date": "2026-11-02",
|
|
1944
|
+
"kind": "fixed",
|
|
1945
|
+
"area": "Updates",
|
|
1946
|
+
"text": "Automatic updates no longer get stuck after the first one because of a file the build rewrites.",
|
|
1947
|
+
"url": "https://github.com/saadnvd1/agent-os/pull/555"
|
|
1940
1948
|
}
|
|
1941
1949
|
]
|
|
1942
1950
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
/** Files the build rewrites in a checkout; one list for the updater and the deploy scripts. */
|
|
5
|
+
export function generatedFiles(root: string): string[] {
|
|
6
|
+
try {
|
|
7
|
+
return fs
|
|
8
|
+
.readFileSync(path.join(root, "scripts/generated-files.txt"), "utf8")
|
|
9
|
+
.split("\n")
|
|
10
|
+
.map((l) => l.trim())
|
|
11
|
+
.filter((l) => l && !l.startsWith("#"));
|
|
12
|
+
} catch {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Splits `git status --porcelain` paths into generated files and real local changes. */
|
|
18
|
+
export function splitDirty(
|
|
19
|
+
porcelain: string,
|
|
20
|
+
generated: string[]
|
|
21
|
+
): { generated: string[]; blocking: string[] } {
|
|
22
|
+
const out = { generated: [] as string[], blocking: [] as string[] };
|
|
23
|
+
for (const line of porcelain.split("\n")) {
|
|
24
|
+
const file = line.slice(3).trim();
|
|
25
|
+
if (!file) continue;
|
|
26
|
+
(generated.includes(file) ? out.generated : out.blocking).push(file);
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
package/lib/updates/run.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
type CheckDeps,
|
|
33
33
|
type CheckResult,
|
|
34
34
|
} from "./check";
|
|
35
|
+
import { generatedFiles, splitDirty } from "./generated";
|
|
35
36
|
import { decideAutoUpdate, pickTarget, type AutoDecision } from "./auto";
|
|
36
37
|
import {
|
|
37
38
|
attemptOf,
|
|
@@ -325,6 +326,16 @@ export type Refusal = { code: UpdateRefusal; error: string };
|
|
|
325
326
|
export const NO_RESTART =
|
|
326
327
|
"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 (started at login on Linux: run agent-os enable again to update its service).";
|
|
327
328
|
|
|
329
|
+
/** A build leaves these modified; the pull would refuse to overwrite them. */
|
|
330
|
+
export async function discardGenerated(deps: RunDeps): Promise<void> {
|
|
331
|
+
const { generated } = splitDirty(
|
|
332
|
+
await deps.git(["status", "--porcelain", "--untracked-files=no"]),
|
|
333
|
+
generatedFiles(deps.cwd)
|
|
334
|
+
);
|
|
335
|
+
if (generated.length)
|
|
336
|
+
await deps.git(["checkout", "--", ...generated]).catch(() => {});
|
|
337
|
+
}
|
|
338
|
+
|
|
328
339
|
export async function preflight(
|
|
329
340
|
check: CheckResult | null,
|
|
330
341
|
deps: RunDeps
|
|
@@ -356,12 +367,10 @@ export async function preflight(
|
|
|
356
367
|
error:
|
|
357
368
|
"Another update or deploy is building. Try again when it finishes.",
|
|
358
369
|
};
|
|
359
|
-
const dirty = (
|
|
360
|
-
await deps.git(["status", "--porcelain", "--untracked-files=no"])
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
.map((l) => l.slice(3).trim())
|
|
364
|
-
.filter(Boolean);
|
|
370
|
+
const dirty = splitDirty(
|
|
371
|
+
await deps.git(["status", "--porcelain", "--untracked-files=no"]),
|
|
372
|
+
generatedFiles(deps.cwd)
|
|
373
|
+
).blocking;
|
|
365
374
|
if (dirty.length)
|
|
366
375
|
return {
|
|
367
376
|
code: "dirty",
|
|
@@ -463,6 +472,7 @@ async function begin(
|
|
|
463
472
|
code: "unknown",
|
|
464
473
|
error: `Couldn't look for updates: ${result.error}`,
|
|
465
474
|
};
|
|
475
|
+
await discardGenerated(deps);
|
|
466
476
|
const refused = await preflight(result, deps);
|
|
467
477
|
if (refused) return { ok: false, ...refused };
|
|
468
478
|
if (when === "idle") {
|
package/package.json
CHANGED
package/scripts/autodeploy
CHANGED
|
@@ -51,6 +51,10 @@ published=$(cat "$state/published" 2>/dev/null || true)
|
|
|
51
51
|
[ "$head" = "$deployed" ] && [ "$head" = "$published" ] && exit 0
|
|
52
52
|
log() { echo "$(date '+%Y-%m-%d %-I:%M %p') $*"; }
|
|
53
53
|
|
|
54
|
+
# shellcheck source=lib/generated.sh
|
|
55
|
+
source scripts/lib/generated.sh
|
|
56
|
+
discard_generated
|
|
57
|
+
|
|
54
58
|
# A head that differs from the deployed one only in the changelog file needs
|
|
55
59
|
# a pull, not a build or a restart: the file is read from disk when asked.
|
|
56
60
|
changelog=content/whats-new.json
|
|
@@ -22,7 +22,12 @@ function setup() {
|
|
|
22
22
|
fs.mkdirSync(path.join(work, "scripts"));
|
|
23
23
|
fs.copyFileSync(script, path.join(work, "scripts/autodeploy"));
|
|
24
24
|
fs.mkdirSync(path.join(work, "scripts/lib"));
|
|
25
|
-
for (const f of [
|
|
25
|
+
for (const f of [
|
|
26
|
+
"sync-cli-links",
|
|
27
|
+
"lib/cli-links.sh",
|
|
28
|
+
"lib/generated.sh",
|
|
29
|
+
"generated-files.txt",
|
|
30
|
+
])
|
|
26
31
|
fs.copyFileSync(path.join(__dirname, f), path.join(work, "scripts", f));
|
|
27
32
|
fs.chmodSync(path.join(work, "scripts/sync-cli-links"), 0o755);
|
|
28
33
|
fs.mkdirSync(path.join(work, "bin"));
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { tempDir } from "@/lib/test-temp";
|
|
6
|
+
|
|
7
|
+
// discard_generated in a real clone: a build-modified generated file must not
|
|
8
|
+
// block a pull that deletes it upstream, and a real local edit still does.
|
|
9
|
+
const git = (cwd: string, ...args: string[]) =>
|
|
10
|
+
execFileSync("git", ["-c", "user.name=t", "-c", "user.email=t@t", ...args], {
|
|
11
|
+
cwd,
|
|
12
|
+
encoding: "utf-8",
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
function setup() {
|
|
16
|
+
const root = tempDir("aos-generated-");
|
|
17
|
+
const origin = path.join(root, "origin");
|
|
18
|
+
const seed = path.join(root, "seed");
|
|
19
|
+
const work = path.join(root, "work");
|
|
20
|
+
git(root, "init", "-q", "-b", "main", seed);
|
|
21
|
+
fs.mkdirSync(path.join(seed, "scripts/lib"), { recursive: true });
|
|
22
|
+
for (const f of ["generated-files.txt", "lib/generated.sh"])
|
|
23
|
+
fs.copyFileSync(
|
|
24
|
+
path.join(import.meta.dirname, f),
|
|
25
|
+
path.join(seed, "scripts", f)
|
|
26
|
+
);
|
|
27
|
+
fs.writeFileSync(path.join(seed, "next-env.d.ts"), "dev\n");
|
|
28
|
+
fs.writeFileSync(path.join(seed, "app.ts"), "a\n");
|
|
29
|
+
git(seed, "add", ".");
|
|
30
|
+
git(seed, "commit", "-qm", "first");
|
|
31
|
+
git(root, "clone", "-q", "--bare", seed, origin);
|
|
32
|
+
git(root, "clone", "-q", origin, work);
|
|
33
|
+
// Upstream stops tracking the generated file.
|
|
34
|
+
git(seed, "remote", "add", "origin", origin);
|
|
35
|
+
git(seed, "rm", "-q", "--cached", "next-env.d.ts");
|
|
36
|
+
git(seed, "commit", "-qm", "untrack");
|
|
37
|
+
git(seed, "push", "-q", "origin", "main");
|
|
38
|
+
return work;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const discard = (cwd: string) =>
|
|
42
|
+
execFileSync(
|
|
43
|
+
"bash",
|
|
44
|
+
["-c", "source scripts/lib/generated.sh && discard_generated"],
|
|
45
|
+
{ cwd, encoding: "utf-8" }
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
describe("discard_generated", () => {
|
|
49
|
+
it("restores a build-modified generated file so the pull goes through", () => {
|
|
50
|
+
const work = setup();
|
|
51
|
+
fs.writeFileSync(path.join(work, "next-env.d.ts"), "build\n");
|
|
52
|
+
discard(work);
|
|
53
|
+
expect(git(work, "status", "--porcelain", "--untracked-files=no")).toBe("");
|
|
54
|
+
git(work, "pull", "-q", "--ff-only");
|
|
55
|
+
expect(git(work, "ls-files", "next-env.d.ts")).toBe("");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("leaves real local changes in place", () => {
|
|
59
|
+
const work = setup();
|
|
60
|
+
fs.writeFileSync(path.join(work, "next-env.d.ts"), "build\n");
|
|
61
|
+
fs.writeFileSync(path.join(work, "app.ts"), "edited\n");
|
|
62
|
+
discard(work);
|
|
63
|
+
expect(git(work, "status", "--porcelain", "--untracked-files=no")).toBe(
|
|
64
|
+
" M app.ts\n"
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("leaves an untracked generated file alone", () => {
|
|
69
|
+
const work = setup();
|
|
70
|
+
git(work, "pull", "-q", "--ff-only");
|
|
71
|
+
fs.writeFileSync(path.join(work, "next-env.d.ts"), "build\n");
|
|
72
|
+
discard(work);
|
|
73
|
+
expect(fs.readFileSync(path.join(work, "next-env.d.ts"), "utf-8")).toBe(
|
|
74
|
+
"build\n"
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
# .next-prev for one rollback. A failed build never touches .next.
|
|
8
8
|
# Sourced by scripts/redeploy; run from the repository root.
|
|
9
9
|
|
|
10
|
+
# shellcheck source=generated.sh
|
|
11
|
+
source scripts/lib/generated.sh
|
|
12
|
+
|
|
10
13
|
LIVE=.next
|
|
11
14
|
NEXT_BUILD=.next-build
|
|
12
15
|
PREV=.next-prev
|
|
@@ -47,6 +50,7 @@ build_beside() {
|
|
|
47
50
|
[ -d "$LIVE/types.building" ] && mv "$LIVE/types.building" "$LIVE/types"
|
|
48
51
|
for f in "${REWRITTEN[@]}"; do [ -f "$saved/$f" ] && cp -p "$saved/$f" "$f"; done
|
|
49
52
|
rm -rf "$saved"
|
|
53
|
+
discard_generated
|
|
50
54
|
# `next build` exits non-zero on a type error, so a build that got here
|
|
51
55
|
# typechecked; a missing BUILD_ID means it stopped short anyway.
|
|
52
56
|
if [ "$status" = 0 ] && [ ! -s "$NEXT_BUILD/BUILD_ID" ]; then
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Generated files (scripts/generated-files.txt) are rewritten by `next build`
|
|
3
|
+
# and `next dev`, which would leave the checkout dirty and make a pull fail.
|
|
4
|
+
# Sourced by scripts/redeploy and scripts/autodeploy; run from the repository root.
|
|
5
|
+
|
|
6
|
+
# Put every tracked, modified generated file back to HEAD. Untracked ones are
|
|
7
|
+
# left alone.
|
|
8
|
+
discard_generated() {
|
|
9
|
+
local f
|
|
10
|
+
[ -f scripts/generated-files.txt ] || return 0
|
|
11
|
+
while IFS= read -r f; do
|
|
12
|
+
case "$f" in ''|'#'*) continue ;; esac
|
|
13
|
+
if git ls-files --error-unmatch -- "$f" >/dev/null 2>&1 &&
|
|
14
|
+
[ -n "$(git status --porcelain --untracked-files=no -- "$f")" ]; then
|
|
15
|
+
git checkout -q -- "$f" || true
|
|
16
|
+
fi
|
|
17
|
+
done < scripts/generated-files.txt
|
|
18
|
+
}
|
package/scripts/redeploy
CHANGED
|
@@ -38,6 +38,7 @@ check_disk() {
|
|
|
38
38
|
# The pull can replace this script, so what follows runs from a fresh start.
|
|
39
39
|
if [ "${1:-}" != "--rollback" ] && [ "${1:-}" != "--pulled" ]; then
|
|
40
40
|
check_disk
|
|
41
|
+
discard_generated
|
|
41
42
|
echo "::phase pulling"
|
|
42
43
|
if [ -n "${AGENTOS_UPDATE_TO:-}" ]; then
|
|
43
44
|
git fetch -q origin
|