add S3-backed config + memory sync extension
New extensions/sync/ that bidirectionally syncs ~/.pi/agent/ configs + memories with an S3 bucket so fresh OpenShell sandboxes (or new laptops) boot with existing pi state. - Allowlist: settings.json, k8s.json, websearch.json, custom-providers.json, hosts.json, local-models.json, memory/**. Excludes auth.json (provider API keys; injected per-sandbox). - Per-file last-write-wins by real mtime carried in x-amz-meta-pi-mtime; 1s clock-jitter tolerance. - Per-machine ~/.pi/agent/.sync-manifest.json records what was last seen on S3 so auto-prune can't wipe a concurrent laptop's data. - Manual /sync command opens a TUI panel; subcommands /sync pull, push, prune, status work without the panel. - session_start auto-pulls (5s soft timeout, non-blocking). - session_shutdown auto-prunes then auto-pushes (10s hard timeout). - Config via PI_AWS_* env vars only (chicken/egg: nothing to bootstrap). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
9c64807ac3
commit
4f387a278b
@@ -0,0 +1,63 @@
|
||||
# sync extension — setup
|
||||
|
||||
Bidirectionally sync `~/.pi/agent/` (configs + memories) with an S3 bucket so a fresh OpenShell sandbox boots with your existing pi state already present.
|
||||
|
||||
## What gets synced
|
||||
|
||||
| Path under `~/.pi/agent/` | Synced |
|
||||
|---|---|
|
||||
| `settings.json` | yes |
|
||||
| `k8s.json`, `websearch.json`, `custom-providers.json`, `hosts.json`, `local-models.json` | yes |
|
||||
| `memory/**` | yes (recursive) |
|
||||
| `auth.json` | **no** (provider API keys; inject via OpenShell credentials) |
|
||||
| `.sync-manifest.json` | **no** (per-machine bookkeeping; never uploaded) |
|
||||
|
||||
S3 key shape: `${PI_AWS_PREFIX}/agent/<relative path>`.
|
||||
|
||||
## Configuration
|
||||
|
||||
Set these env vars before launching pi. In an OpenShell sandbox, inject them through the provider/credential mechanism; on a laptop, set them in your shell rc.
|
||||
|
||||
| Var | Purpose | Required | Default |
|
||||
|---|---|---|---|
|
||||
| `PI_AWS_ACCESS_KEY_ID` | S3 access key | yes | — |
|
||||
| `PI_AWS_SECRET_ACCESS_KEY` | S3 secret key | yes | — |
|
||||
| `PI_AWS_ENDPOINT_URL_S3` | S3 endpoint URL | yes | — |
|
||||
| `PI_AWS_BUCKET` | bucket name | yes | — |
|
||||
| `PI_AWS_REGION` | region | no | `us-east-1` |
|
||||
| `PI_AWS_PREFIX` | object key prefix | no | empty |
|
||||
| `PI_AWS_FORCE_PATH_STYLE` | path-style addressing (rustfs / minio) | no | `true` |
|
||||
|
||||
If any required var is missing, the lifecycle hooks silently no-op and `/sync` shows a "not configured" panel listing exactly which vars are missing.
|
||||
|
||||
## Lifecycle hooks
|
||||
|
||||
- **session_start** → pull-only (newer remote → local). Soft timeout 5 s. Never blocks pi from starting.
|
||||
- **session_shutdown** → manifest-based prune, then push-only. Hard timeout 10 s. Never hangs exit.
|
||||
|
||||
Crash-safe: if pi is killed, neither hook fires. Files on disk are never half-written (atomic tmp + rename). Run `/sync` manually after the next start to recover.
|
||||
|
||||
## Slash commands
|
||||
|
||||
| Command | Behavior |
|
||||
|---|---|
|
||||
| `/sync` | Open the panel (status + interactive actions). |
|
||||
| `/sync pull` | Pull only (newer remote → local). |
|
||||
| `/sync push` | Push only (newer local → remote). |
|
||||
| `/sync prune` | Manifest-based prune: delete S3 keys this machine pulled but no longer has locally. |
|
||||
| `/sync status` | Notify-only summary; the panel is the live view. |
|
||||
|
||||
Bidirectional sync ("sync now") is reachable from the panel's `s` key only — there is no `/sync sync` slash command, by design.
|
||||
|
||||
## Conflict + deletion model
|
||||
|
||||
- Per-file last-write-wins by **real mtime** carried in `x-amz-meta-pi-mtime`. Clock-jitter tolerance: 1 s.
|
||||
- Deletions are NOT automatic across machines. If you delete a memory locally then restart pi without exiting cleanly, auto-pull re-creates it. The correct deletion path is: delete → exit pi cleanly → auto-prune runs → next start sees nothing.
|
||||
- `/sync prune` only deletes S3 keys this machine pulled before. S3-only-never-pulled keys (e.g. another laptop's recent additions) are left alone. To force-delete, operate on the bucket directly.
|
||||
|
||||
## Security
|
||||
|
||||
- `auth.json` is never read or transmitted by this extension.
|
||||
- `PI_AWS_*` secrets stay in process env; never written to disk by sync code.
|
||||
- `.sync-manifest.json` is `0600`. Atomic writes (tmp + rename).
|
||||
- Bucket-level encryption is out of scope. Enable server-side encryption at the S3 layer if your memories or configs contain sensitive content (e.g. the k8s SA token in `k8s.json`).
|
||||
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readSyncConfig } from "./config.js";
|
||||
|
||||
const fullEnv = {
|
||||
PI_AWS_ACCESS_KEY_ID: "ak",
|
||||
PI_AWS_SECRET_ACCESS_KEY: "sk",
|
||||
PI_AWS_ENDPOINT_URL_S3: "https://rustfs.lan",
|
||||
PI_AWS_BUCKET: "homelab",
|
||||
};
|
||||
|
||||
describe("readSyncConfig", () => {
|
||||
it("returns ok with all required vars and applies defaults", () => {
|
||||
const result = readSyncConfig(fullEnv);
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
expect(result.config).toEqual({
|
||||
accessKeyId: "ak",
|
||||
secretAccessKey: "sk",
|
||||
endpoint: "https://rustfs.lan",
|
||||
bucket: "homelab",
|
||||
region: "us-east-1",
|
||||
prefix: "",
|
||||
forcePathStyle: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("honors PI_AWS_REGION, PI_AWS_PREFIX, PI_AWS_FORCE_PATH_STYLE", () => {
|
||||
const result = readSyncConfig({
|
||||
...fullEnv,
|
||||
PI_AWS_REGION: "eu-west-1",
|
||||
PI_AWS_PREFIX: "pi",
|
||||
PI_AWS_FORCE_PATH_STYLE: "false",
|
||||
});
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
expect(result.config.region).toBe("eu-west-1");
|
||||
expect(result.config.prefix).toBe("pi");
|
||||
expect(result.config.forcePathStyle).toBe(false);
|
||||
});
|
||||
|
||||
it("returns missing list with every absent required var", () => {
|
||||
const result = readSyncConfig({});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.missing.sort()).toEqual([
|
||||
"PI_AWS_ACCESS_KEY_ID",
|
||||
"PI_AWS_BUCKET",
|
||||
"PI_AWS_ENDPOINT_URL_S3",
|
||||
"PI_AWS_SECRET_ACCESS_KEY",
|
||||
]);
|
||||
});
|
||||
|
||||
it("treats empty / whitespace-only required vars as missing", () => {
|
||||
const result = readSyncConfig({
|
||||
...fullEnv,
|
||||
PI_AWS_ACCESS_KEY_ID: "",
|
||||
PI_AWS_BUCKET: " ",
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.missing.sort()).toEqual([
|
||||
"PI_AWS_ACCESS_KEY_ID",
|
||||
"PI_AWS_BUCKET",
|
||||
]);
|
||||
});
|
||||
|
||||
it("trims trailing slashes from prefix", () => {
|
||||
const result = readSyncConfig({ ...fullEnv, PI_AWS_PREFIX: "pi/" });
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
expect(result.config.prefix).toBe("pi");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
export interface SyncConfig {
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
endpoint: string;
|
||||
bucket: string;
|
||||
region: string;
|
||||
prefix: string;
|
||||
forcePathStyle: boolean;
|
||||
}
|
||||
|
||||
export type ReadSyncConfigResult =
|
||||
| { ok: true; config: SyncConfig }
|
||||
| { ok: false; missing: string[] };
|
||||
|
||||
const REQUIRED = [
|
||||
"PI_AWS_ACCESS_KEY_ID",
|
||||
"PI_AWS_SECRET_ACCESS_KEY",
|
||||
"PI_AWS_ENDPOINT_URL_S3",
|
||||
"PI_AWS_BUCKET",
|
||||
] as const;
|
||||
|
||||
function read(
|
||||
env: Record<string, string | undefined>,
|
||||
name: string,
|
||||
): string | undefined {
|
||||
const v = env[name];
|
||||
if (v === undefined) return undefined;
|
||||
const trimmed = v.trim();
|
||||
return trimmed === "" ? undefined : trimmed;
|
||||
}
|
||||
|
||||
export function readSyncConfig(
|
||||
env: Record<string, string | undefined> = process.env,
|
||||
): ReadSyncConfigResult {
|
||||
const missing: string[] = [];
|
||||
for (const name of REQUIRED) {
|
||||
if (read(env, name) === undefined) missing.push(name);
|
||||
}
|
||||
if (missing.length > 0) return { ok: false, missing };
|
||||
|
||||
const forcePathStyleRaw = read(env, "PI_AWS_FORCE_PATH_STYLE");
|
||||
const forcePathStyle =
|
||||
forcePathStyleRaw === undefined ? true : forcePathStyleRaw !== "false";
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
config: {
|
||||
accessKeyId: read(env, "PI_AWS_ACCESS_KEY_ID")!,
|
||||
secretAccessKey: read(env, "PI_AWS_SECRET_ACCESS_KEY")!,
|
||||
endpoint: read(env, "PI_AWS_ENDPOINT_URL_S3")!,
|
||||
bucket: read(env, "PI_AWS_BUCKET")!,
|
||||
region: read(env, "PI_AWS_REGION") ?? "us-east-1",
|
||||
prefix: (read(env, "PI_AWS_PREFIX") ?? "").replace(/\/+$/, ""),
|
||||
forcePathStyle,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { readSyncConfig } from "./config.js";
|
||||
import { createS3Wrapper } from "./s3.js";
|
||||
import { openSyncPanel } from "./panel.js";
|
||||
import { executePrune, executeSync, type SyncReport } from "./sync.js";
|
||||
|
||||
const STARTUP_PULL_SOFT_MS = 5000;
|
||||
const SHUTDOWN_PUSH_HARD_MS = 10_000;
|
||||
|
||||
function withTimeout<T>(p: Promise<T>, ms: number, label: string): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
||||
p.then(
|
||||
(v) => {
|
||||
clearTimeout(timer);
|
||||
resolve(v);
|
||||
},
|
||||
(e) => {
|
||||
clearTimeout(timer);
|
||||
reject(e);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function summarize(report: SyncReport): string {
|
||||
if (report.mode === "prune") {
|
||||
return `sync: pruned ${report.deleted}, errors ${report.errors}`;
|
||||
}
|
||||
return `sync: pushed ${report.pushed}, pulled ${report.pulled}, skipped ${report.skipped}, errors ${report.errors}`;
|
||||
}
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
const cfg = readSyncConfig();
|
||||
if (!cfg.ok) return;
|
||||
const s3 = createS3Wrapper(cfg.config);
|
||||
try {
|
||||
const report = await withTimeout(
|
||||
executeSync({ mode: "pull", s3, prefix: cfg.config.prefix }),
|
||||
STARTUP_PULL_SOFT_MS,
|
||||
"sync: startup pull",
|
||||
);
|
||||
if (ctx.hasUI) ctx.ui.notify(summarize(report), "info");
|
||||
} catch (err) {
|
||||
if (ctx.hasUI) ctx.ui.notify(`sync: pull failed — ${(err as Error).message}`, "warning");
|
||||
}
|
||||
});
|
||||
|
||||
pi.on("session_shutdown", async () => {
|
||||
const cfg = readSyncConfig();
|
||||
if (!cfg.ok) return;
|
||||
const s3 = createS3Wrapper(cfg.config);
|
||||
try {
|
||||
await withTimeout(
|
||||
(async () => {
|
||||
await executePrune({ s3, prefix: cfg.config.prefix });
|
||||
await executeSync({ mode: "push", s3, prefix: cfg.config.prefix });
|
||||
})(),
|
||||
SHUTDOWN_PUSH_HARD_MS,
|
||||
"sync: shutdown prune+push",
|
||||
);
|
||||
} catch {
|
||||
}
|
||||
});
|
||||
|
||||
pi.registerCommand("sync", {
|
||||
description: "Sync ~/.pi/agent configs + memories with S3 (panel + subcommands)",
|
||||
handler: async (args, ctx) => {
|
||||
const sub = args.trim();
|
||||
if (sub === "") {
|
||||
if (!ctx.hasUI) {
|
||||
ctx.ui.notify("/sync needs an interactive session", "warning");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await openSyncPanel(ctx);
|
||||
} catch (err) {
|
||||
ctx.ui.notify(`/sync error: ${(err as Error).message}`, "error");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const cfg = readSyncConfig();
|
||||
if (!cfg.ok) {
|
||||
ctx.ui.notify(
|
||||
`sync not configured — missing: ${cfg.missing.join(", ")}`,
|
||||
"warning",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const s3 = createS3Wrapper(cfg.config);
|
||||
try {
|
||||
let report: SyncReport;
|
||||
if (sub === "pull") {
|
||||
report = await executeSync({ mode: "pull", s3, prefix: cfg.config.prefix });
|
||||
} else if (sub === "push") {
|
||||
report = await executeSync({ mode: "push", s3, prefix: cfg.config.prefix });
|
||||
} else if (sub === "prune") {
|
||||
report = await executePrune({ s3, prefix: cfg.config.prefix });
|
||||
} else if (sub === "status") {
|
||||
ctx.ui.notify(
|
||||
"/sync status is summary-only — open the panel (/sync) for a live view",
|
||||
"info",
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
ctx.ui.notify(
|
||||
`/sync: unknown subcommand "${sub}" — try pull, push, prune, status, or just /sync`,
|
||||
"warning",
|
||||
);
|
||||
return;
|
||||
}
|
||||
ctx.ui.notify(summarize(report), report.errors > 0 ? "warning" : "info");
|
||||
} catch (err) {
|
||||
ctx.ui.notify(`/sync ${sub} error: ${(err as Error).message}`, "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { manifestPath, readManifest, writeManifest } from "./manifest.js";
|
||||
|
||||
let tmpHome: string;
|
||||
let originalHome: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
originalHome = process.env.HOME;
|
||||
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "pi-sync-manifest-"));
|
||||
process.env.HOME = tmpHome;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalHome === undefined) delete process.env.HOME;
|
||||
else process.env.HOME = originalHome;
|
||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("manifest", () => {
|
||||
it("manifestPath lives at ~/.pi/agent/.sync-manifest.json", () => {
|
||||
expect(manifestPath()).toBe(
|
||||
path.join(tmpHome, ".pi", "agent", ".sync-manifest.json"),
|
||||
);
|
||||
});
|
||||
|
||||
it("returns an empty manifest when the file is missing", () => {
|
||||
expect(readManifest()).toEqual({ version: 1, entries: {} });
|
||||
});
|
||||
|
||||
it("round-trips entries", () => {
|
||||
writeManifest({
|
||||
version: 1,
|
||||
entries: {
|
||||
"pi/agent/settings.json": { mtime: 1000, size: 42 },
|
||||
"pi/agent/memory/x.md": { mtime: 2000, size: 100 },
|
||||
},
|
||||
});
|
||||
expect(readManifest().entries).toEqual({
|
||||
"pi/agent/settings.json": { mtime: 1000, size: 42 },
|
||||
"pi/agent/memory/x.md": { mtime: 2000, size: 100 },
|
||||
});
|
||||
});
|
||||
|
||||
it("writes mode 0600", () => {
|
||||
writeManifest({ version: 1, entries: {} });
|
||||
const mode = fs.statSync(manifestPath()).mode & 0o777;
|
||||
expect(mode).toBe(0o600);
|
||||
});
|
||||
|
||||
it("ignores a manifest with the wrong version", () => {
|
||||
fs.mkdirSync(path.dirname(manifestPath()), { recursive: true });
|
||||
fs.writeFileSync(manifestPath(), JSON.stringify({ version: 2, entries: {} }));
|
||||
expect(readManifest()).toEqual({ version: 1, entries: {} });
|
||||
});
|
||||
|
||||
it("ignores unparseable manifest content", () => {
|
||||
fs.mkdirSync(path.dirname(manifestPath()), { recursive: true });
|
||||
fs.writeFileSync(manifestPath(), "not json");
|
||||
expect(readManifest()).toEqual({ version: 1, entries: {} });
|
||||
});
|
||||
|
||||
it("ignores entries with malformed shape", () => {
|
||||
fs.mkdirSync(path.dirname(manifestPath()), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
manifestPath(),
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
entries: {
|
||||
"good/key": { mtime: 100, size: 1 },
|
||||
"bad/key": { mtime: "nope", size: 1 },
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(readManifest().entries).toEqual({
|
||||
"good/key": { mtime: 100, size: 1 },
|
||||
});
|
||||
});
|
||||
|
||||
it("writes atomically (no temp file left behind on success)", () => {
|
||||
writeManifest({ version: 1, entries: { foo: { mtime: 1, size: 1 } } });
|
||||
const dir = path.dirname(manifestPath());
|
||||
const stragglers = fs.readdirSync(dir).filter((f) => f.endsWith(".tmp"));
|
||||
expect(stragglers).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
export interface ManifestEntry {
|
||||
mtime: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface Manifest {
|
||||
version: 1;
|
||||
entries: Record<string, ManifestEntry>;
|
||||
}
|
||||
|
||||
export function manifestPath(): string {
|
||||
return path.join(getAgentDir(), ".sync-manifest.json");
|
||||
}
|
||||
|
||||
function isEntry(value: unknown): value is ManifestEntry {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
typeof (value as ManifestEntry).mtime === "number" &&
|
||||
typeof (value as ManifestEntry).size === "number"
|
||||
);
|
||||
}
|
||||
|
||||
export function readManifest(): Manifest {
|
||||
try {
|
||||
const raw = fs.readFileSync(manifestPath(), "utf8");
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (
|
||||
parsed &&
|
||||
typeof parsed === "object" &&
|
||||
(parsed as { version?: unknown }).version === 1 &&
|
||||
typeof (parsed as { entries?: unknown }).entries === "object" &&
|
||||
(parsed as { entries?: unknown }).entries !== null
|
||||
) {
|
||||
const rawEntries = (parsed as { entries: Record<string, unknown> }).entries;
|
||||
const entries: Record<string, ManifestEntry> = {};
|
||||
for (const [k, v] of Object.entries(rawEntries)) {
|
||||
if (isEntry(v)) entries[k] = v;
|
||||
}
|
||||
return { version: 1, entries };
|
||||
}
|
||||
} catch {}
|
||||
return { version: 1, entries: {} };
|
||||
}
|
||||
|
||||
export function writeManifest(manifest: Manifest): void {
|
||||
const dir = getAgentDir();
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const final = manifestPath();
|
||||
const tmp = `${final}.tmp`;
|
||||
fs.writeFileSync(tmp, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
|
||||
fs.renameSync(tmp, final);
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
||||
import { Key, matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
||||
import { readSyncConfig, type SyncConfig } from "./config.js";
|
||||
import { createS3Wrapper } from "./s3.js";
|
||||
import { executePrune, executeSync, type SyncReport } from "./sync.js";
|
||||
|
||||
type PanelAction =
|
||||
| { kind: "close" }
|
||||
| { kind: "refresh" }
|
||||
| { kind: "sync" }
|
||||
| { kind: "pull" }
|
||||
| { kind: "push" }
|
||||
| { kind: "prune" };
|
||||
|
||||
function renderNotConfigured(
|
||||
ctx: ExtensionCommandContext,
|
||||
missing: string[],
|
||||
): Promise<PanelAction> {
|
||||
return ctx.ui.custom<PanelAction>((_tui, theme, _kb, done) => {
|
||||
function handleInput(data: string): void {
|
||||
if (matchesKey(data, Key.escape) || matchesKey(data, Key.enter)) {
|
||||
done({ kind: "close" });
|
||||
}
|
||||
}
|
||||
function render(width: number): string[] {
|
||||
const lines: string[] = [];
|
||||
const add = (s: string) => lines.push(truncateToWidth(s, width));
|
||||
add(theme.fg("accent", "─".repeat(width)));
|
||||
add(theme.fg("text", " Sync (S3)"));
|
||||
lines.push("");
|
||||
add(theme.fg("warning", " sync not configured."));
|
||||
lines.push("");
|
||||
add(theme.fg("dim", " Missing env vars:"));
|
||||
for (const name of missing) add(theme.fg("text", ` ${name}`));
|
||||
lines.push("");
|
||||
add(theme.fg("dim", " Esc / Enter close"));
|
||||
add(theme.fg("accent", "─".repeat(width)));
|
||||
return lines;
|
||||
}
|
||||
return { render, invalidate: () => {}, handleInput };
|
||||
});
|
||||
}
|
||||
|
||||
function renderStatus(
|
||||
ctx: ExtensionCommandContext,
|
||||
config: SyncConfig,
|
||||
lastReport: SyncReport | undefined,
|
||||
): Promise<PanelAction> {
|
||||
return ctx.ui.custom<PanelAction>((_tui, theme, _kb, done) => {
|
||||
let cached: string[] | undefined;
|
||||
function handleInput(data: string): void {
|
||||
if (matchesKey(data, Key.escape)) {
|
||||
done({ kind: "close" });
|
||||
return;
|
||||
}
|
||||
if (data === "r") done({ kind: "refresh" });
|
||||
if (data === "s") done({ kind: "sync" });
|
||||
if (data === "p") done({ kind: "pull" });
|
||||
if (data === "u") done({ kind: "push" });
|
||||
if (data === "x") done({ kind: "prune" });
|
||||
}
|
||||
function render(width: number): string[] {
|
||||
if (cached) return cached;
|
||||
const lines: string[] = [];
|
||||
const add = (s: string) => lines.push(truncateToWidth(s, width));
|
||||
add(theme.fg("accent", "─".repeat(width)));
|
||||
add(theme.fg("text", " Sync (S3)"));
|
||||
lines.push("");
|
||||
const labelW = 11;
|
||||
add(" " + theme.fg("muted", "Endpoint:".padEnd(labelW)) + theme.fg("text", config.endpoint));
|
||||
add(" " + theme.fg("muted", "Bucket:".padEnd(labelW)) + theme.fg("text", config.bucket));
|
||||
add(" " + theme.fg("muted", "Prefix:".padEnd(labelW)) + theme.fg("text", config.prefix === "" ? "(root)" : config.prefix));
|
||||
lines.push("");
|
||||
if (lastReport) {
|
||||
const summary =
|
||||
lastReport.mode === "prune"
|
||||
? `${lastReport.deleted} deleted, ${lastReport.errors} errors`
|
||||
: `${lastReport.pushed} pushed, ${lastReport.pulled} pulled, ${lastReport.skipped} skipped, ${lastReport.errors} errors`;
|
||||
add(" " + theme.fg("muted", "Last:".padEnd(labelW)) + theme.fg("text", `${lastReport.mode} — ${summary}`));
|
||||
for (const entry of lastReport.actions.slice(0, 20)) {
|
||||
const tag =
|
||||
entry.status === "error"
|
||||
? theme.fg("warning", "✗")
|
||||
: entry.action.kind === "upload"
|
||||
? theme.fg("success", "↑")
|
||||
: entry.action.kind === "download"
|
||||
? theme.fg("success", "↓")
|
||||
: entry.action.kind === "delete-remote"
|
||||
? theme.fg("warning", "✗")
|
||||
: theme.fg("dim", "·");
|
||||
const relPath = "relPath" in entry.action ? entry.action.relPath : "";
|
||||
add(` ${tag} ${theme.fg("text", relPath)}`);
|
||||
}
|
||||
if (lastReport.actions.length > 20) {
|
||||
add(theme.fg("dim", ` … ${lastReport.actions.length - 20} more`));
|
||||
}
|
||||
} else {
|
||||
add(" " + theme.fg("muted", "Last:".padEnd(labelW)) + theme.fg("dim", "(no sync this session)"));
|
||||
}
|
||||
lines.push("");
|
||||
add(theme.fg("dim", " r refresh • s sync now • p pull • u push • x prune • Esc close"));
|
||||
add(theme.fg("accent", "─".repeat(width)));
|
||||
cached = lines;
|
||||
return lines;
|
||||
}
|
||||
return {
|
||||
render,
|
||||
invalidate: () => {
|
||||
cached = undefined;
|
||||
},
|
||||
handleInput,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function openSyncPanel(ctx: ExtensionCommandContext): Promise<void> {
|
||||
let lastReport: SyncReport | undefined;
|
||||
for (;;) {
|
||||
const cfg = readSyncConfig();
|
||||
if (!cfg.ok) {
|
||||
const action = await renderNotConfigured(ctx, cfg.missing);
|
||||
if (action.kind === "close") return;
|
||||
continue;
|
||||
}
|
||||
const action = await renderStatus(ctx, cfg.config, lastReport);
|
||||
if (action.kind === "close") return;
|
||||
if (action.kind === "refresh") {
|
||||
lastReport = undefined;
|
||||
continue;
|
||||
}
|
||||
const s3 = createS3Wrapper(cfg.config);
|
||||
try {
|
||||
if (action.kind === "sync") {
|
||||
lastReport = await executeSync({ mode: "sync", s3, prefix: cfg.config.prefix });
|
||||
} else if (action.kind === "pull") {
|
||||
lastReport = await executeSync({ mode: "pull", s3, prefix: cfg.config.prefix });
|
||||
} else if (action.kind === "push") {
|
||||
lastReport = await executeSync({ mode: "push", s3, prefix: cfg.config.prefix });
|
||||
} else if (action.kind === "prune") {
|
||||
lastReport = await executePrune({ s3, prefix: cfg.config.prefix });
|
||||
}
|
||||
} catch (err) {
|
||||
ctx.ui.notify(`sync error: ${(err as Error).message}`, "error");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
SYNCED_FILES,
|
||||
SYNCED_DIRS,
|
||||
EXCLUDED_FILES,
|
||||
keyForRelPath,
|
||||
relPathForKey,
|
||||
walkLocalFiles,
|
||||
} from "./paths.js";
|
||||
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-sync-paths-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("paths allowlist", () => {
|
||||
it("includes the expected config files", () => {
|
||||
expect(SYNCED_FILES).toEqual([
|
||||
"settings.json",
|
||||
"k8s.json",
|
||||
"websearch.json",
|
||||
"custom-providers.json",
|
||||
"hosts.json",
|
||||
"local-models.json",
|
||||
]);
|
||||
});
|
||||
|
||||
it("includes the memory directory", () => {
|
||||
expect(SYNCED_DIRS).toEqual(["memory"]);
|
||||
});
|
||||
|
||||
it("explicitly excludes auth.json and the manifest", () => {
|
||||
expect(EXCLUDED_FILES).toContain("auth.json");
|
||||
expect(EXCLUDED_FILES).toContain(".sync-manifest.json");
|
||||
});
|
||||
});
|
||||
|
||||
describe("keyForRelPath", () => {
|
||||
it("prefixes with <prefix>/agent/", () => {
|
||||
expect(keyForRelPath("pi", "settings.json")).toBe("pi/agent/settings.json");
|
||||
});
|
||||
|
||||
it("handles empty prefix", () => {
|
||||
expect(keyForRelPath("", "settings.json")).toBe("agent/settings.json");
|
||||
});
|
||||
|
||||
it("preserves nested memory paths", () => {
|
||||
expect(keyForRelPath("pi", "memory/global/feedback/foo.md")).toBe(
|
||||
"pi/agent/memory/global/feedback/foo.md",
|
||||
);
|
||||
});
|
||||
|
||||
it("strips trailing slashes from the prefix", () => {
|
||||
expect(keyForRelPath("pi/", "settings.json")).toBe("pi/agent/settings.json");
|
||||
});
|
||||
});
|
||||
|
||||
describe("relPathForKey", () => {
|
||||
it("inverts keyForRelPath", () => {
|
||||
expect(relPathForKey("pi", "pi/agent/settings.json")).toBe("settings.json");
|
||||
expect(relPathForKey("pi", "pi/agent/memory/x/y.md")).toBe("memory/x/y.md");
|
||||
expect(relPathForKey("", "agent/settings.json")).toBe("settings.json");
|
||||
});
|
||||
|
||||
it("returns undefined for a key outside the prefix", () => {
|
||||
expect(relPathForKey("pi", "other/agent/settings.json")).toBeUndefined();
|
||||
expect(relPathForKey("pi", "pi/different/settings.json")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("walkLocalFiles", () => {
|
||||
it("returns an empty array when nothing exists", async () => {
|
||||
expect(await walkLocalFiles(tmpDir)).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns listed SYNCED_FILES that exist", async () => {
|
||||
fs.writeFileSync(path.join(tmpDir, "settings.json"), "{}");
|
||||
fs.writeFileSync(path.join(tmpDir, "k8s.json"), "{}");
|
||||
const result = await walkLocalFiles(tmpDir);
|
||||
expect(result.sort()).toEqual(["k8s.json", "settings.json"]);
|
||||
});
|
||||
|
||||
it("walks the memory directory recursively", async () => {
|
||||
fs.mkdirSync(path.join(tmpDir, "memory", "global", "feedback"), { recursive: true });
|
||||
fs.writeFileSync(path.join(tmpDir, "memory", "MEMORY.md"), "#");
|
||||
fs.writeFileSync(path.join(tmpDir, "memory", "global", "feedback", "foo.md"), "x");
|
||||
const result = await walkLocalFiles(tmpDir);
|
||||
expect(result.sort()).toEqual([
|
||||
"memory/MEMORY.md",
|
||||
"memory/global/feedback/foo.md",
|
||||
]);
|
||||
});
|
||||
|
||||
it("skips EXCLUDED_FILES even if present", async () => {
|
||||
fs.writeFileSync(path.join(tmpDir, "settings.json"), "{}");
|
||||
fs.writeFileSync(path.join(tmpDir, "auth.json"), "{}");
|
||||
fs.writeFileSync(path.join(tmpDir, ".sync-manifest.json"), "{}");
|
||||
const result = await walkLocalFiles(tmpDir);
|
||||
expect(result).toEqual(["settings.json"]);
|
||||
});
|
||||
|
||||
it("does NOT drop memory entries whose basename matches an excluded file", async () => {
|
||||
fs.mkdirSync(path.join(tmpDir, "memory"), { recursive: true });
|
||||
fs.writeFileSync(path.join(tmpDir, "memory", "auth.json"), "{}");
|
||||
fs.writeFileSync(path.join(tmpDir, "memory", ".sync-manifest.json"), "{}");
|
||||
const result = await walkLocalFiles(tmpDir);
|
||||
expect(result.sort()).toEqual([
|
||||
"memory/.sync-manifest.json",
|
||||
"memory/auth.json",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
|
||||
export const SYNCED_FILES: readonly string[] = [
|
||||
"settings.json",
|
||||
"k8s.json",
|
||||
"websearch.json",
|
||||
"custom-providers.json",
|
||||
"hosts.json",
|
||||
"local-models.json",
|
||||
];
|
||||
|
||||
export const SYNCED_DIRS: readonly string[] = ["memory"];
|
||||
|
||||
export const EXCLUDED_FILES: readonly string[] = [
|
||||
"auth.json",
|
||||
".sync-manifest.json",
|
||||
];
|
||||
|
||||
function normalizePrefix(prefix: string): string {
|
||||
return prefix.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
export function keyForRelPath(prefix: string, relPath: string): string {
|
||||
const normalized = normalizePrefix(prefix);
|
||||
const parts = normalized === "" ? ["agent", relPath] : [normalized, "agent", relPath];
|
||||
return parts.join("/");
|
||||
}
|
||||
|
||||
export function relPathForKey(prefix: string, key: string): string | undefined {
|
||||
const normalized = normalizePrefix(prefix);
|
||||
const expected = normalized === "" ? "agent/" : `${normalized}/agent/`;
|
||||
if (!key.startsWith(expected)) return undefined;
|
||||
return key.slice(expected.length);
|
||||
}
|
||||
|
||||
async function walkDir(absDir: string, relDir: string, out: string[]): Promise<void> {
|
||||
let entries: fs.Dirent[];
|
||||
try {
|
||||
entries = await fs.promises.readdir(absDir, { withFileTypes: true });
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code === "ENOENT") return;
|
||||
throw err;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const childAbs = path.join(absDir, entry.name);
|
||||
const childRel = relDir === "" ? entry.name : `${relDir}/${entry.name}`;
|
||||
if (entry.isDirectory()) {
|
||||
await walkDir(childAbs, childRel, out);
|
||||
} else if (entry.isFile()) {
|
||||
out.push(childRel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function walkLocalFiles(agentDir: string): Promise<string[]> {
|
||||
const found: string[] = [];
|
||||
for (const file of SYNCED_FILES) {
|
||||
try {
|
||||
const stat = await fs.promises.stat(path.join(agentDir, file));
|
||||
if (stat.isFile()) found.push(file);
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
|
||||
}
|
||||
}
|
||||
for (const dir of SYNCED_DIRS) {
|
||||
await walkDir(path.join(agentDir, dir), dir, found);
|
||||
}
|
||||
return found.filter((rel) => !EXCLUDED_FILES.includes(rel));
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
createS3Wrapper,
|
||||
MTIME_META_KEY,
|
||||
type S3Object,
|
||||
} from "./s3.js";
|
||||
import type { SyncConfig } from "./config.js";
|
||||
|
||||
const config: SyncConfig = {
|
||||
accessKeyId: "ak",
|
||||
secretAccessKey: "sk",
|
||||
endpoint: "https://rustfs.lan",
|
||||
bucket: "homelab",
|
||||
region: "us-east-1",
|
||||
prefix: "pi",
|
||||
forcePathStyle: true,
|
||||
};
|
||||
|
||||
function makeMockClient(send: (cmd: unknown) => Promise<unknown>) {
|
||||
return { send: vi.fn(send) } as unknown as Parameters<typeof createS3Wrapper>[1];
|
||||
}
|
||||
|
||||
describe("S3 wrapper", () => {
|
||||
it("MTIME_META_KEY is pi-mtime", () => {
|
||||
expect(MTIME_META_KEY).toBe("pi-mtime");
|
||||
});
|
||||
|
||||
it("list paginates via continuation tokens and parses pi-mtime", async () => {
|
||||
const objectsByKey: Record<string, { Metadata?: Record<string, string> }> = {
|
||||
"pi/agent/a.json": { Metadata: { [MTIME_META_KEY]: "1500" } },
|
||||
"pi/agent/b.json": { Metadata: {} },
|
||||
};
|
||||
let listCallCount = 0;
|
||||
const client = makeMockClient(async (cmd) => {
|
||||
const name = (cmd as { constructor: { name: string } }).constructor.name;
|
||||
if (name === "HeadObjectCommand") {
|
||||
const key = (cmd as { input: { Key: string } }).input.Key;
|
||||
return objectsByKey[key] ?? {};
|
||||
}
|
||||
// ListObjectsV2Command — return two pages
|
||||
listCallCount++;
|
||||
if (listCallCount === 1) {
|
||||
return {
|
||||
Contents: [
|
||||
{ Key: "pi/agent/a.json", Size: 10, LastModified: new Date(1000) },
|
||||
],
|
||||
NextContinuationToken: "t1",
|
||||
IsTruncated: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
Contents: [
|
||||
{ Key: "pi/agent/b.json", Size: 20, LastModified: new Date(3000) },
|
||||
],
|
||||
IsTruncated: false,
|
||||
};
|
||||
});
|
||||
|
||||
const wrapper = createS3Wrapper(config, client);
|
||||
const result = await wrapper.list("pi/agent/");
|
||||
expect(result.map((r) => r.key).sort()).toEqual([
|
||||
"pi/agent/a.json",
|
||||
"pi/agent/b.json",
|
||||
]);
|
||||
// a.json uses pi-mtime metadata (1500); b.json falls back to LastModified (3000).
|
||||
const map = new Map<string, S3Object>(result.map((r) => [r.key, r]));
|
||||
expect(map.get("pi/agent/a.json")?.mtime).toBe(1500);
|
||||
expect(map.get("pi/agent/b.json")?.mtime).toBe(3000);
|
||||
});
|
||||
|
||||
it("put attaches pi-mtime metadata", async () => {
|
||||
let captured: unknown;
|
||||
const client = makeMockClient(async (cmd) => {
|
||||
captured = cmd;
|
||||
return {};
|
||||
});
|
||||
const wrapper = createS3Wrapper(config, client);
|
||||
await wrapper.put("pi/agent/x.json", Buffer.from("hello"), 12345);
|
||||
expect((captured as { input: Record<string, unknown> }).input).toMatchObject({
|
||||
Bucket: "homelab",
|
||||
Key: "pi/agent/x.json",
|
||||
Metadata: { [MTIME_META_KEY]: "12345" },
|
||||
});
|
||||
});
|
||||
|
||||
it("get returns body + mtime from metadata", async () => {
|
||||
const client = makeMockClient(async () => ({
|
||||
Body: {
|
||||
transformToByteArray: async () =>
|
||||
new Uint8Array(Buffer.from("hello")),
|
||||
},
|
||||
Metadata: { [MTIME_META_KEY]: "9999" },
|
||||
LastModified: new Date(5000),
|
||||
}));
|
||||
const wrapper = createS3Wrapper(config, client);
|
||||
const result = await wrapper.get("pi/agent/x.json");
|
||||
expect(result.body.toString()).toBe("hello");
|
||||
expect(result.mtime).toBe(9999);
|
||||
});
|
||||
|
||||
it("get falls back to LastModified when pi-mtime metadata is absent", async () => {
|
||||
const client = makeMockClient(async () => ({
|
||||
Body: {
|
||||
transformToByteArray: async () => new Uint8Array(Buffer.from("h")),
|
||||
},
|
||||
Metadata: {},
|
||||
LastModified: new Date(5000),
|
||||
}));
|
||||
const wrapper = createS3Wrapper(config, client);
|
||||
expect((await wrapper.get("pi/agent/x.json")).mtime).toBe(5000);
|
||||
});
|
||||
|
||||
it("delete sends DeleteObjectCommand with bucket+key", async () => {
|
||||
let captured: unknown;
|
||||
const client = makeMockClient(async (cmd) => {
|
||||
captured = cmd;
|
||||
return {};
|
||||
});
|
||||
const wrapper = createS3Wrapper(config, client);
|
||||
await wrapper.delete("pi/agent/x.json");
|
||||
expect((captured as { input: Record<string, unknown> }).input).toMatchObject({
|
||||
Bucket: "homelab",
|
||||
Key: "pi/agent/x.json",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
import {
|
||||
DeleteObjectCommand,
|
||||
GetObjectCommand,
|
||||
HeadObjectCommand,
|
||||
ListObjectsV2Command,
|
||||
PutObjectCommand,
|
||||
S3Client,
|
||||
} from "@aws-sdk/client-s3";
|
||||
import type { SyncConfig } from "./config.js";
|
||||
|
||||
export const MTIME_META_KEY = "pi-mtime";
|
||||
|
||||
export interface S3Object {
|
||||
key: string;
|
||||
mtime: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface S3Wrapper {
|
||||
list(prefix: string): Promise<S3Object[]>;
|
||||
get(key: string): Promise<{ body: Buffer; mtime: number }>;
|
||||
put(key: string, body: Buffer, mtimeMs: number): Promise<void>;
|
||||
delete(key: string): Promise<void>;
|
||||
}
|
||||
|
||||
interface MinimalS3Client {
|
||||
send(cmd: unknown): Promise<unknown>;
|
||||
}
|
||||
|
||||
export function createS3Client(config: SyncConfig): S3Client {
|
||||
return new S3Client({
|
||||
region: config.region,
|
||||
endpoint: config.endpoint,
|
||||
credentials: {
|
||||
accessKeyId: config.accessKeyId,
|
||||
secretAccessKey: config.secretAccessKey,
|
||||
},
|
||||
forcePathStyle: config.forcePathStyle,
|
||||
});
|
||||
}
|
||||
|
||||
function parseMtime(metadata: Record<string, string> | undefined, fallback: Date | undefined): number {
|
||||
const raw = metadata?.[MTIME_META_KEY];
|
||||
if (raw !== undefined) {
|
||||
const n = Number(raw);
|
||||
if (Number.isFinite(n)) return n;
|
||||
}
|
||||
return fallback ? fallback.getTime() : 0;
|
||||
}
|
||||
|
||||
export function createS3Wrapper(
|
||||
config: SyncConfig,
|
||||
client: MinimalS3Client = createS3Client(config),
|
||||
): S3Wrapper {
|
||||
const bucket = config.bucket;
|
||||
|
||||
async function list(prefix: string): Promise<S3Object[]> {
|
||||
const results: S3Object[] = [];
|
||||
let token: string | undefined;
|
||||
do {
|
||||
const resp = (await client.send(
|
||||
new ListObjectsV2Command({
|
||||
Bucket: bucket,
|
||||
Prefix: prefix,
|
||||
ContinuationToken: token,
|
||||
}),
|
||||
)) as {
|
||||
Contents?: { Key?: string; Size?: number; LastModified?: Date }[];
|
||||
NextContinuationToken?: string;
|
||||
IsTruncated?: boolean;
|
||||
};
|
||||
for (const obj of resp.Contents ?? []) {
|
||||
if (!obj.Key) continue;
|
||||
const head = (await client.send(
|
||||
new HeadObjectCommand({ Bucket: bucket, Key: obj.Key }),
|
||||
)) as { Metadata?: Record<string, string> };
|
||||
results.push({
|
||||
key: obj.Key,
|
||||
mtime: parseMtime(head.Metadata, obj.LastModified),
|
||||
size: obj.Size ?? 0,
|
||||
});
|
||||
}
|
||||
token = resp.IsTruncated ? resp.NextContinuationToken : undefined;
|
||||
} while (token);
|
||||
return results;
|
||||
}
|
||||
|
||||
async function get(key: string): Promise<{ body: Buffer; mtime: number }> {
|
||||
const resp = (await client.send(
|
||||
new GetObjectCommand({ Bucket: bucket, Key: key }),
|
||||
)) as {
|
||||
Body: { transformToByteArray(): Promise<Uint8Array> };
|
||||
Metadata?: Record<string, string>;
|
||||
LastModified?: Date;
|
||||
};
|
||||
const bytes = await resp.Body.transformToByteArray();
|
||||
return {
|
||||
body: Buffer.from(bytes),
|
||||
mtime: parseMtime(resp.Metadata, resp.LastModified),
|
||||
};
|
||||
}
|
||||
|
||||
async function put(key: string, body: Buffer, mtimeMs: number): Promise<void> {
|
||||
await client.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
Body: body,
|
||||
Metadata: { [MTIME_META_KEY]: String(Math.floor(mtimeMs)) },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function deleteObject(key: string): Promise<void> {
|
||||
await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));
|
||||
}
|
||||
|
||||
return { list, get, put, delete: deleteObject };
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { computeDiff, computePrune, executeSync, executePrune } from "./sync.js";
|
||||
import type { S3Wrapper } from "./s3.js";
|
||||
import type { Manifest } from "./manifest.js";
|
||||
|
||||
describe("computeDiff", () => {
|
||||
it("only-local → upload", () => {
|
||||
const local = new Map([["a", { mtime: 100, size: 1 }]]);
|
||||
const remote = new Map();
|
||||
const actions = computeDiff(local, remote);
|
||||
expect(actions).toEqual([
|
||||
{ kind: "upload", relPath: "a", reason: "only-local", mtime: 100, size: 1 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("only-remote → download", () => {
|
||||
const local = new Map();
|
||||
const remote = new Map([["a", { mtime: 100, size: 1 }]]);
|
||||
expect(computeDiff(local, remote)).toEqual([
|
||||
{ kind: "download", relPath: "a", reason: "only-remote", mtime: 100, size: 1 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("equal mtimes within 1s tolerance → skip", () => {
|
||||
const local = new Map([["a", { mtime: 1000, size: 1 }]]);
|
||||
const remote = new Map([["a", { mtime: 1500, size: 1 }]]);
|
||||
const actions = computeDiff(local, remote);
|
||||
expect(actions).toEqual([{ kind: "skip", relPath: "a", reason: "tolerance" }]);
|
||||
});
|
||||
|
||||
it("local newer beyond tolerance → upload", () => {
|
||||
const local = new Map([["a", { mtime: 5000, size: 1 }]]);
|
||||
const remote = new Map([["a", { mtime: 1000, size: 1 }]]);
|
||||
expect(computeDiff(local, remote)).toEqual([
|
||||
{ kind: "upload", relPath: "a", reason: "local-newer", mtime: 5000, size: 1 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("remote newer beyond tolerance → download", () => {
|
||||
const local = new Map([["a", { mtime: 1000, size: 1 }]]);
|
||||
const remote = new Map([["a", { mtime: 5000, size: 1 }]]);
|
||||
expect(computeDiff(local, remote)).toEqual([
|
||||
{ kind: "download", relPath: "a", reason: "remote-newer", mtime: 5000, size: 1 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("results are sorted by relPath for determinism", () => {
|
||||
const local = new Map([["z", { mtime: 1, size: 1 }]]);
|
||||
const remote = new Map([["a", { mtime: 1, size: 1 }]]);
|
||||
const actions = computeDiff(local, remote);
|
||||
expect(actions.map((a) => a.relPath)).toEqual(["a", "z"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("computePrune", () => {
|
||||
const manifest: Manifest = {
|
||||
version: 1,
|
||||
entries: {
|
||||
"agent/keep.json": { mtime: 1, size: 1 },
|
||||
"agent/orphan.json": { mtime: 1, size: 1 },
|
||||
},
|
||||
};
|
||||
|
||||
it("returns delete-remote for manifest entries missing locally", () => {
|
||||
const local = new Map([["keep.json", { mtime: 1, size: 1 }]]);
|
||||
const actions = computePrune(local, manifest, "");
|
||||
expect(actions).toEqual([
|
||||
{ kind: "delete-remote", relPath: "orphan.json", key: "agent/orphan.json" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("leaves alone manifest entries whose local file still exists", () => {
|
||||
const local = new Map([
|
||||
["keep.json", { mtime: 1, size: 1 }],
|
||||
["orphan.json", { mtime: 1, size: 1 }],
|
||||
]);
|
||||
expect(computePrune(local, manifest, "")).toEqual([]);
|
||||
});
|
||||
|
||||
it("ignores manifest entries that fall outside the configured prefix", () => {
|
||||
const altManifest: Manifest = {
|
||||
version: 1,
|
||||
entries: { "other/agent/foo.json": { mtime: 1, size: 1 } },
|
||||
};
|
||||
const local = new Map();
|
||||
expect(computePrune(local, altManifest, "pi")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── executeSync / executePrune integration tests with mocked S3 + tmp dir ───
|
||||
|
||||
let tmpHome: string;
|
||||
let originalHome: string | undefined;
|
||||
|
||||
function mkMockS3(): {
|
||||
wrapper: S3Wrapper;
|
||||
objects: Map<string, { body: Buffer; mtime: number; size: number }>;
|
||||
} {
|
||||
const objects = new Map<string, { body: Buffer; mtime: number; size: number }>();
|
||||
const wrapper: S3Wrapper = {
|
||||
list: vi.fn(async (prefix: string) => {
|
||||
const out = [];
|
||||
for (const [key, val] of objects) {
|
||||
if (key.startsWith(prefix)) out.push({ key, mtime: val.mtime, size: val.size });
|
||||
}
|
||||
return out;
|
||||
}),
|
||||
get: vi.fn(async (key: string) => {
|
||||
const obj = objects.get(key);
|
||||
if (!obj) throw new Error(`NoSuchKey: ${key}`);
|
||||
return { body: obj.body, mtime: obj.mtime };
|
||||
}),
|
||||
put: vi.fn(async (key: string, body: Buffer, mtimeMs: number) => {
|
||||
objects.set(key, { body, mtime: mtimeMs, size: body.length });
|
||||
}),
|
||||
delete: vi.fn(async (key: string) => {
|
||||
objects.delete(key);
|
||||
}),
|
||||
};
|
||||
return { wrapper, objects };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
originalHome = process.env.HOME;
|
||||
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "pi-sync-exec-"));
|
||||
process.env.HOME = tmpHome;
|
||||
fs.mkdirSync(path.join(tmpHome, ".pi", "agent"), { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalHome === undefined) delete process.env.HOME;
|
||||
else process.env.HOME = originalHome;
|
||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("executeSync", () => {
|
||||
function writeLocal(rel: string, body: string, mtime: number): void {
|
||||
const abs = path.join(tmpHome, ".pi", "agent", rel);
|
||||
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
||||
fs.writeFileSync(abs, body);
|
||||
fs.utimesSync(abs, mtime / 1000, mtime / 1000);
|
||||
}
|
||||
|
||||
it("pull mode downloads only-remote files (atomic write) and updates manifest", async () => {
|
||||
const { wrapper, objects } = mkMockS3();
|
||||
objects.set("agent/settings.json", {
|
||||
body: Buffer.from('{"x":1}'),
|
||||
mtime: 1000,
|
||||
size: 7,
|
||||
});
|
||||
const report = await executeSync({
|
||||
mode: "pull",
|
||||
s3: wrapper,
|
||||
prefix: "",
|
||||
});
|
||||
expect(report.pulled).toBe(1);
|
||||
expect(report.errors).toBe(0);
|
||||
const settings = fs.readFileSync(
|
||||
path.join(tmpHome, ".pi", "agent", "settings.json"),
|
||||
"utf8",
|
||||
);
|
||||
expect(settings).toBe('{"x":1}');
|
||||
// No leftover .tmp files
|
||||
expect(
|
||||
fs
|
||||
.readdirSync(path.join(tmpHome, ".pi", "agent"))
|
||||
.filter((f) => f.endsWith(".tmp")),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("push mode uploads only-local files", async () => {
|
||||
const { wrapper, objects } = mkMockS3();
|
||||
writeLocal("settings.json", '{"x":1}', 1000);
|
||||
const report = await executeSync({ mode: "push", s3: wrapper, prefix: "" });
|
||||
expect(report.pushed).toBe(1);
|
||||
expect(objects.has("agent/settings.json")).toBe(true);
|
||||
expect(objects.get("agent/settings.json")?.body.toString()).toBe('{"x":1}');
|
||||
});
|
||||
|
||||
it("sync mode resolves conflicts by mtime", async () => {
|
||||
const { wrapper, objects } = mkMockS3();
|
||||
writeLocal("settings.json", "LOCAL", 5000);
|
||||
objects.set("agent/settings.json", {
|
||||
body: Buffer.from("REMOTE"),
|
||||
mtime: 1000,
|
||||
size: 6,
|
||||
});
|
||||
writeLocal("k8s.json", "LOCAL2", 1000);
|
||||
objects.set("agent/k8s.json", {
|
||||
body: Buffer.from("REMOTE2"),
|
||||
mtime: 5000,
|
||||
size: 7,
|
||||
});
|
||||
await executeSync({ mode: "sync", s3: wrapper, prefix: "" });
|
||||
// settings.json: local newer → uploaded
|
||||
expect(objects.get("agent/settings.json")?.body.toString()).toBe("LOCAL");
|
||||
// k8s.json: remote newer → downloaded
|
||||
expect(
|
||||
fs.readFileSync(path.join(tmpHome, ".pi", "agent", "k8s.json"), "utf8"),
|
||||
).toBe("REMOTE2");
|
||||
});
|
||||
|
||||
it("partial pull failure: manifest only records successfully-downloaded entries", async () => {
|
||||
const { wrapper, objects } = mkMockS3();
|
||||
objects.set("agent/settings.json", { body: Buffer.from("S"), mtime: 1, size: 1 });
|
||||
objects.set("agent/k8s.json", { body: Buffer.from("K"), mtime: 1, size: 1 });
|
||||
(wrapper.get as ReturnType<typeof vi.fn>).mockImplementation(
|
||||
async (key: string) => {
|
||||
if (key === "agent/k8s.json") throw new Error("boom");
|
||||
return { body: Buffer.from("S"), mtime: 1 };
|
||||
},
|
||||
);
|
||||
const report = await executeSync({ mode: "pull", s3: wrapper, prefix: "" });
|
||||
expect(report.errors).toBe(1);
|
||||
const { readManifest } = await import("./manifest.js");
|
||||
const m = readManifest();
|
||||
expect(Object.keys(m.entries).sort()).toEqual(["agent/settings.json"]);
|
||||
});
|
||||
|
||||
it("never touches auth.json or .sync-manifest.json", async () => {
|
||||
const { wrapper, objects } = mkMockS3();
|
||||
writeLocal("auth.json", "SECRET", 1000);
|
||||
writeLocal("settings.json", "OK", 1000);
|
||||
await executeSync({ mode: "push", s3: wrapper, prefix: "" });
|
||||
expect(objects.has("agent/auth.json")).toBe(false);
|
||||
expect(objects.has("agent/settings.json")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("executePrune", () => {
|
||||
function writeLocal(rel: string, body: string): void {
|
||||
const abs = path.join(tmpHome, ".pi", "agent", rel);
|
||||
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
||||
fs.writeFileSync(abs, body);
|
||||
}
|
||||
|
||||
it("deletes remote keys that were in manifest but missing locally", async () => {
|
||||
const { wrapper, objects } = mkMockS3();
|
||||
const { writeManifest } = await import("./manifest.js");
|
||||
writeLocal("settings.json", "K");
|
||||
writeManifest({
|
||||
version: 1,
|
||||
entries: {
|
||||
"agent/settings.json": { mtime: 1, size: 1 },
|
||||
"agent/k8s.json": { mtime: 1, size: 1 },
|
||||
},
|
||||
});
|
||||
objects.set("agent/k8s.json", {
|
||||
body: Buffer.from("X"),
|
||||
mtime: 1,
|
||||
size: 1,
|
||||
});
|
||||
const report = await executePrune({ s3: wrapper, prefix: "" });
|
||||
expect(report.deleted).toBe(1);
|
||||
expect(objects.has("agent/k8s.json")).toBe(false);
|
||||
});
|
||||
|
||||
it("leaves S3-only-never-pulled keys alone", async () => {
|
||||
const { wrapper, objects } = mkMockS3();
|
||||
objects.set("agent/never-saw.json", {
|
||||
body: Buffer.from("X"),
|
||||
mtime: 1,
|
||||
size: 1,
|
||||
});
|
||||
// Empty manifest — never pulled.
|
||||
const report = await executePrune({ s3: wrapper, prefix: "" });
|
||||
expect(report.deleted).toBe(0);
|
||||
expect(objects.has("agent/never-saw.json")).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,229 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
||||
import {
|
||||
EXCLUDED_FILES,
|
||||
keyForRelPath,
|
||||
relPathForKey,
|
||||
walkLocalFiles,
|
||||
} from "./paths.js";
|
||||
import { readManifest, writeManifest, type Manifest } from "./manifest.js";
|
||||
import type { S3Wrapper } from "./s3.js";
|
||||
|
||||
export interface FileInfo {
|
||||
mtime: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export type Action =
|
||||
| { kind: "upload"; relPath: string; reason: "only-local" | "local-newer"; mtime: number; size: number }
|
||||
| { kind: "download"; relPath: string; reason: "only-remote" | "remote-newer"; mtime: number; size: number }
|
||||
| { kind: "skip"; relPath: string; reason: "tolerance" }
|
||||
| { kind: "delete-remote"; relPath: string; key: string };
|
||||
|
||||
export type SyncMode = "pull" | "push" | "sync";
|
||||
|
||||
export interface SyncReport {
|
||||
mode: SyncMode | "prune";
|
||||
actions: { action: Action; status: "done" | "error"; error?: string }[];
|
||||
pushed: number;
|
||||
pulled: number;
|
||||
skipped: number;
|
||||
deleted: number;
|
||||
errors: number;
|
||||
}
|
||||
|
||||
const MTIME_TOLERANCE_MS = 1000;
|
||||
|
||||
export function computeDiff(
|
||||
local: Map<string, FileInfo>,
|
||||
remote: Map<string, FileInfo>,
|
||||
): Action[] {
|
||||
const allKeys = new Set([...local.keys(), ...remote.keys()]);
|
||||
const sorted = [...allKeys].sort();
|
||||
const actions: Action[] = [];
|
||||
for (const relPath of sorted) {
|
||||
const l = local.get(relPath);
|
||||
const r = remote.get(relPath);
|
||||
if (l && !r) {
|
||||
actions.push({ kind: "upload", relPath, reason: "only-local", mtime: l.mtime, size: l.size });
|
||||
} else if (!l && r) {
|
||||
actions.push({ kind: "download", relPath, reason: "only-remote", mtime: r.mtime, size: r.size });
|
||||
} else if (l && r) {
|
||||
const delta = Math.abs(l.mtime - r.mtime);
|
||||
if (delta <= MTIME_TOLERANCE_MS) {
|
||||
actions.push({ kind: "skip", relPath, reason: "tolerance" });
|
||||
} else if (l.mtime > r.mtime) {
|
||||
actions.push({ kind: "upload", relPath, reason: "local-newer", mtime: l.mtime, size: l.size });
|
||||
} else {
|
||||
actions.push({ kind: "download", relPath, reason: "remote-newer", mtime: r.mtime, size: r.size });
|
||||
}
|
||||
}
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
|
||||
export function computePrune(
|
||||
local: Map<string, FileInfo>,
|
||||
manifest: Manifest,
|
||||
prefix: string,
|
||||
): Action[] {
|
||||
const actions: Action[] = [];
|
||||
for (const [key] of Object.entries(manifest.entries)) {
|
||||
const relPath = relPathForKey(prefix, key);
|
||||
if (relPath === undefined) continue;
|
||||
if (local.has(relPath)) continue;
|
||||
actions.push({ kind: "delete-remote", relPath, key });
|
||||
}
|
||||
return actions.sort((a, b) => a.relPath.localeCompare(b.relPath));
|
||||
}
|
||||
|
||||
async function statLocal(agentDir: string, relPath: string): Promise<FileInfo | undefined> {
|
||||
try {
|
||||
const s = await fs.promises.stat(path.join(agentDir, relPath));
|
||||
return { mtime: s.mtimeMs, size: s.size };
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function buildLocalMap(agentDir: string): Promise<Map<string, FileInfo>> {
|
||||
const files = await walkLocalFiles(agentDir);
|
||||
const map = new Map<string, FileInfo>();
|
||||
for (const rel of files) {
|
||||
const info = await statLocal(agentDir, rel);
|
||||
if (info) map.set(rel, info);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
async function buildRemoteMap(s3: S3Wrapper, prefix: string): Promise<Map<string, FileInfo>> {
|
||||
const objects = await s3.list(prefix === "" ? "agent/" : `${prefix}/agent/`);
|
||||
const map = new Map<string, FileInfo>();
|
||||
for (const obj of objects) {
|
||||
const rel = relPathForKey(prefix, obj.key);
|
||||
if (rel === undefined) continue;
|
||||
const base = path.basename(rel);
|
||||
if (EXCLUDED_FILES.includes(rel) || EXCLUDED_FILES.includes(base)) continue;
|
||||
map.set(rel, { mtime: obj.mtime, size: obj.size });
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
async function atomicWrite(absPath: string, body: Buffer, mtimeMs: number): Promise<void> {
|
||||
await fs.promises.mkdir(path.dirname(absPath), { recursive: true });
|
||||
const tmp = `${absPath}.tmp`;
|
||||
await fs.promises.writeFile(tmp, body);
|
||||
const mtimeSec = mtimeMs / 1000;
|
||||
await fs.promises.utimes(tmp, mtimeSec, mtimeSec);
|
||||
await fs.promises.rename(tmp, absPath);
|
||||
}
|
||||
|
||||
export async function executeSync(opts: {
|
||||
mode: SyncMode;
|
||||
s3: S3Wrapper;
|
||||
prefix: string;
|
||||
}): Promise<SyncReport> {
|
||||
const { mode, s3, prefix } = opts;
|
||||
const agentDir = getAgentDir();
|
||||
const local = await buildLocalMap(agentDir);
|
||||
const remote = await buildRemoteMap(s3, prefix);
|
||||
const allActions = computeDiff(local, remote);
|
||||
|
||||
const filtered = allActions.filter((a) => {
|
||||
if (mode === "pull") return a.kind === "download" || a.kind === "skip";
|
||||
if (mode === "push") return a.kind === "upload" || a.kind === "skip";
|
||||
return true;
|
||||
});
|
||||
|
||||
const manifest = readManifest();
|
||||
const report: SyncReport = {
|
||||
mode,
|
||||
actions: [],
|
||||
pushed: 0,
|
||||
pulled: 0,
|
||||
skipped: 0,
|
||||
deleted: 0,
|
||||
errors: 0,
|
||||
};
|
||||
|
||||
for (const action of filtered) {
|
||||
try {
|
||||
if (action.kind === "upload") {
|
||||
const abs = path.join(agentDir, action.relPath);
|
||||
const body = await fs.promises.readFile(abs);
|
||||
const key = keyForRelPath(prefix, action.relPath);
|
||||
await s3.put(key, body, action.mtime);
|
||||
manifest.entries[key] = { mtime: action.mtime, size: action.size };
|
||||
report.pushed++;
|
||||
report.actions.push({ action, status: "done" });
|
||||
} else if (action.kind === "download") {
|
||||
const key = keyForRelPath(prefix, action.relPath);
|
||||
const { body, mtime } = await s3.get(key);
|
||||
const abs = path.join(agentDir, action.relPath);
|
||||
await atomicWrite(abs, body, mtime);
|
||||
manifest.entries[key] = { mtime, size: body.length };
|
||||
report.pulled++;
|
||||
report.actions.push({ action, status: "done" });
|
||||
} else if (action.kind === "skip") {
|
||||
const key = keyForRelPath(prefix, action.relPath);
|
||||
const info = local.get(action.relPath);
|
||||
if (info) manifest.entries[key] = { mtime: info.mtime, size: info.size };
|
||||
report.skipped++;
|
||||
report.actions.push({ action, status: "done" });
|
||||
}
|
||||
} catch (err) {
|
||||
report.errors++;
|
||||
report.actions.push({
|
||||
action,
|
||||
status: "error",
|
||||
error: (err as Error).message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
writeManifest(manifest);
|
||||
return report;
|
||||
}
|
||||
|
||||
export async function executePrune(opts: {
|
||||
s3: S3Wrapper;
|
||||
prefix: string;
|
||||
}): Promise<SyncReport> {
|
||||
const { s3, prefix } = opts;
|
||||
const agentDir = getAgentDir();
|
||||
const local = await buildLocalMap(agentDir);
|
||||
const manifest = readManifest();
|
||||
const actions = computePrune(local, manifest, prefix);
|
||||
|
||||
const report: SyncReport = {
|
||||
mode: "prune",
|
||||
actions: [],
|
||||
pushed: 0,
|
||||
pulled: 0,
|
||||
skipped: 0,
|
||||
deleted: 0,
|
||||
errors: 0,
|
||||
};
|
||||
|
||||
for (const action of actions) {
|
||||
if (action.kind !== "delete-remote") continue;
|
||||
try {
|
||||
await s3.delete(action.key);
|
||||
delete manifest.entries[action.key];
|
||||
report.deleted++;
|
||||
report.actions.push({ action, status: "done" });
|
||||
} catch (err) {
|
||||
report.errors++;
|
||||
report.actions.push({
|
||||
action,
|
||||
status: "error",
|
||||
error: (err as Error).message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
writeManifest(manifest);
|
||||
return report;
|
||||
}
|
||||
Generated
+331
-195
@@ -8,6 +8,7 @@
|
||||
"name": "pi-customizations",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.1057.0",
|
||||
"@mozilla/readability": "^0.5.0",
|
||||
"linkedom": "^0.18.0",
|
||||
"turndown": "^7.2.0",
|
||||
@@ -55,7 +56,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz",
|
||||
"integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@aws-crypto/util": "^5.2.0",
|
||||
"@aws-sdk/types": "^3.222.0",
|
||||
@@ -65,12 +65,36 @@
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-crypto/crc32c": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz",
|
||||
"integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-crypto/util": "^5.2.0",
|
||||
"@aws-sdk/types": "^3.222.0",
|
||||
"tslib": "^2.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-crypto/sha1-browser": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz",
|
||||
"integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-crypto/supports-web-crypto": "^5.2.0",
|
||||
"@aws-crypto/util": "^5.2.0",
|
||||
"@aws-sdk/types": "^3.222.0",
|
||||
"@aws-sdk/util-locate-window": "^3.0.0",
|
||||
"@smithy/util-utf8": "^2.0.0",
|
||||
"tslib": "^2.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-crypto/sha256-browser": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz",
|
||||
"integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@aws-crypto/sha256-js": "^5.2.0",
|
||||
"@aws-crypto/supports-web-crypto": "^5.2.0",
|
||||
@@ -86,7 +110,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz",
|
||||
"integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@aws-crypto/util": "^5.2.0",
|
||||
"@aws-sdk/types": "^3.222.0",
|
||||
@@ -101,7 +124,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz",
|
||||
"integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.2"
|
||||
}
|
||||
@@ -111,7 +133,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz",
|
||||
"integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@aws-sdk/types": "^3.222.0",
|
||||
"@smithy/util-utf8": "^2.0.0",
|
||||
@@ -152,18 +173,61 @@
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/core": {
|
||||
"version": "3.974.9",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.9.tgz",
|
||||
"integrity": "sha512-bXxosFunr+v/kqNb99r1NRkrVBha7CG036fRSpWGbC1A/e363XFQN6wcZMx7MYTdRr1tYwNnkrWX2xc1rT3BCQ==",
|
||||
"node_modules/@aws-sdk/client-s3": {
|
||||
"version": "3.1057.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1057.0.tgz",
|
||||
"integrity": "sha512-4MV5+ph7WSLEqStKYdWf2EIHIvLpPzV8xN98jWSVJfUpp5j7T8dyN3AROPPsKWvCme8hbx1ybCjtK76ALCZUYg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@aws-sdk/types": "^3.973.8",
|
||||
"@aws-sdk/xml-builder": "^3.972.23",
|
||||
"@smithy/core": "^3.24.1",
|
||||
"@smithy/signature-v4": "^5.4.1",
|
||||
"@smithy/types": "^4.14.1",
|
||||
"@aws-crypto/sha1-browser": "5.2.0",
|
||||
"@aws-crypto/sha256-browser": "5.2.0",
|
||||
"@aws-crypto/sha256-js": "5.2.0",
|
||||
"@aws-sdk/core": "^3.974.15",
|
||||
"@aws-sdk/credential-provider-node": "^3.972.47",
|
||||
"@aws-sdk/middleware-bucket-endpoint": "^3.972.17",
|
||||
"@aws-sdk/middleware-expect-continue": "^3.972.14",
|
||||
"@aws-sdk/middleware-flexible-checksums": "^3.974.23",
|
||||
"@aws-sdk/middleware-location-constraint": "^3.972.11",
|
||||
"@aws-sdk/middleware-sdk-s3": "^3.972.44",
|
||||
"@aws-sdk/middleware-ssec": "^3.972.11",
|
||||
"@aws-sdk/signature-v4-multi-region": "^3.996.30",
|
||||
"@aws-sdk/types": "^3.973.9",
|
||||
"@smithy/core": "^3.24.5",
|
||||
"@smithy/fetch-http-handler": "^5.4.5",
|
||||
"@smithy/node-http-handler": "^4.7.5",
|
||||
"@smithy/types": "^4.14.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/core": {
|
||||
"version": "3.974.15",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.15.tgz",
|
||||
"integrity": "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/types": "^3.973.9",
|
||||
"@aws-sdk/xml-builder": "^3.972.26",
|
||||
"@aws/lambda-invoke-store": "^0.2.2",
|
||||
"@smithy/core": "^3.24.5",
|
||||
"@smithy/signature-v4": "^5.4.5",
|
||||
"@smithy/types": "^4.14.2",
|
||||
"bowser": "^2.11.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/crc64-nvme": {
|
||||
"version": "3.972.9",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.9.tgz",
|
||||
"integrity": "sha512-P+QGozmXn2mZZI7sDgk+aUm+RTI61MPSFB+Ir2vjEjEbEsE4e7hYtzrDvAUxZy9ko81h53e11+F/GYlvwDkaOQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^4.14.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -171,16 +235,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-env": {
|
||||
"version": "3.972.35",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.35.tgz",
|
||||
"integrity": "sha512-WkFQ8BedszVomhh/Zzs8WwnE/XBmTqZjoQVB8u/4zH6kZCjouXZpPpb93gD8m0EZmzAl7dxHE/y+yDpuKzNCMw==",
|
||||
"version": "3.972.41",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.41.tgz",
|
||||
"integrity": "sha512-n1EbJ98yvPWWdHZZv8bRBMqqDQJrtgtxyJ4xLy2Uqrh25BCOZQ7nnS1CsFXvuH8r0b0KVHDZEGEH5FxmEMP8jg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.9",
|
||||
"@aws-sdk/types": "^3.973.8",
|
||||
"@smithy/core": "^3.24.1",
|
||||
"@smithy/types": "^4.14.1",
|
||||
"@aws-sdk/core": "^3.974.15",
|
||||
"@aws-sdk/types": "^3.973.9",
|
||||
"@smithy/core": "^3.24.5",
|
||||
"@smithy/types": "^4.14.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -188,18 +251,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-http": {
|
||||
"version": "3.972.37",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.37.tgz",
|
||||
"integrity": "sha512-ylx0ZJTU+2eNcvXQ69VNR3TVSYa/ibpvdK717/NxqR9aXRMn2QRWZaiI8aa5yY/fOWZ5mknSmxGaVxxtdwv3EA==",
|
||||
"version": "3.972.43",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.43.tgz",
|
||||
"integrity": "sha512-TT76RN1NkI9WoyZqCNxOw6/WBMF7pYOTJcXbMokNFU+euSG40Kaf/t/FhDACVZWP+43wEM6ZynIPIkzS1wR1iA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.9",
|
||||
"@aws-sdk/types": "^3.973.8",
|
||||
"@smithy/core": "^3.24.1",
|
||||
"@smithy/fetch-http-handler": "^5.4.1",
|
||||
"@smithy/node-http-handler": "^4.7.1",
|
||||
"@smithy/types": "^4.14.1",
|
||||
"@aws-sdk/core": "^3.974.15",
|
||||
"@aws-sdk/types": "^3.973.9",
|
||||
"@smithy/core": "^3.24.5",
|
||||
"@smithy/fetch-http-handler": "^5.4.5",
|
||||
"@smithy/node-http-handler": "^4.7.5",
|
||||
"@smithy/types": "^4.14.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -207,24 +269,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-ini": {
|
||||
"version": "3.972.39",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.39.tgz",
|
||||
"integrity": "sha512-QhRSrdkk+Gq0AFIylpiI0N6lcJqFYV9Jtr4Luz5FpYOYbjJSfyTG6iLhnK/UPIgN1Jnon8WAmSC//16XYGvwkA==",
|
||||
"version": "3.972.46",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.46.tgz",
|
||||
"integrity": "sha512-hvcgcwOiS0nb2XFb5Op1Pz/vYaWz5K8kKullziGpdNRuG0NwzRXseuPt2CoBqknHGaSPVesu1aOn2OcctEYdCA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.9",
|
||||
"@aws-sdk/credential-provider-env": "^3.972.35",
|
||||
"@aws-sdk/credential-provider-http": "^3.972.37",
|
||||
"@aws-sdk/credential-provider-login": "^3.972.39",
|
||||
"@aws-sdk/credential-provider-process": "^3.972.35",
|
||||
"@aws-sdk/credential-provider-sso": "^3.972.39",
|
||||
"@aws-sdk/credential-provider-web-identity": "^3.972.39",
|
||||
"@aws-sdk/nested-clients": "^3.997.7",
|
||||
"@aws-sdk/types": "^3.973.8",
|
||||
"@smithy/core": "^3.24.1",
|
||||
"@smithy/credential-provider-imds": "^4.3.1",
|
||||
"@smithy/types": "^4.14.1",
|
||||
"@aws-sdk/core": "^3.974.15",
|
||||
"@aws-sdk/credential-provider-env": "^3.972.41",
|
||||
"@aws-sdk/credential-provider-http": "^3.972.43",
|
||||
"@aws-sdk/credential-provider-login": "^3.972.45",
|
||||
"@aws-sdk/credential-provider-process": "^3.972.41",
|
||||
"@aws-sdk/credential-provider-sso": "^3.972.45",
|
||||
"@aws-sdk/credential-provider-web-identity": "^3.972.45",
|
||||
"@aws-sdk/nested-clients": "^3.997.13",
|
||||
"@aws-sdk/types": "^3.973.9",
|
||||
"@smithy/core": "^3.24.5",
|
||||
"@smithy/credential-provider-imds": "^4.3.6",
|
||||
"@smithy/types": "^4.14.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -232,17 +293,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-login": {
|
||||
"version": "3.972.39",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.39.tgz",
|
||||
"integrity": "sha512-1hU0NtC04QbFIuoBuF4aQ2A97GsSE5/A0ZJpDijwexsBREIQ4KPRYl3v/FfKCPBYsaTeGjkOFx5nLhWHY24LOw==",
|
||||
"version": "3.972.45",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.45.tgz",
|
||||
"integrity": "sha512-MZQv4SNjByk1iOKmrqmzcUF/uCB05wjvEHyXKxmGQTUANTIVayX6HPUF0bzkWLvtnkH7sAn9kUCfkXbSpj9sDA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.9",
|
||||
"@aws-sdk/nested-clients": "^3.997.7",
|
||||
"@aws-sdk/types": "^3.973.8",
|
||||
"@smithy/core": "^3.24.1",
|
||||
"@smithy/types": "^4.14.1",
|
||||
"@aws-sdk/core": "^3.974.15",
|
||||
"@aws-sdk/nested-clients": "^3.997.13",
|
||||
"@aws-sdk/types": "^3.973.9",
|
||||
"@smithy/core": "^3.24.5",
|
||||
"@smithy/types": "^4.14.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -250,22 +310,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-node": {
|
||||
"version": "3.972.40",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.40.tgz",
|
||||
"integrity": "sha512-ZgrQaGkpyTlVSCCsffzijVg+KgftTAWYvI5Otc36J/4jNiHb+7MmBiJIR0a5AHLvifC92PiYHt5pijP0dswd1w==",
|
||||
"version": "3.972.47",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.47.tgz",
|
||||
"integrity": "sha512-HrId+C0DWA5qDIyLG64/kjUB2RNtPypxmABnIctK+TA1P1kHlOYoE/Wf5T5tKOMKgb08P7k/zNyhvfJ3lh5Oag==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-provider-env": "^3.972.35",
|
||||
"@aws-sdk/credential-provider-http": "^3.972.37",
|
||||
"@aws-sdk/credential-provider-ini": "^3.972.39",
|
||||
"@aws-sdk/credential-provider-process": "^3.972.35",
|
||||
"@aws-sdk/credential-provider-sso": "^3.972.39",
|
||||
"@aws-sdk/credential-provider-web-identity": "^3.972.39",
|
||||
"@aws-sdk/types": "^3.973.8",
|
||||
"@smithy/core": "^3.24.1",
|
||||
"@smithy/credential-provider-imds": "^4.3.1",
|
||||
"@smithy/types": "^4.14.1",
|
||||
"@aws-sdk/credential-provider-env": "^3.972.41",
|
||||
"@aws-sdk/credential-provider-http": "^3.972.43",
|
||||
"@aws-sdk/credential-provider-ini": "^3.972.46",
|
||||
"@aws-sdk/credential-provider-process": "^3.972.41",
|
||||
"@aws-sdk/credential-provider-sso": "^3.972.45",
|
||||
"@aws-sdk/credential-provider-web-identity": "^3.972.45",
|
||||
"@aws-sdk/types": "^3.973.9",
|
||||
"@smithy/core": "^3.24.5",
|
||||
"@smithy/credential-provider-imds": "^4.3.6",
|
||||
"@smithy/types": "^4.14.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -273,16 +332,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-process": {
|
||||
"version": "3.972.35",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.35.tgz",
|
||||
"integrity": "sha512-hNj1rAwZWT1vfz54BwH8FUWxZuqStrM25Q5LEIwn2erHPMRVAjLlpZqEbCEEqS99eEEOhdeetnS0WeNa3iYeEQ==",
|
||||
"version": "3.972.41",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.41.tgz",
|
||||
"integrity": "sha512-7I/n1zkysouLOWvkEhjNEP4vMnD2v4kzzr3/3QBdrripEpn7ap1/I5DF3Hou1SUqkKWo1f3oPGMyFAA1FAMvsQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.9",
|
||||
"@aws-sdk/types": "^3.973.8",
|
||||
"@smithy/core": "^3.24.1",
|
||||
"@smithy/types": "^4.14.1",
|
||||
"@aws-sdk/core": "^3.974.15",
|
||||
"@aws-sdk/types": "^3.973.9",
|
||||
"@smithy/core": "^3.24.5",
|
||||
"@smithy/types": "^4.14.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -290,18 +348,34 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-sso": {
|
||||
"version": "3.972.39",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.39.tgz",
|
||||
"integrity": "sha512-mwIPNPldyCZkvHozb6E0X/vuQLN1UCjcA6MwUf1gaO7EwghCmuNZXatq0L3zptKFvPC4Nds7+WFUkifI1XmbSw==",
|
||||
"version": "3.972.45",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.45.tgz",
|
||||
"integrity": "sha512-oHgbz/eFD8IKiksqDsz9ZMU4A59BpQq4QwJedBnGD80ZqYcHPPHZBwjBnxLVkB7iRVVHWpDclR8yWdD2PkQIUA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.9",
|
||||
"@aws-sdk/nested-clients": "^3.997.7",
|
||||
"@aws-sdk/token-providers": "3.1046.0",
|
||||
"@aws-sdk/types": "^3.973.8",
|
||||
"@smithy/core": "^3.24.1",
|
||||
"@smithy/types": "^4.14.1",
|
||||
"@aws-sdk/core": "^3.974.15",
|
||||
"@aws-sdk/nested-clients": "^3.997.13",
|
||||
"@aws-sdk/token-providers": "3.1056.0",
|
||||
"@aws-sdk/types": "^3.973.9",
|
||||
"@smithy/core": "^3.24.5",
|
||||
"@smithy/types": "^4.14.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": {
|
||||
"version": "3.1056.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1056.0.tgz",
|
||||
"integrity": "sha512-81duvlltQlsfn5K+o8zILcystBRdbT1G2JJYVCML5NZHBz4CL/zf+sAemCtBh/uh6RQUMyInGeZLQ7/8igZhbA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.15",
|
||||
"@aws-sdk/nested-clients": "^3.997.13",
|
||||
"@aws-sdk/types": "^3.973.9",
|
||||
"@smithy/core": "^3.24.5",
|
||||
"@smithy/types": "^4.14.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -309,17 +383,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-web-identity": {
|
||||
"version": "3.972.39",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.39.tgz",
|
||||
"integrity": "sha512-b9HT8CnpyPVn1hU14Q7ihjwSPlRzToYmRYJxRd5jNHEZ43lrIhoLaTT8JmfQQt5j5M8rTX1iN1X8mvu0SM1dXA==",
|
||||
"version": "3.972.45",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.45.tgz",
|
||||
"integrity": "sha512-CDhzKdb2onv5bpnjn/acgdNmJOQthPDLsPizU7rZflsEcgMMp8Mlri+U5hdxf8ldvZJpvM3vLU6D56vfJm5AMQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.9",
|
||||
"@aws-sdk/nested-clients": "^3.997.7",
|
||||
"@aws-sdk/types": "^3.973.8",
|
||||
"@smithy/core": "^3.24.1",
|
||||
"@smithy/types": "^4.14.1",
|
||||
"@aws-sdk/core": "^3.974.15",
|
||||
"@aws-sdk/nested-clients": "^3.997.13",
|
||||
"@aws-sdk/types": "^3.973.9",
|
||||
"@smithy/core": "^3.24.5",
|
||||
"@smithy/types": "^4.14.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -342,6 +415,22 @@
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/middleware-bucket-endpoint": {
|
||||
"version": "3.972.17",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.17.tgz",
|
||||
"integrity": "sha512-lbDmWuHenc+kiwCNrxz4MyN6nkxCWyTXPIWuspJN0ibziu+8CXci7vI1bK9MAkwy8cwJOEXNu0gBM5S0uTGRIg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.15",
|
||||
"@aws-sdk/types": "^3.973.9",
|
||||
"@smithy/core": "^3.24.5",
|
||||
"@smithy/types": "^4.14.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/middleware-eventstream": {
|
||||
"version": "3.972.11",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.11.tgz",
|
||||
@@ -358,6 +447,41 @@
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/middleware-expect-continue": {
|
||||
"version": "3.972.14",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.14.tgz",
|
||||
"integrity": "sha512-3TNFEVGO4sWZj9TEXOCZLzGEctXHnaO4fk2EQ8KVaboTbwHmEPEQrm17Xb9koImUIXEw0sgi2xtHjg7LuTS3rA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/types": "^3.973.9",
|
||||
"@smithy/core": "^3.24.5",
|
||||
"@smithy/types": "^4.14.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/middleware-flexible-checksums": {
|
||||
"version": "3.974.23",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.23.tgz",
|
||||
"integrity": "sha512-4nPKARo2lfKvQGUt2fPA5NlS/mEohckdxpuC9ecbjVfj7B7NFFYHeTg+Bf5BEQwdn3yRfUIzFiEkPp8Yuaw3wA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-crypto/crc32": "5.2.0",
|
||||
"@aws-crypto/crc32c": "5.2.0",
|
||||
"@aws-crypto/util": "5.2.0",
|
||||
"@aws-sdk/core": "^3.974.15",
|
||||
"@aws-sdk/crc64-nvme": "^3.972.9",
|
||||
"@aws-sdk/types": "^3.973.9",
|
||||
"@smithy/core": "^3.24.5",
|
||||
"@smithy/types": "^4.14.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/middleware-host-header": {
|
||||
"version": "3.972.11",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.11.tgz",
|
||||
@@ -374,6 +498,20 @@
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/middleware-location-constraint": {
|
||||
"version": "3.972.11",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.11.tgz",
|
||||
"integrity": "sha512-hkfspNUP4criAH6ton6BGKgnm5dZx+7bUOy1YqlTfejDeUPAM23D81q/IX+hdlS3KUsfwGz5ADTqZWKBEUpf4A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/types": "^3.973.9",
|
||||
"@smithy/types": "^4.14.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/middleware-logger": {
|
||||
"version": "3.972.10",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.10.tgz",
|
||||
@@ -406,6 +544,37 @@
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/middleware-sdk-s3": {
|
||||
"version": "3.972.44",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.44.tgz",
|
||||
"integrity": "sha512-8HQsRg1NpX8vR4vNl1E8pyLnqZroq9VSL2vZQVSgBqp6wv6365LzYD08/c9FFh/9FTg7YRc7aTtEmXF0ir/pqg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.15",
|
||||
"@aws-sdk/signature-v4-multi-region": "^3.996.30",
|
||||
"@aws-sdk/types": "^3.973.9",
|
||||
"@smithy/core": "^3.24.5",
|
||||
"@smithy/types": "^4.14.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/middleware-ssec": {
|
||||
"version": "3.972.11",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.11.tgz",
|
||||
"integrity": "sha512-7PQvGNhtveKlvVqNahqWx5yrwxP7ecwAoB1dYBf8eKwfo2tzzCbNnW+q2nO3N066ktQaB4iBQbDRWtizm+amoQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/types": "^3.973.9",
|
||||
"@smithy/types": "^4.14.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/middleware-user-agent": {
|
||||
"version": "3.972.39",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.39.tgz",
|
||||
@@ -444,29 +613,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/nested-clients": {
|
||||
"version": "3.997.7",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.7.tgz",
|
||||
"integrity": "sha512-jT2AXOODobQfTYGC2SChMSnZ/voIcRV/LHlY1suyhY1bdgP/voKkhEg8Ci1jiGQ4lBiaso5BEAV3ZWWpPTfmYA==",
|
||||
"version": "3.997.13",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.13.tgz",
|
||||
"integrity": "sha512-2pA6eyb5nSo/ZD2cayhOTEMoGQYgspq0RI05GDLkzQ3ajZ6isS6waV6E92Am/hz4LIlLUTrbwPLurJ/fuiHvkg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@aws-crypto/sha256-browser": "5.2.0",
|
||||
"@aws-crypto/sha256-js": "5.2.0",
|
||||
"@aws-sdk/core": "^3.974.9",
|
||||
"@aws-sdk/middleware-host-header": "^3.972.11",
|
||||
"@aws-sdk/middleware-logger": "^3.972.10",
|
||||
"@aws-sdk/middleware-recursion-detection": "^3.972.12",
|
||||
"@aws-sdk/middleware-user-agent": "^3.972.39",
|
||||
"@aws-sdk/region-config-resolver": "^3.972.14",
|
||||
"@aws-sdk/signature-v4-multi-region": "^3.996.26",
|
||||
"@aws-sdk/types": "^3.973.8",
|
||||
"@aws-sdk/util-endpoints": "^3.996.9",
|
||||
"@aws-sdk/util-user-agent-browser": "^3.972.11",
|
||||
"@aws-sdk/util-user-agent-node": "^3.973.25",
|
||||
"@smithy/core": "^3.24.1",
|
||||
"@smithy/fetch-http-handler": "^5.4.1",
|
||||
"@smithy/node-http-handler": "^4.7.1",
|
||||
"@smithy/types": "^4.14.1",
|
||||
"@aws-sdk/core": "^3.974.15",
|
||||
"@aws-sdk/signature-v4-multi-region": "^3.996.30",
|
||||
"@aws-sdk/types": "^3.973.9",
|
||||
"@smithy/core": "^3.24.5",
|
||||
"@smithy/fetch-http-handler": "^5.4.5",
|
||||
"@smithy/node-http-handler": "^4.7.5",
|
||||
"@smithy/types": "^4.14.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -490,16 +650,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/signature-v4-multi-region": {
|
||||
"version": "3.996.26",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.26.tgz",
|
||||
"integrity": "sha512-2N62veqdMZBCwQUHsbhtnaovOFjOa5Dn3dAD1nRqFTUXR4QmirT3HZnfus/L1DS08Vm5CkoKmL0iMVt6YbqEag==",
|
||||
"version": "3.996.30",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.30.tgz",
|
||||
"integrity": "sha512-HULDLMVzkmTSEv6//7kx2kRevp/VYUpm8hJNNFbmhxDn0fUiGTxVcM9yg31TukvTq8nyOBDUN2gH0o5IRbKjdw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@aws-sdk/types": "^3.973.8",
|
||||
"@smithy/core": "^3.24.1",
|
||||
"@smithy/signature-v4": "^5.4.1",
|
||||
"@smithy/types": "^4.14.1",
|
||||
"@aws-sdk/types": "^3.973.9",
|
||||
"@smithy/signature-v4": "^5.4.5",
|
||||
"@smithy/types": "^4.14.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -525,13 +683,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/types": {
|
||||
"version": "3.973.8",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz",
|
||||
"integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==",
|
||||
"version": "3.973.9",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.9.tgz",
|
||||
"integrity": "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@smithy/types": "^4.14.1",
|
||||
"@smithy/types": "^4.14.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -559,7 +716,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz",
|
||||
"integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
@@ -606,15 +762,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/xml-builder": {
|
||||
"version": "3.972.23",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.23.tgz",
|
||||
"integrity": "sha512-A0YmgYFv+hTI9c17Ntvd2hSehm9bmJfkb+ggADBwVKA8H/3+Jx94SzR2qOB9bAA9WFeDqnfz9PKKQ+D+YAKomA==",
|
||||
"version": "3.972.26",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.26.tgz",
|
||||
"integrity": "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@nodable/entities": "2.1.0",
|
||||
"@smithy/types": "^4.14.1",
|
||||
"fast-xml-parser": "5.7.2",
|
||||
"@smithy/types": "^4.14.2",
|
||||
"fast-xml-parser": "5.7.3",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -626,7 +780,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz",
|
||||
"integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
@@ -1458,17 +1611,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@nodable/entities": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz",
|
||||
"integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.1.tgz",
|
||||
"integrity": "sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/nodable"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
@@ -1941,14 +2093,13 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@smithy/core": {
|
||||
"version": "3.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.2.tgz",
|
||||
"integrity": "sha512-IKS7qX59fAGCYBmt5JChcDswQDupZqT2Yn2ZBA3UgTlsjRNNkQzZobbn95xoAAdtTyJmBiJB3Y02qR3rgy3Zog==",
|
||||
"version": "3.24.5",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.5.tgz",
|
||||
"integrity": "sha512-Kt8phUg45M15EjhYAbZ+fFikYneijLu9Liugz8ZsYz2i8j0hzGv27LWKpEHYRfvj+LyCOSijpcR/2i8RouV+cA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@aws-crypto/crc32": "5.2.0",
|
||||
"@smithy/types": "^4.14.1",
|
||||
"@smithy/types": "^4.14.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1956,14 +2107,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/credential-provider-imds": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.2.tgz",
|
||||
"integrity": "sha512-iYr9ekBjmZ+FwkiHEopqGscBbl78X62cq3p5Dd0eC+gNd7fybNZFQQdDuOQjTVmFymleuA8YRWZnuXWZ8B3kKA==",
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.6.tgz",
|
||||
"integrity": "sha512-tHhdiWZfG1ZIh2YcRfPJmY2gHcBmqbAzqm3ER4TIDFYsSEqTD5tICT7cgQ/kI8LRakxp12myOYyK68XPn7MnHw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.24.2",
|
||||
"@smithy/types": "^4.14.1",
|
||||
"@smithy/core": "^3.24.5",
|
||||
"@smithy/types": "^4.14.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1971,14 +2121,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/fetch-http-handler": {
|
||||
"version": "5.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.2.tgz",
|
||||
"integrity": "sha512-3wF40g8OOCA5BnwQUvwtzZqYBbWWftDjpAlWIUo6Yld3ZzJaMAKqg7MWQBPjE8oLaqvZQUE7tVGlZPsae6A4bQ==",
|
||||
"version": "5.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.5.tgz",
|
||||
"integrity": "sha512-SK3VMeH0fibgdTg2QeB+O4p7Yy/2E5HBOHJeC58FshkDdeuX8lOgO7PfjYfLyPLP1ch55j91cQqKBzDS0mRjSQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.24.2",
|
||||
"@smithy/types": "^4.14.1",
|
||||
"@smithy/core": "^3.24.5",
|
||||
"@smithy/types": "^4.14.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1990,7 +2139,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz",
|
||||
"integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
@@ -1999,14 +2147,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/node-http-handler": {
|
||||
"version": "4.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.2.tgz",
|
||||
"integrity": "sha512-EdksTZ8UXYxGUgQ4mpIKrHoaj9WVGsp66TpZuixLAz1Jex8YDLnS4RH9ktGED5aOpN0OJlEtrsC9IGt76go1eA==",
|
||||
"version": "4.7.5",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.5.tgz",
|
||||
"integrity": "sha512-3dA9TQ+ybRSZ/m0wnbZhiBy4Dezjgq1Ib/ZZrYTpJDBgpoLLU/SDzZc/g0x0MNAdOJe1wPcM+x2PBRmoOur+Sw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.24.2",
|
||||
"@smithy/types": "^4.14.1",
|
||||
"@smithy/core": "^3.24.5",
|
||||
"@smithy/types": "^4.14.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2014,14 +2161,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/signature-v4": {
|
||||
"version": "5.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.2.tgz",
|
||||
"integrity": "sha512-1km1OjdLRFuITWpCPofjFqzZ+tbeWuB72ZhcYjbjkCxZ21tTPfIs4GUxRrelMyKMLxLghGD58RENnXorU/O8cw==",
|
||||
"version": "5.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.5.tgz",
|
||||
"integrity": "sha512-QBJKWGqIknH0dc9LWpfH1mkdokAx6iXYN3UcQ3eY6uIEyScuoQAhfl94ge7ozUy9WgFUdE8xsvwBjaYBbWmPNA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.24.2",
|
||||
"@smithy/types": "^4.14.1",
|
||||
"@smithy/core": "^3.24.5",
|
||||
"@smithy/types": "^4.14.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2029,11 +2175,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/types": {
|
||||
"version": "4.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz",
|
||||
"integrity": "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==",
|
||||
"version": "4.14.2",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz",
|
||||
"integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
@@ -2046,7 +2191,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz",
|
||||
"integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@smithy/is-array-buffer": "^2.2.0",
|
||||
"tslib": "^2.6.2"
|
||||
@@ -2060,7 +2204,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz",
|
||||
"integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@smithy/util-buffer-from": "^2.2.0",
|
||||
"tslib": "^2.6.2"
|
||||
@@ -2413,8 +2556,7 @@
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz",
|
||||
"integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "5.0.6",
|
||||
@@ -2954,16 +3096,15 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"path-expression-matcher": "^1.5.0",
|
||||
"xml-naming": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-xml-parser": {
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.2.tgz",
|
||||
"integrity": "sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==",
|
||||
"version": "5.7.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz",
|
||||
"integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -2971,10 +3112,9 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@nodable/entities": "^2.1.0",
|
||||
"fast-xml-builder": "^1.1.5",
|
||||
"fast-xml-builder": "^1.1.7",
|
||||
"path-expression-matcher": "^1.5.0",
|
||||
"strnum": "^2.2.3"
|
||||
},
|
||||
@@ -3808,7 +3948,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
@@ -4258,8 +4397,7 @@
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/strtok3": {
|
||||
"version": "10.3.5",
|
||||
@@ -4405,8 +4543,7 @@
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD",
|
||||
"peer": true
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/turndown": {
|
||||
"version": "7.2.4",
|
||||
@@ -4777,7 +4914,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.1057.0",
|
||||
"@mozilla/readability": "^0.5.0",
|
||||
"linkedom": "^0.18.0",
|
||||
"turndown": "^7.2.0",
|
||||
|
||||
Reference in New Issue
Block a user