Merge v3-t80: extension snapshot of extensions/ only (git archive + tar), atomic last-good marker (T80)
This commit is contained in:
@@ -32712,7 +32712,7 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
const loaderGit = async (...args) => {
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
try {
|
||||
const result = await exec(["git", ...args], { cwd: repo, timeout: 120, signal: commands.signal });
|
||||
const result = await exec(["git", ...args], { cwd: repo, signal: commands.signal });
|
||||
if (result.code) throw new Error(result.stderr || `git exited ${result.code}`);
|
||||
return result.stdout.trim();
|
||||
} catch (error) {
|
||||
@@ -32728,7 +32728,8 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
const publish2 = async () => {
|
||||
await harness2.commit(async (tx) => Object.assign(await tx.doc(ExtensionState), state), context);
|
||||
ops2.op_extension_status(JSON.stringify(state));
|
||||
await fs.write("/src/sausage/.git/sausage-last-good", state.good ?? "");
|
||||
await fs.write("/src/sausage/.git/sausage-last-good.tmp", state.good ?? "");
|
||||
await fs.rename("/src/sausage/.git/sausage-last-good.tmp", "/src/sausage/.git/sausage-last-good");
|
||||
};
|
||||
const dispose = async (entries2) => {
|
||||
for (const facet of entries2) for (const cleanup of facet.close) {
|
||||
@@ -32818,8 +32819,19 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
try {
|
||||
await fs.stat(snapshot);
|
||||
} catch {
|
||||
await loaderGit("worktree", "prune");
|
||||
await loaderGit("worktree", "add", "--detach", snapshot, sha);
|
||||
const staging = `${snapshot}.tmp`, archive = `${snapshot}.tar`;
|
||||
try {
|
||||
await fs.mkdir(staging);
|
||||
} catch (error) {
|
||||
if (error.code !== "already_exists") throw error;
|
||||
}
|
||||
if (await loaderGit("ls-tree", "--name-only", sha, "extensions")) {
|
||||
await loaderGit("archive", "--format=tar", `--output=${archive}`, sha, "extensions");
|
||||
const result = await exec(["tar", "-xf", archive, "-C", staging], { cwd: repo, signal: commands.signal });
|
||||
if (result.code) throw new Error(`tar: ${result.stderr || result.code}`);
|
||||
await fs.remove(archive);
|
||||
}
|
||||
await fs.rename(staging, snapshot);
|
||||
}
|
||||
const modules = [];
|
||||
for (const entry2 of await fs.list(`${snapshot}/extensions`).catch((error) => {
|
||||
|
||||
+16
-4
@@ -70720,7 +70720,7 @@ async function loadExtensions(harness2, registry3, storage2, models2, context, b
|
||||
const loaderGit = async (...args) => {
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
try {
|
||||
const result = await exec(["git", ...args], { cwd: repo, timeout: 120, signal: commands.signal });
|
||||
const result = await exec(["git", ...args], { cwd: repo, signal: commands.signal });
|
||||
if (result.code) throw new Error(result.stderr || `git exited ${result.code}`);
|
||||
return result.stdout.trim();
|
||||
} catch (error) {
|
||||
@@ -70736,7 +70736,8 @@ async function loadExtensions(harness2, registry3, storage2, models2, context, b
|
||||
const publish2 = async () => {
|
||||
await harness2.commit(async (tx) => Object.assign(await tx.doc(ExtensionState), state2), context);
|
||||
ops2.op_extension_status(JSON.stringify(state2));
|
||||
await fs2.write("/src/sausage/.git/sausage-last-good", state2.good ?? "");
|
||||
await fs2.write("/src/sausage/.git/sausage-last-good.tmp", state2.good ?? "");
|
||||
await fs2.rename("/src/sausage/.git/sausage-last-good.tmp", "/src/sausage/.git/sausage-last-good");
|
||||
};
|
||||
const dispose = async (entries2) => {
|
||||
for (const facet of entries2) for (const cleanup of facet.close) {
|
||||
@@ -70826,8 +70827,19 @@ async function loadExtensions(harness2, registry3, storage2, models2, context, b
|
||||
try {
|
||||
await fs2.stat(snapshot);
|
||||
} catch {
|
||||
await loaderGit("worktree", "prune");
|
||||
await loaderGit("worktree", "add", "--detach", snapshot, sha);
|
||||
const staging = `${snapshot}.tmp`, archive = `${snapshot}.tar`;
|
||||
try {
|
||||
await fs2.mkdir(staging);
|
||||
} catch (error) {
|
||||
if (error.code !== "already_exists") throw error;
|
||||
}
|
||||
if (await loaderGit("ls-tree", "--name-only", sha, "extensions")) {
|
||||
await loaderGit("archive", "--format=tar", `--output=${archive}`, sha, "extensions");
|
||||
const result = await exec(["tar", "-xf", archive, "-C", staging], { cwd: repo, signal: commands.signal });
|
||||
if (result.code) throw new Error(`tar: ${result.stderr || result.code}`);
|
||||
await fs2.remove(archive);
|
||||
}
|
||||
await fs2.rename(staging, snapshot);
|
||||
}
|
||||
const modules = [];
|
||||
for (const entry2 of await fs2.list(`${snapshot}/extensions`).catch((error) => {
|
||||
|
||||
@@ -31486,7 +31486,7 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
const loaderGit = async (...args) => {
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
try {
|
||||
const result = await exec(["git", ...args], { cwd: repo, timeout: 120, signal: commands.signal });
|
||||
const result = await exec(["git", ...args], { cwd: repo, signal: commands.signal });
|
||||
if (result.code) throw new Error(result.stderr || `git exited ${result.code}`);
|
||||
return result.stdout.trim();
|
||||
} catch (error) {
|
||||
@@ -31502,7 +31502,8 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
const publish2 = async () => {
|
||||
await harness2.commit(async (tx) => Object.assign(await tx.doc(ExtensionState), state), context);
|
||||
ops2.op_extension_status(JSON.stringify(state));
|
||||
await fs.write("/src/sausage/.git/sausage-last-good", state.good ?? "");
|
||||
await fs.write("/src/sausage/.git/sausage-last-good.tmp", state.good ?? "");
|
||||
await fs.rename("/src/sausage/.git/sausage-last-good.tmp", "/src/sausage/.git/sausage-last-good");
|
||||
};
|
||||
const dispose = async (entries2) => {
|
||||
for (const facet of entries2) for (const cleanup of facet.close) {
|
||||
@@ -31592,8 +31593,19 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
try {
|
||||
await fs.stat(snapshot);
|
||||
} catch {
|
||||
await loaderGit("worktree", "prune");
|
||||
await loaderGit("worktree", "add", "--detach", snapshot, sha);
|
||||
const staging = `${snapshot}.tmp`, archive = `${snapshot}.tar`;
|
||||
try {
|
||||
await fs.mkdir(staging);
|
||||
} catch (error) {
|
||||
if (error.code !== "already_exists") throw error;
|
||||
}
|
||||
if (await loaderGit("ls-tree", "--name-only", sha, "extensions")) {
|
||||
await loaderGit("archive", "--format=tar", `--output=${archive}`, sha, "extensions");
|
||||
const result = await exec(["tar", "-xf", archive, "-C", staging], { cwd: repo, signal: commands.signal });
|
||||
if (result.code) throw new Error(`tar: ${result.stderr || result.code}`);
|
||||
await fs.remove(archive);
|
||||
}
|
||||
await fs.rename(staging, snapshot);
|
||||
}
|
||||
const modules = [];
|
||||
for (const entry2 of await fs.list(`${snapshot}/extensions`).catch((error) => {
|
||||
|
||||
@@ -30546,7 +30546,7 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
const loaderGit = async (...args) => {
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
try {
|
||||
const result = await exec(["git", ...args], { cwd: repo, timeout: 120, signal: commands.signal });
|
||||
const result = await exec(["git", ...args], { cwd: repo, signal: commands.signal });
|
||||
if (result.code) throw new Error(result.stderr || `git exited ${result.code}`);
|
||||
return result.stdout.trim();
|
||||
} catch (error) {
|
||||
@@ -30562,7 +30562,8 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
const publish2 = async () => {
|
||||
await harness2.commit(async (tx) => Object.assign(await tx.doc(ExtensionState), state), context);
|
||||
ops2.op_extension_status(JSON.stringify(state));
|
||||
await fs.write("/src/sausage/.git/sausage-last-good", state.good ?? "");
|
||||
await fs.write("/src/sausage/.git/sausage-last-good.tmp", state.good ?? "");
|
||||
await fs.rename("/src/sausage/.git/sausage-last-good.tmp", "/src/sausage/.git/sausage-last-good");
|
||||
};
|
||||
const dispose = async (entries3) => {
|
||||
for (const facet of entries3) for (const cleanup of facet.close) {
|
||||
@@ -30652,8 +30653,19 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
try {
|
||||
await fs.stat(snapshot);
|
||||
} catch {
|
||||
await loaderGit("worktree", "prune");
|
||||
await loaderGit("worktree", "add", "--detach", snapshot, sha);
|
||||
const staging = `${snapshot}.tmp`, archive = `${snapshot}.tar`;
|
||||
try {
|
||||
await fs.mkdir(staging);
|
||||
} catch (error) {
|
||||
if (error.code !== "already_exists") throw error;
|
||||
}
|
||||
if (await loaderGit("ls-tree", "--name-only", sha, "extensions")) {
|
||||
await loaderGit("archive", "--format=tar", `--output=${archive}`, sha, "extensions");
|
||||
const result = await exec(["tar", "-xf", archive, "-C", staging], { cwd: repo, signal: commands.signal });
|
||||
if (result.code) throw new Error(`tar: ${result.stderr || result.code}`);
|
||||
await fs.remove(archive);
|
||||
}
|
||||
await fs.rename(staging, snapshot);
|
||||
}
|
||||
const modules = [];
|
||||
for (const entry2 of await fs.list(`${snapshot}/extensions`).catch((error) => {
|
||||
|
||||
@@ -31486,7 +31486,7 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
const loaderGit = async (...args) => {
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
try {
|
||||
const result = await exec(["git", ...args], { cwd: repo, timeout: 120, signal: commands.signal });
|
||||
const result = await exec(["git", ...args], { cwd: repo, signal: commands.signal });
|
||||
if (result.code) throw new Error(result.stderr || `git exited ${result.code}`);
|
||||
return result.stdout.trim();
|
||||
} catch (error) {
|
||||
@@ -31502,7 +31502,8 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
const publish2 = async () => {
|
||||
await harness2.commit(async (tx) => Object.assign(await tx.doc(ExtensionState), state), context);
|
||||
ops2.op_extension_status(JSON.stringify(state));
|
||||
await fs.write("/src/sausage/.git/sausage-last-good", state.good ?? "");
|
||||
await fs.write("/src/sausage/.git/sausage-last-good.tmp", state.good ?? "");
|
||||
await fs.rename("/src/sausage/.git/sausage-last-good.tmp", "/src/sausage/.git/sausage-last-good");
|
||||
};
|
||||
const dispose = async (entries2) => {
|
||||
for (const facet of entries2) for (const cleanup of facet.close) {
|
||||
@@ -31592,8 +31593,19 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
try {
|
||||
await fs.stat(snapshot);
|
||||
} catch {
|
||||
await loaderGit("worktree", "prune");
|
||||
await loaderGit("worktree", "add", "--detach", snapshot, sha);
|
||||
const staging = `${snapshot}.tmp`, archive = `${snapshot}.tar`;
|
||||
try {
|
||||
await fs.mkdir(staging);
|
||||
} catch (error) {
|
||||
if (error.code !== "already_exists") throw error;
|
||||
}
|
||||
if (await loaderGit("ls-tree", "--name-only", sha, "extensions")) {
|
||||
await loaderGit("archive", "--format=tar", `--output=${archive}`, sha, "extensions");
|
||||
const result = await exec(["tar", "-xf", archive, "-C", staging], { cwd: repo, signal: commands.signal });
|
||||
if (result.code) throw new Error(`tar: ${result.stderr || result.code}`);
|
||||
await fs.remove(archive);
|
||||
}
|
||||
await fs.rename(staging, snapshot);
|
||||
}
|
||||
const modules = [];
|
||||
for (const entry2 of await fs.list(`${snapshot}/extensions`).catch((error) => {
|
||||
|
||||
@@ -31486,7 +31486,7 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
const loaderGit = async (...args) => {
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
try {
|
||||
const result = await exec(["git", ...args], { cwd: repo, timeout: 120, signal: commands.signal });
|
||||
const result = await exec(["git", ...args], { cwd: repo, signal: commands.signal });
|
||||
if (result.code) throw new Error(result.stderr || `git exited ${result.code}`);
|
||||
return result.stdout.trim();
|
||||
} catch (error) {
|
||||
@@ -31502,7 +31502,8 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
const publish2 = async () => {
|
||||
await harness2.commit(async (tx) => Object.assign(await tx.doc(ExtensionState), state), context);
|
||||
ops2.op_extension_status(JSON.stringify(state));
|
||||
await fs.write("/src/sausage/.git/sausage-last-good", state.good ?? "");
|
||||
await fs.write("/src/sausage/.git/sausage-last-good.tmp", state.good ?? "");
|
||||
await fs.rename("/src/sausage/.git/sausage-last-good.tmp", "/src/sausage/.git/sausage-last-good");
|
||||
};
|
||||
const dispose = async (entries2) => {
|
||||
for (const facet of entries2) for (const cleanup of facet.close) {
|
||||
@@ -31592,8 +31593,19 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
try {
|
||||
await fs.stat(snapshot);
|
||||
} catch {
|
||||
await loaderGit("worktree", "prune");
|
||||
await loaderGit("worktree", "add", "--detach", snapshot, sha);
|
||||
const staging = `${snapshot}.tmp`, archive = `${snapshot}.tar`;
|
||||
try {
|
||||
await fs.mkdir(staging);
|
||||
} catch (error) {
|
||||
if (error.code !== "already_exists") throw error;
|
||||
}
|
||||
if (await loaderGit("ls-tree", "--name-only", sha, "extensions")) {
|
||||
await loaderGit("archive", "--format=tar", `--output=${archive}`, sha, "extensions");
|
||||
const result = await exec(["tar", "-xf", archive, "-C", staging], { cwd: repo, signal: commands.signal });
|
||||
if (result.code) throw new Error(`tar: ${result.stderr || result.code}`);
|
||||
await fs.remove(archive);
|
||||
}
|
||||
await fs.rename(staging, snapshot);
|
||||
}
|
||||
const modules = [];
|
||||
for (const entry2 of await fs.list(`${snapshot}/extensions`).catch((error) => {
|
||||
@@ -31720,6 +31732,9 @@ var harness = await Harness.open(storage, { models, registry, settings: { get ex
|
||||
} } }, BACKGROUND_CONTEXT);
|
||||
var root = await harness.root(BACKGROUND_CONTEXT, { agent: { model: { provider: "faux", modelId: "faux-1" } } });
|
||||
globalThis.shared = { gate: Promise.withResolvers(), started: false, wait: false };
|
||||
var markerWrite = fs.write;
|
||||
var markerBlocked = Promise.withResolvers();
|
||||
var markerRelease = Promise.withResolvers();
|
||||
var loader = await loadExtensions(harness, registry, storage, models, BACKGROUND_CONTEXT);
|
||||
harness.resume();
|
||||
var turn = async () => {
|
||||
@@ -31783,15 +31798,30 @@ return defineExtension({name:'demo',tools:[defineTool({name:'demo',description:'
|
||||
globalThis.shared.gate.resolve();
|
||||
assert((await running).includes("result A"), "Old call must finish on A");
|
||||
assert((await turn()).includes("result C"), "Next call must use C");
|
||||
await loader.reload();
|
||||
fs.write = async (path, text) => {
|
||||
if (path.includes("sausage-last-good")) {
|
||||
fs.write = markerWrite;
|
||||
await markerWrite(path, "");
|
||||
markerBlocked.resolve();
|
||||
await markerRelease.promise;
|
||||
}
|
||||
return markerWrite(path, text);
|
||||
};
|
||||
await git("/src/sausage", "update-ref", "refs/heads/main", b);
|
||||
await until2(() => loader.state().current === b && !!loader.state().error);
|
||||
assert((await exec(["extensions-revert"], { cwd: "/" })).code === 0, "Revert command");
|
||||
await markerBlocked.promise;
|
||||
const reverted = await exec(["extensions-revert"], { cwd: "/" });
|
||||
markerRelease.resolve();
|
||||
assert(reverted.code === 0, `Revert command: ${JSON.stringify(reverted)}`);
|
||||
await until2(() => loader.state().current === c && !loader.state().error);
|
||||
await git("/src/sausage", "update-ref", "refs/heads/main", b);
|
||||
await until2(() => loader.state().current === b && !!loader.state().error);
|
||||
await fs.write("/src/sausage/good.txt", c);
|
||||
}
|
||||
} finally {
|
||||
markerRelease.resolve();
|
||||
fs.write = markerWrite;
|
||||
globalThis.shared.gate.resolve();
|
||||
await loader.close();
|
||||
await harness.close(BACKGROUND_CONTEXT);
|
||||
|
||||
@@ -12,6 +12,7 @@ const registry=createRegistry(), storage=await openStorage(globalThis.storagePat
|
||||
const harness=await Harness.open(storage,{models,registry,settings:{get extensions(){return registry.snapshot().installed();}}},context);
|
||||
const root=await harness.root(context,{agent:{model:{provider:'faux',modelId:'faux-1'}}});
|
||||
globalThis.shared={gate:Promise.withResolvers(),started:false,wait:false};
|
||||
const markerWrite=fs.write, markerBlocked=Promise.withResolvers(), markerRelease=Promise.withResolvers();
|
||||
const loader=await loadExtensions(harness,registry,storage,models,context);
|
||||
harness.resume();
|
||||
const turn = async()=> {
|
||||
@@ -67,12 +68,24 @@ return defineExtension({name:'demo',tools:[defineTool({name:'demo',description:'
|
||||
globalThis.shared.wait=false;globalThis.shared.gate.resolve();
|
||||
assert((await running).includes('result A'),'Old call must finish on A');
|
||||
assert((await turn()).includes('result C'),'Next call must use C');
|
||||
await loader.reload();
|
||||
// Suspend a marker write after truncation, as the asynchronous VFS write can do under load.
|
||||
fs.write=async(path,text)=>{
|
||||
if(path.includes('sausage-last-good')) {
|
||||
fs.write=markerWrite;
|
||||
await markerWrite(path,'');markerBlocked.resolve();await markerRelease.promise;
|
||||
}
|
||||
return markerWrite(path,text);
|
||||
};
|
||||
await git('/src/sausage','update-ref','refs/heads/main',b);
|
||||
await until(()=>loader.state().current===b && !!loader.state().error);
|
||||
assert((await exec(['extensions-revert'],{cwd:'/'})).code===0,'Revert command');
|
||||
await markerBlocked.promise;
|
||||
const reverted=await exec(['extensions-revert'],{cwd:'/'});
|
||||
markerRelease.resolve();
|
||||
assert(reverted.code===0,`Revert command: ${JSON.stringify(reverted)}`);
|
||||
await until(()=>loader.state().current===c && !loader.state().error);
|
||||
await git('/src/sausage','update-ref','refs/heads/main',b);
|
||||
await until(()=>loader.state().current===b && !!loader.state().error);
|
||||
await fs.write('/src/sausage/good.txt',c);
|
||||
}
|
||||
} finally {globalThis.shared.gate.resolve();await loader.close();await harness.close(context);}
|
||||
} finally {markerRelease.resolve();fs.write=markerWrite;globalThis.shared.gate.resolve();await loader.close();await harness.close(context);}
|
||||
|
||||
+16
-4
@@ -84,11 +84,11 @@ export async function loadExtensions(harness:any, registry:any, storage:any, mod
|
||||
let settings:any = {};
|
||||
let active = true;
|
||||
const commands = new AbortController();
|
||||
// Snapshot checkout copies the source tree. The tool helper's 30 seconds is too short under load.
|
||||
// Loader commands have no wall-clock deadline. close() cancels them, even under machine load.
|
||||
const loaderGit = async (...args:string[]) => {
|
||||
for (let attempt=0;;attempt++) {
|
||||
try {
|
||||
const result = await exec(['git',...args],{cwd:repo,timeout:120,signal:commands.signal});
|
||||
const result = await exec(['git',...args],{cwd:repo,signal:commands.signal});
|
||||
if(result.code)throw new Error(result.stderr || `git exited ${result.code}`);
|
||||
return result.stdout.trim();
|
||||
} catch(error) {
|
||||
@@ -104,7 +104,8 @@ export async function loadExtensions(harness:any, registry:any, storage:any, mod
|
||||
const publish = async () => {
|
||||
await harness.commit(async (tx:any)=>Object.assign(await tx.doc(ExtensionState),state),context);
|
||||
ops.op_extension_status(JSON.stringify(state));
|
||||
await fs.write('/src/sausage/.git/sausage-last-good',state.good ?? '');
|
||||
await fs.write('/src/sausage/.git/sausage-last-good.tmp',state.good ?? '');
|
||||
await fs.rename('/src/sausage/.git/sausage-last-good.tmp','/src/sausage/.git/sausage-last-good');
|
||||
};
|
||||
const dispose = async (entries:any[]) => { for(const facet of entries) for(const cleanup of facet.close) { try {await cleanup();} catch(error) {ops.op_output(`Extension cleanup: ${String(error)}\n`);} } };
|
||||
const scope = (name:string, staged:Map<string,any>) => {
|
||||
@@ -151,7 +152,18 @@ export async function loadExtensions(harness:any, registry:any, storage:any, mod
|
||||
try {
|
||||
const snapshot = `/run/sausage/extensions/${sha}`;
|
||||
for(const path of ['/run/sausage','/run/sausage/extensions']) {try {await fs.mkdir(path);} catch(error) {if((error as any).code!=='already_exists')throw error;} }
|
||||
try {await fs.stat(snapshot);} catch {await loaderGit('worktree','prune');await loaderGit('worktree','add','--detach',snapshot,sha);}
|
||||
try {await fs.stat(snapshot);} catch {
|
||||
// Only publish a complete subtree. A cancelled extraction can leave its staging directory.
|
||||
const staging=`${snapshot}.tmp`, archive=`${snapshot}.tar`;
|
||||
try {await fs.mkdir(staging);} catch(error) {if((error as any).code!=='already_exists')throw error;}
|
||||
if(await loaderGit('ls-tree','--name-only',sha,'extensions')) {
|
||||
await loaderGit('archive','--format=tar',`--output=${archive}`,sha,'extensions');
|
||||
const result=await exec(['tar','-xf',archive,'-C',staging],{cwd:repo,signal:commands.signal});
|
||||
if(result.code)throw new Error(`tar: ${result.stderr || result.code}`);
|
||||
await fs.remove(archive);
|
||||
}
|
||||
await fs.rename(staging,snapshot);
|
||||
}
|
||||
const modules:any[] = [];
|
||||
for(const entry of await fs.list(`${snapshot}/extensions`).catch(error=>{if(error.code==='not_found')return [];throw error;})) {
|
||||
if(entry.kind!=='directory')continue;
|
||||
|
||||
@@ -32712,7 +32712,7 @@ async function loadExtensions(harness, registry, storage, models, context, built
|
||||
const loaderGit = async (...args) => {
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
try {
|
||||
const result = await exec(["git", ...args], { cwd: repo, timeout: 120, signal: commands.signal });
|
||||
const result = await exec(["git", ...args], { cwd: repo, signal: commands.signal });
|
||||
if (result.code) throw new Error(result.stderr || `git exited ${result.code}`);
|
||||
return result.stdout.trim();
|
||||
} catch (error) {
|
||||
@@ -32728,7 +32728,8 @@ async function loadExtensions(harness, registry, storage, models, context, built
|
||||
const publish2 = async () => {
|
||||
await harness.commit(async (tx) => Object.assign(await tx.doc(ExtensionState), state), context);
|
||||
ops2.op_extension_status(JSON.stringify(state));
|
||||
await fs.write("/src/sausage/.git/sausage-last-good", state.good ?? "");
|
||||
await fs.write("/src/sausage/.git/sausage-last-good.tmp", state.good ?? "");
|
||||
await fs.rename("/src/sausage/.git/sausage-last-good.tmp", "/src/sausage/.git/sausage-last-good");
|
||||
};
|
||||
const dispose = async (entries2) => {
|
||||
for (const facet of entries2) for (const cleanup of facet.close) {
|
||||
@@ -32818,8 +32819,19 @@ async function loadExtensions(harness, registry, storage, models, context, built
|
||||
try {
|
||||
await fs.stat(snapshot);
|
||||
} catch {
|
||||
await loaderGit("worktree", "prune");
|
||||
await loaderGit("worktree", "add", "--detach", snapshot, sha);
|
||||
const staging = `${snapshot}.tmp`, archive = `${snapshot}.tar`;
|
||||
try {
|
||||
await fs.mkdir(staging);
|
||||
} catch (error) {
|
||||
if (error.code !== "already_exists") throw error;
|
||||
}
|
||||
if (await loaderGit("ls-tree", "--name-only", sha, "extensions")) {
|
||||
await loaderGit("archive", "--format=tar", `--output=${archive}`, sha, "extensions");
|
||||
const result = await exec(["tar", "-xf", archive, "-C", staging], { cwd: repo, signal: commands.signal });
|
||||
if (result.code) throw new Error(`tar: ${result.stderr || result.code}`);
|
||||
await fs.remove(archive);
|
||||
}
|
||||
await fs.rename(staging, snapshot);
|
||||
}
|
||||
const modules = [];
|
||||
for (const entry2 of await fs.list(`${snapshot}/extensions`).catch((error) => {
|
||||
|
||||
@@ -31486,7 +31486,7 @@ async function loadExtensions(harness2, registry, storage, models, context, buil
|
||||
const loaderGit = async (...args) => {
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
try {
|
||||
const result = await exec(["git", ...args], { cwd: repo, timeout: 120, signal: commands.signal });
|
||||
const result = await exec(["git", ...args], { cwd: repo, signal: commands.signal });
|
||||
if (result.code) throw new Error(result.stderr || `git exited ${result.code}`);
|
||||
return result.stdout.trim();
|
||||
} catch (error) {
|
||||
@@ -31502,7 +31502,8 @@ async function loadExtensions(harness2, registry, storage, models, context, buil
|
||||
const publish2 = async () => {
|
||||
await harness2.commit(async (tx) => Object.assign(await tx.doc(ExtensionState), state), context);
|
||||
ops2.op_extension_status(JSON.stringify(state));
|
||||
await fs.write("/src/sausage/.git/sausage-last-good", state.good ?? "");
|
||||
await fs.write("/src/sausage/.git/sausage-last-good.tmp", state.good ?? "");
|
||||
await fs.rename("/src/sausage/.git/sausage-last-good.tmp", "/src/sausage/.git/sausage-last-good");
|
||||
};
|
||||
const dispose = async (entries2) => {
|
||||
for (const facet of entries2) for (const cleanup of facet.close) {
|
||||
@@ -31592,8 +31593,19 @@ async function loadExtensions(harness2, registry, storage, models, context, buil
|
||||
try {
|
||||
await fs.stat(snapshot);
|
||||
} catch {
|
||||
await loaderGit("worktree", "prune");
|
||||
await loaderGit("worktree", "add", "--detach", snapshot, sha);
|
||||
const staging = `${snapshot}.tmp`, archive = `${snapshot}.tar`;
|
||||
try {
|
||||
await fs.mkdir(staging);
|
||||
} catch (error) {
|
||||
if (error.code !== "already_exists") throw error;
|
||||
}
|
||||
if (await loaderGit("ls-tree", "--name-only", sha, "extensions")) {
|
||||
await loaderGit("archive", "--format=tar", `--output=${archive}`, sha, "extensions");
|
||||
const result = await exec(["tar", "-xf", archive, "-C", staging], { cwd: repo, signal: commands.signal });
|
||||
if (result.code) throw new Error(`tar: ${result.stderr || result.code}`);
|
||||
await fs.remove(archive);
|
||||
}
|
||||
await fs.rename(staging, snapshot);
|
||||
}
|
||||
const modules = [];
|
||||
for (const entry2 of await fs.list(`${snapshot}/extensions`).catch((error) => {
|
||||
|
||||
@@ -30546,7 +30546,7 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
const loaderGit = async (...args) => {
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
try {
|
||||
const result = await exec(["git", ...args], { cwd: repo, timeout: 120, signal: commands.signal });
|
||||
const result = await exec(["git", ...args], { cwd: repo, signal: commands.signal });
|
||||
if (result.code) throw new Error(result.stderr || `git exited ${result.code}`);
|
||||
return result.stdout.trim();
|
||||
} catch (error) {
|
||||
@@ -30562,7 +30562,8 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
const publish2 = async () => {
|
||||
await harness2.commit(async (tx) => Object.assign(await tx.doc(ExtensionState), state), context);
|
||||
ops2.op_extension_status(JSON.stringify(state));
|
||||
await fs.write("/src/sausage/.git/sausage-last-good", state.good ?? "");
|
||||
await fs.write("/src/sausage/.git/sausage-last-good.tmp", state.good ?? "");
|
||||
await fs.rename("/src/sausage/.git/sausage-last-good.tmp", "/src/sausage/.git/sausage-last-good");
|
||||
};
|
||||
const dispose = async (entries3) => {
|
||||
for (const facet of entries3) for (const cleanup of facet.close) {
|
||||
@@ -30652,8 +30653,19 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
try {
|
||||
await fs.stat(snapshot2);
|
||||
} catch {
|
||||
await loaderGit("worktree", "prune");
|
||||
await loaderGit("worktree", "add", "--detach", snapshot2, sha);
|
||||
const staging = `${snapshot2}.tmp`, archive = `${snapshot2}.tar`;
|
||||
try {
|
||||
await fs.mkdir(staging);
|
||||
} catch (error) {
|
||||
if (error.code !== "already_exists") throw error;
|
||||
}
|
||||
if (await loaderGit("ls-tree", "--name-only", sha, "extensions")) {
|
||||
await loaderGit("archive", "--format=tar", `--output=${archive}`, sha, "extensions");
|
||||
const result = await exec(["tar", "-xf", archive, "-C", staging], { cwd: repo, signal: commands.signal });
|
||||
if (result.code) throw new Error(`tar: ${result.stderr || result.code}`);
|
||||
await fs.remove(archive);
|
||||
}
|
||||
await fs.rename(staging, snapshot2);
|
||||
}
|
||||
const modules = [];
|
||||
for (const entry2 of await fs.list(`${snapshot2}/extensions`).catch((error) => {
|
||||
|
||||
@@ -30546,7 +30546,7 @@ async function loadExtensions(harness, registry, storage, models, context, built
|
||||
const loaderGit = async (...args) => {
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
try {
|
||||
const result = await exec(["git", ...args], { cwd: repo, timeout: 120, signal: commands.signal });
|
||||
const result = await exec(["git", ...args], { cwd: repo, signal: commands.signal });
|
||||
if (result.code) throw new Error(result.stderr || `git exited ${result.code}`);
|
||||
return result.stdout.trim();
|
||||
} catch (error) {
|
||||
@@ -30562,7 +30562,8 @@ async function loadExtensions(harness, registry, storage, models, context, built
|
||||
const publish2 = async () => {
|
||||
await harness.commit(async (tx) => Object.assign(await tx.doc(ExtensionState), state), context);
|
||||
ops2.op_extension_status(JSON.stringify(state));
|
||||
await fs.write("/src/sausage/.git/sausage-last-good", state.good ?? "");
|
||||
await fs.write("/src/sausage/.git/sausage-last-good.tmp", state.good ?? "");
|
||||
await fs.rename("/src/sausage/.git/sausage-last-good.tmp", "/src/sausage/.git/sausage-last-good");
|
||||
};
|
||||
const dispose = async (entries2) => {
|
||||
for (const facet of entries2) for (const cleanup of facet.close) {
|
||||
@@ -30652,8 +30653,19 @@ async function loadExtensions(harness, registry, storage, models, context, built
|
||||
try {
|
||||
await fs.stat(snapshot);
|
||||
} catch {
|
||||
await loaderGit("worktree", "prune");
|
||||
await loaderGit("worktree", "add", "--detach", snapshot, sha);
|
||||
const staging = `${snapshot}.tmp`, archive = `${snapshot}.tar`;
|
||||
try {
|
||||
await fs.mkdir(staging);
|
||||
} catch (error) {
|
||||
if (error.code !== "already_exists") throw error;
|
||||
}
|
||||
if (await loaderGit("ls-tree", "--name-only", sha, "extensions")) {
|
||||
await loaderGit("archive", "--format=tar", `--output=${archive}`, sha, "extensions");
|
||||
const result = await exec(["tar", "-xf", archive, "-C", staging], { cwd: repo, signal: commands.signal });
|
||||
if (result.code) throw new Error(`tar: ${result.stderr || result.code}`);
|
||||
await fs.remove(archive);
|
||||
}
|
||||
await fs.rename(staging, snapshot);
|
||||
}
|
||||
const modules = [];
|
||||
for (const entry2 of await fs.list(`${snapshot}/extensions`).catch((error) => {
|
||||
|
||||
@@ -31486,7 +31486,7 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
const loaderGit = async (...args) => {
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
try {
|
||||
const result = await exec(["git", ...args], { cwd: repo, timeout: 120, signal: commands.signal });
|
||||
const result = await exec(["git", ...args], { cwd: repo, signal: commands.signal });
|
||||
if (result.code) throw new Error(result.stderr || `git exited ${result.code}`);
|
||||
return result.stdout.trim();
|
||||
} catch (error) {
|
||||
@@ -31502,7 +31502,8 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
const publish2 = async () => {
|
||||
await harness2.commit(async (tx) => Object.assign(await tx.doc(ExtensionState), state), context);
|
||||
ops2.op_extension_status(JSON.stringify(state));
|
||||
await fs.write("/src/sausage/.git/sausage-last-good", state.good ?? "");
|
||||
await fs.write("/src/sausage/.git/sausage-last-good.tmp", state.good ?? "");
|
||||
await fs.rename("/src/sausage/.git/sausage-last-good.tmp", "/src/sausage/.git/sausage-last-good");
|
||||
};
|
||||
const dispose = async (entries2) => {
|
||||
for (const facet of entries2) for (const cleanup of facet.close) {
|
||||
@@ -31592,8 +31593,19 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
try {
|
||||
await fs.stat(snapshot);
|
||||
} catch {
|
||||
await loaderGit("worktree", "prune");
|
||||
await loaderGit("worktree", "add", "--detach", snapshot, sha);
|
||||
const staging = `${snapshot}.tmp`, archive = `${snapshot}.tar`;
|
||||
try {
|
||||
await fs.mkdir(staging);
|
||||
} catch (error) {
|
||||
if (error.code !== "already_exists") throw error;
|
||||
}
|
||||
if (await loaderGit("ls-tree", "--name-only", sha, "extensions")) {
|
||||
await loaderGit("archive", "--format=tar", `--output=${archive}`, sha, "extensions");
|
||||
const result = await exec(["tar", "-xf", archive, "-C", staging], { cwd: repo, signal: commands.signal });
|
||||
if (result.code) throw new Error(`tar: ${result.stderr || result.code}`);
|
||||
await fs.remove(archive);
|
||||
}
|
||||
await fs.rename(staging, snapshot);
|
||||
}
|
||||
const modules = [];
|
||||
for (const entry2 of await fs.list(`${snapshot}/extensions`).catch((error) => {
|
||||
|
||||
@@ -30546,7 +30546,7 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
const loaderGit = async (...args) => {
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
try {
|
||||
const result = await exec(["git", ...args], { cwd: repo, timeout: 120, signal: commands.signal });
|
||||
const result = await exec(["git", ...args], { cwd: repo, signal: commands.signal });
|
||||
if (result.code) throw new Error(result.stderr || `git exited ${result.code}`);
|
||||
return result.stdout.trim();
|
||||
} catch (error) {
|
||||
@@ -30562,7 +30562,8 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
const publish2 = async () => {
|
||||
await harness2.commit(async (tx) => Object.assign(await tx.doc(ExtensionState), state), context);
|
||||
ops2.op_extension_status(JSON.stringify(state));
|
||||
await fs.write("/src/sausage/.git/sausage-last-good", state.good ?? "");
|
||||
await fs.write("/src/sausage/.git/sausage-last-good.tmp", state.good ?? "");
|
||||
await fs.rename("/src/sausage/.git/sausage-last-good.tmp", "/src/sausage/.git/sausage-last-good");
|
||||
};
|
||||
const dispose = async (entries2) => {
|
||||
for (const facet of entries2) for (const cleanup of facet.close) {
|
||||
@@ -30652,8 +30653,19 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
try {
|
||||
await fs.stat(snapshot2);
|
||||
} catch {
|
||||
await loaderGit("worktree", "prune");
|
||||
await loaderGit("worktree", "add", "--detach", snapshot2, sha);
|
||||
const staging = `${snapshot2}.tmp`, archive = `${snapshot2}.tar`;
|
||||
try {
|
||||
await fs.mkdir(staging);
|
||||
} catch (error) {
|
||||
if (error.code !== "already_exists") throw error;
|
||||
}
|
||||
if (await loaderGit("ls-tree", "--name-only", sha, "extensions")) {
|
||||
await loaderGit("archive", "--format=tar", `--output=${archive}`, sha, "extensions");
|
||||
const result = await exec(["tar", "-xf", archive, "-C", staging], { cwd: repo, signal: commands.signal });
|
||||
if (result.code) throw new Error(`tar: ${result.stderr || result.code}`);
|
||||
await fs.remove(archive);
|
||||
}
|
||||
await fs.rename(staging, snapshot2);
|
||||
}
|
||||
const modules = [];
|
||||
for (const entry2 of await fs.list(`${snapshot2}/extensions`).catch((error) => {
|
||||
|
||||
@@ -30546,7 +30546,7 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
const loaderGit = async (...args) => {
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
try {
|
||||
const result = await exec(["git", ...args], { cwd: repo, timeout: 120, signal: commands.signal });
|
||||
const result = await exec(["git", ...args], { cwd: repo, signal: commands.signal });
|
||||
if (result.code) throw new Error(result.stderr || `git exited ${result.code}`);
|
||||
return result.stdout.trim();
|
||||
} catch (error) {
|
||||
@@ -30562,7 +30562,8 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
const publish2 = async () => {
|
||||
await harness2.commit(async (tx) => Object.assign(await tx.doc(ExtensionState), state), context);
|
||||
ops2.op_extension_status(JSON.stringify(state));
|
||||
await fs.write("/src/sausage/.git/sausage-last-good", state.good ?? "");
|
||||
await fs.write("/src/sausage/.git/sausage-last-good.tmp", state.good ?? "");
|
||||
await fs.rename("/src/sausage/.git/sausage-last-good.tmp", "/src/sausage/.git/sausage-last-good");
|
||||
};
|
||||
const dispose = async (entries2) => {
|
||||
for (const facet of entries2) for (const cleanup of facet.close) {
|
||||
@@ -30652,8 +30653,19 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
try {
|
||||
await fs.stat(snapshot);
|
||||
} catch {
|
||||
await loaderGit("worktree", "prune");
|
||||
await loaderGit("worktree", "add", "--detach", snapshot, sha2);
|
||||
const staging = `${snapshot}.tmp`, archive = `${snapshot}.tar`;
|
||||
try {
|
||||
await fs.mkdir(staging);
|
||||
} catch (error) {
|
||||
if (error.code !== "already_exists") throw error;
|
||||
}
|
||||
if (await loaderGit("ls-tree", "--name-only", sha2, "extensions")) {
|
||||
await loaderGit("archive", "--format=tar", `--output=${archive}`, sha2, "extensions");
|
||||
const result = await exec(["tar", "-xf", archive, "-C", staging], { cwd: repo, signal: commands.signal });
|
||||
if (result.code) throw new Error(`tar: ${result.stderr || result.code}`);
|
||||
await fs.remove(archive);
|
||||
}
|
||||
await fs.rename(staging, snapshot);
|
||||
}
|
||||
const modules = [];
|
||||
for (const entry2 of await fs.list(`${snapshot}/extensions`).catch((error) => {
|
||||
|
||||
@@ -30546,7 +30546,7 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
const loaderGit = async (...args) => {
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
try {
|
||||
const result = await exec(["git", ...args], { cwd: repo, timeout: 120, signal: commands.signal });
|
||||
const result = await exec(["git", ...args], { cwd: repo, signal: commands.signal });
|
||||
if (result.code) throw new Error(result.stderr || `git exited ${result.code}`);
|
||||
return result.stdout.trim();
|
||||
} catch (error) {
|
||||
@@ -30562,7 +30562,8 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
const publish2 = async () => {
|
||||
await harness2.commit(async (tx) => Object.assign(await tx.doc(ExtensionState), state), context);
|
||||
ops2.op_extension_status(JSON.stringify(state));
|
||||
await fs.write("/src/sausage/.git/sausage-last-good", state.good ?? "");
|
||||
await fs.write("/src/sausage/.git/sausage-last-good.tmp", state.good ?? "");
|
||||
await fs.rename("/src/sausage/.git/sausage-last-good.tmp", "/src/sausage/.git/sausage-last-good");
|
||||
};
|
||||
const dispose = async (entries2) => {
|
||||
for (const facet of entries2) for (const cleanup of facet.close) {
|
||||
@@ -30652,8 +30653,19 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
|
||||
try {
|
||||
await fs.stat(snapshot);
|
||||
} catch {
|
||||
await loaderGit("worktree", "prune");
|
||||
await loaderGit("worktree", "add", "--detach", snapshot, sha);
|
||||
const staging = `${snapshot}.tmp`, archive = `${snapshot}.tar`;
|
||||
try {
|
||||
await fs.mkdir(staging);
|
||||
} catch (error) {
|
||||
if (error.code !== "already_exists") throw error;
|
||||
}
|
||||
if (await loaderGit("ls-tree", "--name-only", sha, "extensions")) {
|
||||
await loaderGit("archive", "--format=tar", `--output=${archive}`, sha, "extensions");
|
||||
const result = await exec(["tar", "-xf", archive, "-C", staging], { cwd: repo, signal: commands.signal });
|
||||
if (result.code) throw new Error(`tar: ${result.stderr || result.code}`);
|
||||
await fs.remove(archive);
|
||||
}
|
||||
await fs.rename(staging, snapshot);
|
||||
}
|
||||
const modules = [];
|
||||
for (const entry2 of await fs.list(`${snapshot}/extensions`).catch((error) => {
|
||||
|
||||
@@ -36,6 +36,8 @@ fn setup(directory: &Path) -> Result<()> {
|
||||
"git",
|
||||
"-p",
|
||||
"curl",
|
||||
"-p",
|
||||
"sausage-archive-tools",
|
||||
"--target",
|
||||
"wasm32-wasip2",
|
||||
"--target-dir"
|
||||
@@ -58,6 +60,7 @@ fn setup(directory: &Path) -> Result<()> {
|
||||
std::fs::create_dir_all(disk.join("bin"))?;
|
||||
for (source, name) in [
|
||||
("git.wasm", "git.wasm"),
|
||||
("tar.wasm", "tar.wasm"),
|
||||
("curl.wasm", "curl.wasm"),
|
||||
("brush.wasm", "sh.wasm"),
|
||||
] {
|
||||
|
||||
@@ -141,7 +141,15 @@ pub fn build_git() -> &'static Path {
|
||||
cwd_init();
|
||||
assert!(
|
||||
Command::new("cargo")
|
||||
.args(["build", "--release", "-p", "git", "--manifest-path"])
|
||||
.args([
|
||||
"build",
|
||||
"--release",
|
||||
"-p",
|
||||
"git",
|
||||
"-p",
|
||||
"sausage-archive-tools",
|
||||
"--manifest-path"
|
||||
])
|
||||
.arg(userland.join("Cargo.toml"))
|
||||
.args(["--target", "wasm32-wasip2", "--target-dir"])
|
||||
.arg(&target)
|
||||
|
||||
@@ -52,10 +52,12 @@ async fn core_extensions() -> anyhow::Result<()> {
|
||||
let world = World::open(dir.path())?;
|
||||
let disk = dir.path().join("world");
|
||||
std::fs::create_dir_all(disk.join("bin"))?;
|
||||
std::fs::copy(
|
||||
target.join("wasm32-wasip2/release/git.wasm"),
|
||||
disk.join("bin/git.wasm"),
|
||||
)?;
|
||||
for binary in ["git", "tar"] {
|
||||
std::fs::copy(
|
||||
target.join(format!("wasm32-wasip2/release/{binary}.wasm")),
|
||||
disk.join(format!("bin/{binary}.wasm")),
|
||||
)?;
|
||||
}
|
||||
let source = disk.join("src/sausage");
|
||||
for name in ["core", "models", "prompt", "conversations"] {
|
||||
copy_tree(
|
||||
|
||||
@@ -38,10 +38,12 @@ fn prepare(dir: &Path) -> anyhow::Result<()> {
|
||||
let source = Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
|
||||
let target = crate::common::build_git();
|
||||
std::fs::create_dir_all(disk.join("bin"))?;
|
||||
std::fs::copy(
|
||||
target.join("wasm32-wasip2/release/git.wasm"),
|
||||
disk.join("bin/git.wasm"),
|
||||
)?;
|
||||
for binary in ["git", "tar"] {
|
||||
std::fs::copy(
|
||||
target.join(format!("wasm32-wasip2/release/{binary}.wasm")),
|
||||
disk.join(format!("bin/{binary}.wasm")),
|
||||
)?;
|
||||
}
|
||||
copy_tree(&source.join("extensions"), &repo.join("extensions"))?;
|
||||
std::fs::create_dir_all(repo.join("extensions/probe"))?;
|
||||
std::fs::write(
|
||||
|
||||
@@ -95,8 +95,12 @@ async fn world_extensions() -> anyhow::Result<()> {
|
||||
let world = World::open(dir.path())?;
|
||||
let disk = dir.path().join("world");
|
||||
std::fs::create_dir_all(disk.join("bin"))?;
|
||||
let git = target.join("wasm32-wasip2/release/git.wasm");
|
||||
std::fs::copy(git, disk.join("bin/git.wasm"))?;
|
||||
for binary in ["git", "tar"] {
|
||||
std::fs::copy(
|
||||
target.join(format!("wasm32-wasip2/release/{binary}.wasm")),
|
||||
disk.join(format!("bin/{binary}.wasm")),
|
||||
)?;
|
||||
}
|
||||
let repo = disk.join("src/sausage");
|
||||
std::fs::create_dir_all(&repo)?;
|
||||
assert!(
|
||||
|
||||
+38
-13
@@ -22,7 +22,15 @@ impl Exec for Author {
|
||||
] {
|
||||
options.env.push((key.into(), value.into()));
|
||||
}
|
||||
self.0.exec(command, options, output).await
|
||||
let label = format!("{command:?}");
|
||||
let budget = options.timeout;
|
||||
let started = std::time::Instant::now();
|
||||
let result = self.0.exec(command, options, output).await;
|
||||
eprintln!(
|
||||
"Extension {label}: deadline {budget:?}, elapsed {:?}, result {result:?}",
|
||||
started.elapsed()
|
||||
);
|
||||
result
|
||||
}
|
||||
}
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
@@ -41,6 +49,8 @@ async fn commits_and_restart() -> anyhow::Result<()> {
|
||||
"git",
|
||||
"-p",
|
||||
"sausage-exec-tools",
|
||||
"-p",
|
||||
"sausage-archive-tools",
|
||||
"--manifest-path"
|
||||
])
|
||||
.arg(userland.join("Cargo.toml"))
|
||||
@@ -59,7 +69,7 @@ async fn commits_and_restart() -> anyhow::Result<()> {
|
||||
target.join("wasm32-wasip2/release/git.wasm"),
|
||||
disk.join("bin/git.wasm"),
|
||||
)?;
|
||||
for name in ["extensions-revert", "env"] {
|
||||
for name in ["extensions-revert", "env", "tar"] {
|
||||
std::fs::copy(
|
||||
target.join(format!("wasm32-wasip2/release/{name}.wasm")),
|
||||
disk.join(format!("bin/{name}.wasm")),
|
||||
@@ -113,7 +123,7 @@ async fn commits_and_restart() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Model command time without a wall-clock delay, including work longer than the old 30 second budget.
|
||||
/// Model loader commands without delays, and require cancellation instead of a wall-clock deadline.
|
||||
struct LoaderCommand {
|
||||
root: Arc<dyn sausage_vfs::Layer>,
|
||||
failures: usize,
|
||||
@@ -131,18 +141,21 @@ impl Exec for LoaderCommand {
|
||||
let Command::Argv(args) = command else {
|
||||
anyhow::bail!("unexpected shell")
|
||||
};
|
||||
assert!(
|
||||
options.timeout.is_none(),
|
||||
"Loader work must not have a wall-clock deadline"
|
||||
);
|
||||
if args[0] == "tar" {
|
||||
return Ok(0);
|
||||
}
|
||||
match args[1].as_str() {
|
||||
"rev-parse" => output(Stream::Stdout, b"new\n"),
|
||||
"worktree" if args[2] == "prune" => {}
|
||||
"worktree" if args[2] == "add" => {
|
||||
"ls-tree" => output(Stream::Stdout, b"extensions\n"),
|
||||
"archive" => {
|
||||
let attempt = self
|
||||
.attempts
|
||||
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
if options
|
||||
.timeout
|
||||
.is_some_and(|budget| budget < std::time::Duration::from_secs(31))
|
||||
|| (attempt < self.failures && self.timeout)
|
||||
{
|
||||
if attempt < self.failures && self.timeout {
|
||||
anyhow::bail!("command timed out");
|
||||
}
|
||||
if attempt < self.failures {
|
||||
@@ -159,9 +172,21 @@ impl Exec for LoaderCommand {
|
||||
"Retry must publish its error: {status}"
|
||||
);
|
||||
}
|
||||
let snapshot = &args[4];
|
||||
let archive = args[3].strip_prefix("--output=").unwrap();
|
||||
let file = self
|
||||
.root
|
||||
.open(
|
||||
archive,
|
||||
sausage_vfs::Open {
|
||||
write: true,
|
||||
create: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
file.write(0, vec![]).await?;
|
||||
let snapshot = format!("{}.tmp", archive.strip_suffix(".tar").unwrap());
|
||||
for path in [
|
||||
snapshot.clone(),
|
||||
format!("{snapshot}/extensions"),
|
||||
format!("{snapshot}/extensions/demo"),
|
||||
] {
|
||||
@@ -217,7 +242,7 @@ async fn loader_command(failures: usize, timeout: bool) -> anyhow::Result<()> {
|
||||
const testState = testLoader.state();
|
||||
if(testState.good !== '{expected}') throw new Error(JSON.stringify(testState));
|
||||
if((testState.error !== null) !== ({expected:?} === 'old')) throw new Error(JSON.stringify(testState));
|
||||
if(testState.error && !testState.error.includes('git worktree add')) throw new Error(testState.error);
|
||||
if(testState.error && !testState.error.includes('git archive')) throw new Error(testState.error);
|
||||
await testLoader.close();
|
||||
await testHarness.close(BACKGROUND_CONTEXT);
|
||||
"#
|
||||
|
||||
@@ -13,10 +13,12 @@ fn prepare(dir: &Path) -> anyhow::Result<()> {
|
||||
let source = Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
|
||||
let target = crate::common::build_git();
|
||||
std::fs::create_dir_all(disk.join("bin"))?;
|
||||
std::fs::copy(
|
||||
target.join("wasm32-wasip2/release/git.wasm"),
|
||||
disk.join("bin/git.wasm"),
|
||||
)?;
|
||||
for binary in ["git", "tar"] {
|
||||
std::fs::copy(
|
||||
target.join(format!("wasm32-wasip2/release/{binary}.wasm")),
|
||||
disk.join(format!("bin/{binary}.wasm")),
|
||||
)?;
|
||||
}
|
||||
crate::common::copy_tree(&source.join("extensions"), &repo.join("extensions"))?;
|
||||
std::fs::create_dir_all(repo.join("agents"))?;
|
||||
std::fs::create_dir_all(repo.join("extensions/probe"))?;
|
||||
|
||||
@@ -26,6 +26,8 @@ fn prepare(dir: &Path) -> anyhow::Result<()> {
|
||||
"git",
|
||||
"-p",
|
||||
"curl",
|
||||
"-p",
|
||||
"sausage-archive-tools",
|
||||
"--manifest-path"
|
||||
])
|
||||
.arg(userland.join("Cargo.toml"))
|
||||
@@ -57,10 +59,12 @@ fn prepare(dir: &Path) -> anyhow::Result<()> {
|
||||
target.join("wasm32-wasip2/release/harness-api-client.wasm"),
|
||||
disk.join("bin/harness-api-client.wasm"),
|
||||
)?;
|
||||
std::fs::copy(
|
||||
target.join("wasm32-wasip2/release/git.wasm"),
|
||||
disk.join("bin/git.wasm"),
|
||||
)?;
|
||||
for binary in ["git", "tar"] {
|
||||
std::fs::copy(
|
||||
target.join(format!("wasm32-wasip2/release/{binary}.wasm")),
|
||||
disk.join(format!("bin/{binary}.wasm")),
|
||||
)?;
|
||||
}
|
||||
std::fs::copy(
|
||||
target.join("wasm32-wasip2/release/curl.wasm"),
|
||||
disk.join("bin/curl.wasm"),
|
||||
|
||||
@@ -17,10 +17,12 @@ fn prepare(dir: &Path) -> anyhow::Result<()> {
|
||||
let source = Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
|
||||
let target = crate::common::build_git();
|
||||
std::fs::create_dir_all(disk.join("bin"))?;
|
||||
std::fs::copy(
|
||||
target.join("wasm32-wasip2/release/git.wasm"),
|
||||
disk.join("bin/git.wasm"),
|
||||
)?;
|
||||
for binary in ["git", "tar"] {
|
||||
std::fs::copy(
|
||||
target.join(format!("wasm32-wasip2/release/{binary}.wasm")),
|
||||
disk.join(format!("bin/{binary}.wasm")),
|
||||
)?;
|
||||
}
|
||||
for directory in [
|
||||
"extensions/subagents",
|
||||
"extensions/messages",
|
||||
|
||||
@@ -338,6 +338,8 @@ async fn bundled_extensions_load() -> anyhow::Result<()> {
|
||||
const bundleLoader = await loadExtensions(bundleHarness, bundleRegistry, bundleStorage,
|
||||
bundleModels, BACKGROUND_CONTEXT, [CodingTools2, CredentialRequests]);
|
||||
if (bundleLoader.state().error) throw new Error(bundleLoader.state().error);
|
||||
const snapshotEntries=await fs2.list(`/run/sausage/extensions/${{bundleLoader.state().good}}`);
|
||||
if(JSON.stringify(snapshotEntries.map(entry=>entry.name)) !== '["extensions"]') throw new Error(JSON.stringify(snapshotEntries));
|
||||
await bundleLoader.close();
|
||||
await bundleHarness.close(BACKGROUND_CONTEXT);
|
||||
"#
|
||||
|
||||
+6
-4
@@ -30,10 +30,12 @@ fn prepare(dir: &Path, all_extensions: bool) -> anyhow::Result<()> {
|
||||
let source = Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
|
||||
let target = crate::common::build_git();
|
||||
std::fs::create_dir_all(disk.join("bin"))?;
|
||||
std::fs::copy(
|
||||
target.join("wasm32-wasip2/release/git.wasm"),
|
||||
disk.join("bin/git.wasm"),
|
||||
)?;
|
||||
for binary in ["git", "tar"] {
|
||||
std::fs::copy(
|
||||
target.join(format!("wasm32-wasip2/release/{binary}.wasm")),
|
||||
disk.join(format!("bin/{binary}.wasm")),
|
||||
)?;
|
||||
}
|
||||
if !target.join("wasm32-wasip2/release/brush.wasm").exists() {
|
||||
anyhow::ensure!(
|
||||
crate::common::build_brush()?.success(),
|
||||
|
||||
+6
-1
@@ -24,7 +24,9 @@ fn prepare(dir: &Path) -> anyhow::Result<PathBuf> {
|
||||
.args(["rev-parse", "HEAD"])
|
||||
.output()?;
|
||||
ensure!(revision.status.success(), "fixture revision");
|
||||
if fs::read(bundle.join("revision")).ok().as_deref() != Some(revision.stdout.as_slice()) {
|
||||
if fs::read(bundle.join("revision")).ok().as_deref() != Some(revision.stdout.as_slice())
|
||||
|| !bundle.join("bin/tar.wasm").is_file()
|
||||
{
|
||||
if !userland.join("cwd-init/init.o").exists() {
|
||||
crate::common::cwd_init();
|
||||
}
|
||||
@@ -42,6 +44,8 @@ fn prepare(dir: &Path) -> anyhow::Result<PathBuf> {
|
||||
"undo",
|
||||
"-p",
|
||||
"sausage-coreutils",
|
||||
"-p",
|
||||
"sausage-archive-tools",
|
||||
]))?;
|
||||
ensure!(
|
||||
crate::common::build_brush()?.success(),
|
||||
@@ -53,6 +57,7 @@ fn prepare(dir: &Path) -> anyhow::Result<PathBuf> {
|
||||
("brush", "sh"),
|
||||
("brush", "bash"),
|
||||
("git", "git"),
|
||||
("tar", "tar"),
|
||||
("cargo", "cargo"),
|
||||
("box", "box"),
|
||||
("undo", "undo"),
|
||||
|
||||
@@ -10,10 +10,12 @@ async fn plan_todos_and_restart() -> anyhow::Result<()> {
|
||||
let disk = dir.path().join("world");
|
||||
let _world = World::open(dir.path())?;
|
||||
std::fs::create_dir_all(disk.join("bin"))?;
|
||||
std::fs::copy(
|
||||
target.join("wasm32-wasip2/release/git.wasm"),
|
||||
disk.join("bin/git.wasm"),
|
||||
)?;
|
||||
for binary in ["git", "tar"] {
|
||||
std::fs::copy(
|
||||
target.join(format!("wasm32-wasip2/release/{binary}.wasm")),
|
||||
disk.join(format!("bin/{binary}.wasm")),
|
||||
)?;
|
||||
}
|
||||
let repo = disk.join("src/sausage");
|
||||
std::fs::create_dir_all(&repo)?;
|
||||
assert!(
|
||||
|
||||
@@ -15,10 +15,12 @@ fn prepare(dir: &Path) -> anyhow::Result<()> {
|
||||
let source = Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
|
||||
let target = crate::common::build_git();
|
||||
std::fs::create_dir_all(disk.join("bin"))?;
|
||||
std::fs::copy(
|
||||
target.join("wasm32-wasip2/release/git.wasm"),
|
||||
disk.join("bin/git.wasm"),
|
||||
)?;
|
||||
for binary in ["git", "tar"] {
|
||||
std::fs::copy(
|
||||
target.join(format!("wasm32-wasip2/release/{binary}.wasm")),
|
||||
disk.join(format!("bin/{binary}.wasm")),
|
||||
)?;
|
||||
}
|
||||
for directory in [
|
||||
"extensions/subagents",
|
||||
"extensions/models",
|
||||
|
||||
@@ -17,10 +17,12 @@ fn prepare(dir: &Path) -> anyhow::Result<()> {
|
||||
let source = Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
|
||||
let target = crate::common::build_git();
|
||||
std::fs::create_dir_all(disk.join("bin"))?;
|
||||
std::fs::copy(
|
||||
target.join("wasm32-wasip2/release/git.wasm"),
|
||||
disk.join("bin/git.wasm"),
|
||||
)?;
|
||||
for binary in ["git", "tar"] {
|
||||
std::fs::copy(
|
||||
target.join(format!("wasm32-wasip2/release/{binary}.wasm")),
|
||||
disk.join(format!("bin/{binary}.wasm")),
|
||||
)?;
|
||||
}
|
||||
for directory in [
|
||||
"extensions/subagents",
|
||||
"extensions/messages",
|
||||
|
||||
@@ -34,7 +34,7 @@ fn prepare(dir: &Path) -> anyhow::Result<()> {
|
||||
assert!(crate::common::build_brush()?.success());
|
||||
}
|
||||
std::fs::create_dir_all(disk.join("bin"))?;
|
||||
for (binary, name) in [("git", "git"), ("brush", "sh")] {
|
||||
for (binary, name) in [("git", "git"), ("tar", "tar"), ("brush", "sh")] {
|
||||
std::fs::copy(
|
||||
target.join(format!("wasm32-wasip2/release/{binary}.wasm")),
|
||||
disk.join(format!("bin/{name}.wasm")),
|
||||
|
||||
Generated
+1
@@ -1525,6 +1525,7 @@ dependencies = [
|
||||
"sausage-ssh",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tar",
|
||||
"url",
|
||||
"wasi 0.14.7+wasi-0.2.4",
|
||||
]
|
||||
|
||||
@@ -16,3 +16,4 @@ gix-zlib = "0.4"
|
||||
url = "2"
|
||||
wasi = "0.14"
|
||||
sausage-ssh = { path = "../ssh-client" }
|
||||
tar = { version = "0.4", default-features = false }
|
||||
|
||||
@@ -78,6 +78,9 @@ It also checks these forms:
|
||||
- `show --stat`, `--oneline`, `--name-only`, `show HEAD:path`, and merge commits without a combined diff.
|
||||
- `cat-file -t`, `-p`, `-s`, and `-e` for object inspection.
|
||||
- `ls-tree [-r] [--name-only] <rev> [path]` for literal paths.
|
||||
- `archive [--format=tar] [-o <file>] <rev> [path]` for a tree or literal subtree.
|
||||
The archive preserves file contents, executable modes, and symbolic links.
|
||||
`archive-test.nu` compares extracted output with native Git.
|
||||
- `diff HEAD~1`, `diff A..B`, `--name-only`, `--name-status`, and one literal path after `--`.
|
||||
- `diff --check` and `diff --cached --check` for added whitespace errors and conflict markers.
|
||||
- `branch -a`, `-d`, `-D`, `-f`, `-m`, and `--show-current`.
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env nu
|
||||
# Compare the literal subtree archive with native Git, including binary files and links.
|
||||
def main [component: path] {
|
||||
let component = ($component | path expand)
|
||||
let root = (mktemp -d -p /tmp git-archive.XXXXXX)
|
||||
let repo = ($root | path join repo)
|
||||
mkdir ($repo | path join extensions/demo) ($root | path join native) ($root | path join wasi)
|
||||
^git init -q -b main $repo
|
||||
'outside' | save ($repo | path join outside.txt)
|
||||
0x[00 ff 7f 80] | save ($repo | path join 'extensions/demo/payload space.bin')
|
||||
'executable' | save ($repo | path join extensions/demo/run)
|
||||
^chmod +x ($repo | path join extensions/demo/run)
|
||||
^ln -s 'payload space.bin' ($repo | path join extensions/demo/link)
|
||||
^git -C $repo add .
|
||||
^git -C $repo -c user.name=Test -c user.email=test@example.com commit --no-gpg-sign -q -m fixture
|
||||
^git -C $repo archive --format=tar $'--output=($root)/native.tar' HEAD extensions
|
||||
^wasmtime run -S http -S cli-exit-with-code=y --dir $root $component -C $repo archive --format=tar $'--output=($root)/wasi.tar' HEAD extensions
|
||||
if $env.LAST_EXIT_CODE != 0 { error make {msg: 'WASI archive failed'} }
|
||||
for side in [native wasi] { ^tar -xf $'($root)/($side).tar' -C $'($root)/($side)' }
|
||||
^diff -r $'($root)/native' $'($root)/wasi'
|
||||
if $env.LAST_EXIT_CODE != 0 { error make {msg: 'Extracted contents differ'} }
|
||||
if ($'($root)/wasi/outside.txt' | path exists) { error make {msg: 'Archive includes unrelated files'} }
|
||||
let native_mode = (^stat -c '%a' $'($root)/native/extensions/demo/run')
|
||||
let wasi_mode = (^stat -c '%a' $'($root)/wasi/extensions/demo/run')
|
||||
if $native_mode != $wasi_mode { error make {msg: 'Executable mode differs'} }
|
||||
if (^readlink $'($root)/wasi/extensions/demo/link') != 'payload space.bin' { error make {msg: 'Symlink differs'} }
|
||||
for args in [[archive bad-revision extensions] [archive HEAD missing] [archive --format=zip HEAD]] {
|
||||
let result = (^wasmtime run -S http -S cli-exit-with-code=y --dir $root $component -C $repo ...$args | complete)
|
||||
if $result.exit_code == 0 { error make {msg: $'Invalid archive succeeded: ($args)'} }
|
||||
}
|
||||
print $'Archive comparison passed: ($root)'
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
//! Export a committed tree or literal subtree as tar, without an index or worktree.
|
||||
use crate::{Result, repo::error};
|
||||
use std::{io::Write, path::Path};
|
||||
|
||||
pub(crate) fn archive(
|
||||
repo: &gix::Repository,
|
||||
revision: &str,
|
||||
path: Option<&str>,
|
||||
output: Option<&Path>,
|
||||
) -> Result<()> {
|
||||
if let Some(path) = path {
|
||||
crate::repo::validate_path(path)?;
|
||||
}
|
||||
let root = crate::local::tree(repo, revision)?;
|
||||
let writer: Box<dyn Write> = match output {
|
||||
Some(path) => {
|
||||
Box::new(std::fs::File::create(path).map_err(|e| error("archive output", e))?)
|
||||
}
|
||||
None => Box::new(std::io::stdout().lock()),
|
||||
};
|
||||
let mut tar = tar::Builder::new(writer);
|
||||
fn walk(
|
||||
repo: &gix::Repository,
|
||||
id: gix::ObjectId,
|
||||
prefix: &str,
|
||||
filter: Option<&str>,
|
||||
tar: &mut tar::Builder<Box<dyn Write>>,
|
||||
) -> Result<bool> {
|
||||
let tree = repo.find_tree(id).map_err(|e| error("archive tree", e))?;
|
||||
let mut found = filter.is_none();
|
||||
for entry in tree.iter() {
|
||||
let entry = entry.map_err(|e| error("archive entry", e))?;
|
||||
let path = format!("{prefix}{}", entry.filename());
|
||||
let selected = filter.is_none_or(|f| path == f || path.starts_with(&format!("{f}/")));
|
||||
let ancestor = filter.is_some_and(|f| f.starts_with(&format!("{path}/")));
|
||||
if !selected && !ancestor {
|
||||
continue;
|
||||
}
|
||||
found |= selected;
|
||||
if entry.mode().is_tree() {
|
||||
if selected {
|
||||
let mut header = tar::Header::new_gnu();
|
||||
header.set_entry_type(tar::EntryType::Directory);
|
||||
header.set_mode(0o755);
|
||||
header.set_size(0);
|
||||
tar.append_data(&mut header, &path, std::io::empty())
|
||||
.map_err(|e| error("archive directory", e))?;
|
||||
}
|
||||
found |= walk(
|
||||
repo,
|
||||
entry.oid().to_owned(),
|
||||
&format!("{path}/"),
|
||||
filter,
|
||||
tar,
|
||||
)?;
|
||||
} else if selected && !entry.mode().is_commit() {
|
||||
let object = repo
|
||||
.find_blob(entry.oid())
|
||||
.map_err(|e| error("archive blob", e))?;
|
||||
let mut header = tar::Header::new_gnu();
|
||||
header.set_mode(entry.mode().kind() as u32 & 0o777);
|
||||
if entry.mode().is_link() {
|
||||
header.set_entry_type(tar::EntryType::Symlink);
|
||||
header.set_size(0);
|
||||
header
|
||||
.set_link_name(
|
||||
std::str::from_utf8(&object.data)
|
||||
.map_err(|e| error("archive link", e))?,
|
||||
)
|
||||
.map_err(|e| error("archive link", e))?;
|
||||
tar.append_data(&mut header, &path, std::io::empty())
|
||||
.map_err(|e| error("archive link", e))?;
|
||||
} else {
|
||||
header.set_size(object.data.len() as u64);
|
||||
tar.append_data(&mut header, &path, object.data.as_slice())
|
||||
.map_err(|e| error("archive file", e))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(found)
|
||||
}
|
||||
if !walk(repo, root, "", path, &mut tar)? {
|
||||
return Err(error("archive", "path did not match any files"));
|
||||
}
|
||||
tar.finish().map_err(|e| error("archive finish", e))
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Local Git operations adapted from the v2 gix plugin, without RPC or host imports.
|
||||
#![allow(dead_code)]
|
||||
mod archive;
|
||||
mod branches;
|
||||
mod changes;
|
||||
mod checkout;
|
||||
@@ -78,6 +79,14 @@ pub struct Cli {
|
||||
}
|
||||
#[derive(Subcommand)]
|
||||
enum Command {
|
||||
Archive {
|
||||
#[arg(long, default_value = "tar", value_parser = ["tar"])]
|
||||
format: String,
|
||||
#[arg(short = 'o', long)]
|
||||
output: Option<std::path::PathBuf>,
|
||||
revision: String,
|
||||
path: Option<String>,
|
||||
},
|
||||
Show {
|
||||
#[arg(short = 's', long = "no-patch")]
|
||||
no_patch: bool,
|
||||
@@ -1501,6 +1510,14 @@ pub fn run(cli: Cli) -> Result<i32> {
|
||||
)?;
|
||||
}
|
||||
},
|
||||
Command::Archive {
|
||||
format: _,
|
||||
output,
|
||||
revision,
|
||||
path,
|
||||
} => {
|
||||
archive::archive(&repository, &revision, path.as_deref(), output.as_deref())?;
|
||||
}
|
||||
Command::CatFile {
|
||||
kind,
|
||||
pretty: _,
|
||||
|
||||
Reference in New Issue
Block a user