diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..5aee6947 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,16 @@ +name: ci +on: + push: { branches: [main] } + pull_request: +jobs: + fast: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 # version pinned by packageManager in package.json + - uses: actions/setup-node@v4 + with: { node-version: 20, cache: pnpm } + - run: pnpm install --frozen-lockfile + - run: pnpm -r run build + - run: pnpm -r run typecheck + - run: pnpm --filter @amicode/amico-run test # includes the S31 grep-rule test diff --git a/.vscode/launch.json b/.vscode/launch.json index 49ecf6a0..8fa91fd5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,8 +5,8 @@ "name": "Run Extension (amicode-v2)", "type": "extensionHost", "request": "launch", - "args": ["--extensionDevelopmentPath=${workspaceFolder}"], - "outFiles": ["${workspaceFolder}/dist/**/*.js"] + "args": ["--extensionDevelopmentPath=${workspaceFolder}/packages/extension"], + "outFiles": ["${workspaceFolder}/packages/extension/dist/**/*.js"] } ] } diff --git a/bin/amico-run b/bin/amico-run deleted file mode 100755 index e7d4e6c3..00000000 --- a/bin/amico-run +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env bash -# ============================================================================ -# amico-run — CLI wrapper around spike_solve.jl for opencode's `bash` tool. -# -# Why: the v2 architecture went CLI-direct (no MCP, no callback HTTP). The -# LLM picks bash + amico-run, julia writes to a deterministic dir, and the -# VS Code extension watches that dir via fs.watch. One process boundary -# instead of three, no env propagation across them. -# -# Usage: -# amico-run --system -# --gate -# --pulse -# [--T-ns ] -# [--omega-cap ] -# [--max-iter ] -# [--seed ] -# [--solver ipopt] -# -# Side effects: -# 1. Creates /tmp/amicode-runs// (timestamp + random hex) -# 2. Updates /tmp/amicode-runs/latest -> -# 3. Spawns stdbuf + julia, streaming AMICODE_ITER lines to stdout. -# 4. On exit, prints a one-line summary the LLM can quote back. -# ============================================================================ -set -euo pipefail - -# --help short-circuit: don't fire julia just because someone probed. -for arg in "$@"; do - case "$arg" in - -h|--help|help) - cat <<'EOF' -amico-run — Amicode quantum optimal-control solve runner. - -USAGE - amico-run --system --gate - --pulse - [--T-ns ] [--omega-cap ] [--max-iter ] - [--seed ] [--solver ipopt] - -EXAMPLES - amico-run --system transmon --gate X --pulse zero-order --T-ns 24 - amico-run --system qubit --gate H --pulse zero-order - amico-run --system transmon --gate CNOT --pulse linear-spline --T-ns 200 - -OUTPUT - /tmp/amicode-runs// per-run dir (PNGs, formulation.md, result.toml) - /tmp/amicode-runs/latest symlink to most recent run dir - -The VS Code extension's Run Inspector watches /tmp/amicode-runs/latest/. -EOF - exit 0 - ;; - esac -done - -# Locate spike_solve.jl relative to this script. -SCRIPT="$(realpath "$0")" -SCRIPT_DIR="$(dirname "$SCRIPT")" -# amicode-v2/bin/amico-run → amicode/julia/spike_solve.jl (sibling tree) -JULIA_SCRIPT_DEFAULT="$(realpath "$SCRIPT_DIR/../../amicode/julia/spike_solve.jl")" -JULIA_PROJECT_DEFAULT="/tmp/amicode-spike-julia" - -JULIA_SCRIPT="${AMICO_JULIA_SCRIPT:-$JULIA_SCRIPT_DEFAULT}" -JULIA_PROJECT="${AMICO_JULIA_PROJECT:-$JULIA_PROJECT_DEFAULT}" - -if [[ ! -f "$JULIA_SCRIPT" ]]; then - echo "amico-run: julia script not found at $JULIA_SCRIPT" >&2 - echo "amico-run: set AMICO_JULIA_SCRIPT or place spike_solve.jl at that location." >&2 - exit 64 -fi -if [[ ! -d "$JULIA_PROJECT" ]]; then - echo "amico-run: julia project not found at $JULIA_PROJECT" >&2 - echo "amico-run: set AMICO_JULIA_PROJECT to your dev'd Piccolissimo env." >&2 - exit 64 -fi - -# Generate a runId, prep the output dir, swing the 'latest' symlink. -RUNS_ROOT="${AMICO_RUNS_ROOT:-/tmp/amicode-runs}" -mkdir -p "$RUNS_ROOT" -RUN_ID="r$(date +%Y%m%d-%H%M%S)-$(openssl rand -hex 2 2>/dev/null || printf '%04x' $RANDOM)" -OUT_DIR="$RUNS_ROOT/$RUN_ID" -mkdir -p "$OUT_DIR" - -# Atomic-ish symlink swap. -ln -sfn "$OUT_DIR" "$RUNS_ROOT/.latest.tmp" && mv -Tf "$RUNS_ROOT/.latest.tmp" "$RUNS_ROOT/latest" - -# Emit a one-line start record (the extension's file watcher picks this up -# as soon as the .start file lands in latest/). -{ - printf 'runId=%s\n' "$RUN_ID" - printf 'outputDir=%s\n' "$OUT_DIR" - printf 'started=%s\n' "$(date -Iseconds)" -} > "$OUT_DIR/.start" - -# Build the julia argv. We pass --output-dir explicitly; all other args -# go through as-is. spike_solve.jl silently swallows unknown flags. -JULIA_ARGS=( "--project=$JULIA_PROJECT" "$JULIA_SCRIPT" "--output-dir" "$OUT_DIR" "$@" ) - -# stdbuf -oL -eL bypasses Ipopt's C-side stdout buffering on Linux. Without -# it, AMICODE_ITER lines spool until program exit. -EXE="julia"; PRE=() -if command -v stdbuf >/dev/null 2>&1; then - PRE=( "stdbuf" "-oL" "-eL" ) -fi - -echo "amico-run: spawning $EXE ${JULIA_ARGS[*]}" >&2 -echo "amico-run: output dir = $OUT_DIR" >&2 -echo "amico-run: latest symlink -> $RUNS_ROOT/latest" >&2 - -# Tee julia stdout to both our stdout (for the LLM) and a log file (for -# the extension, in case the LLM truncates). -set +e -"${PRE[@]}" "$EXE" "${JULIA_ARGS[@]}" 2>&1 | tee "$OUT_DIR/run.log" -JULIA_RC=${PIPESTATUS[0]} -set -e - -# Final summary. The extension also reads result.toml; this is for the LLM. -if [[ -f "$OUT_DIR/result.toml" ]]; then - FID=$(grep -E '^fidelity\s*=' "$OUT_DIR/result.toml" | head -1 | awk -F= '{gsub(/ /,"",$2); print $2}') - ITERS=$(grep -E '^iterations\s*=' "$OUT_DIR/result.toml" | head -1 | awk -F= '{gsub(/ /,"",$2); print $2}') - WALL=$(grep -E '^wall_time_s\s*=' "$OUT_DIR/result.toml" | head -1 | awk -F= '{gsub(/ /,"",$2); print $2}') - echo - echo "amico-run: DONE runId=$RUN_ID F=$FID iter=$ITERS wall=${WALL}s outputDir=$OUT_DIR" -else - echo - echo "amico-run: FAILED runId=$RUN_ID rc=$JULIA_RC outputDir=$OUT_DIR (no result.toml)" -fi - -# Mark completion so the extension's watcher knows to read result.toml. -touch "$OUT_DIR/FINISHED" - -exit "$JULIA_RC" diff --git a/package.json b/package.json index f2cca778..e57da6a8 100644 --- a/package.json +++ b/package.json @@ -1,85 +1,10 @@ { - "name": "amicode-v2", - "displayName": "Amicode v2", - "description": "Amico research IDE — opencode-backed chat + native VS Code surfaces for vault, catalog, and live solve inspection.", - "version": "0.0.1", - "publisher": "harmoniqs", - "license": "Apache-2.0", + "name": "amicode-workspace", "private": true, - "engines": { - "vscode": "^1.95.0" - }, - "categories": ["Other"], - "activationEvents": [ - "onCommand:amicode.openChat", - "onCommand:amicode.openInspector", - "onView:amicode.runInspector", - "onView:amicode.vault", - "onStartupFinished" - ], - "main": "./dist/extension.js", - "contributes": { - "viewsContainers": { - "activitybar": [ - { - "id": "amicode", - "title": "Amicode", - "icon": "media/amico.svg" - } - ], - "panel": [ - { - "id": "amicode-panel", - "title": "Amicode", - "icon": "media/amico.svg" - } - ] - }, - "views": { - "amicode": [ - { "id": "amicode.vault", "name": "Vault", "type": "tree" }, - { "id": "amicode.catalog", "name": "Catalog", "type": "tree" }, - { "id": "amicode.armonia", "name": "Armonia", "type": "tree" } - ], - "amicode-panel": [ - { "id": "amicode.runInspector", "name": "Run Inspector", "type": "webview" } - ] - }, - "commands": [ - { "command": "amicode.openChat", "title": "Amicode: Open Chat" }, - { "command": "amicode.openInspector", "title": "Amicode: Open Run Inspector" }, - { "command": "amicode.restartServer", "title": "Amicode: Restart opencode server" } - ], - "configuration": { - "title": "Amicode", - "properties": { - "amicode.opencodeBinary": { - "type": "string", - "default": "opencode", - "description": "Path or command name for the opencode CLI." - }, - "amicode.juliaProject": { - "type": "string", - "default": "/tmp/amicode-spike-julia", - "description": "Julia project root (--project=...) the run_julia tool will use." - }, - "amicode.juliaScript": { - "type": "string", - "default": "", - "description": "Absolute path to spike_solve.jl. If empty, looks up via the extensionPath/../amicode/julia/spike_solve.jl convention." - } - } - } - }, "scripts": { - "build": "node esbuild.config.mjs", - "watch": "node esbuild.config.mjs --watch", - "typecheck": "tsc --noEmit" + "build": "pnpm -r run build", + "typecheck": "pnpm -r run typecheck", + "test": "pnpm -r run test" }, - "devDependencies": { - "@types/node": "^22.0.0", - "@types/vscode": "^1.95.0", - "esbuild": "^0.24.0", - "typescript": "^5.6.0" - } + "packageManager": "pnpm@9.15.9" } diff --git a/packages/amico-run/esbuild.config.mjs b/packages/amico-run/esbuild.config.mjs new file mode 100644 index 00000000..eb12aaa7 --- /dev/null +++ b/packages/amico-run/esbuild.config.mjs @@ -0,0 +1,17 @@ +import { build } from 'esbuild' +import { chmodSync } from 'node:fs' + +await build({ + entryPoints: ['src/cli.ts'], + bundle: true, + platform: 'node', + target: 'node20', + // ESM, not CJS: the package is "type": "module", so node executes dist/amico-run.js + // as ESM — a CJS bundle would die on `require is not defined in ES module scope`. + format: 'esm', + outfile: 'dist/amico-run.js', + banner: { js: '#!/usr/bin/env node' }, + sourcemap: true, + logLevel: 'info', +}) +chmodSync('dist/amico-run.js', 0o755) diff --git a/packages/amico-run/launcher/amico-run b/packages/amico-run/launcher/amico-run new file mode 100755 index 00000000..a9b35f71 --- /dev/null +++ b/packages/amico-run/launcher/amico-run @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Thin launcher: resolve node, exec the bundled orchestrator. No logic lives here. +set -euo pipefail +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ]; do # resolve symlink chains (node_modules/.bin) + DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)" + SOURCE="$(readlink "$SOURCE")" + [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" +done +DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)" +if ! command -v node >/dev/null 2>&1; then + echo "amico-run: node >= 20 not found on PATH (install node or fix PATH; see provisioning runbook)" >&2 + exit 64 +fi +exec node "$DIR/../dist/amico-run.js" "$@" diff --git a/packages/amico-run/package.json b/packages/amico-run/package.json new file mode 100644 index 00000000..b831f2d6 --- /dev/null +++ b/packages/amico-run/package.json @@ -0,0 +1,23 @@ +{ + "name": "@amicode/amico-run", + "version": "0.1.0", + "license": "Apache-2.0", + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "bin": { "amico-run": "./launcher/amico-run" }, + "engines": { "node": ">=20" }, + "scripts": { + "build": "node esbuild.config.mjs", + "typecheck": "tsc --noEmit", + "test": "vitest run --passWithNoTests --exclude '**/slow/**'", + "test:slow": "vitest run test/slow" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "esbuild": "^0.24.0", + "smol-toml": "^1.3.0", + "typescript": "^5.6.0", + "vitest": "^2.1.0" + } +} diff --git a/packages/amico-run/src/cli.ts b/packages/amico-run/src/cli.ts new file mode 100644 index 00000000..b00ddfbc --- /dev/null +++ b/packages/amico-run/src/cli.ts @@ -0,0 +1,82 @@ +import { existsSync } from 'node:fs' +import { join } from 'node:path' +import { LocalExecutor } from './local_executor.js' +import { ConfigError, type Finished, type SubmitOpts } from './types.js' + +const USAGE = `usage: amico-run [--executor local] [--lab ] + [--runs-root ] [--julia ] [--project ] [--sysimage ]` + +export async function main(argv: string[]): Promise { + let script: string | undefined + let executor = 'local' + const opts: SubmitOpts = { julia: {} } + + for (let i = 0; i < argv.length; i++) { + const a = argv[i] + const next = (): string => { + const v = argv[++i] + if (v === undefined) throw new ConfigError(`flag ${a} requires a value`) + return v + } + try { + switch (a) { + case '--help': case '-h': console.log(USAGE); return 0 + case '--executor': executor = next(); break + case '--lab': opts.lab = next(); break + case '--runs-root': opts.runsRoot = next(); break + case '--julia': opts.julia!.julia = next(); break + case '--project': opts.julia!.project = next(); break + case '--sysimage': opts.julia!.sysimage = next(); break + default: + if (a.startsWith('-')) { console.error(`amico-run: unknown flag ${a}\n${USAGE}`); return 64 } + if (script) { console.error(`amico-run: multiple scripts given`); return 64 } + script = a + } + } catch (e) { + if (e instanceof ConfigError) { console.error(`amico-run: ${e.message}`); return 64 } + throw e + } + } + if (!script) { console.error(`amico-run: no script given\n${USAGE}`); return 64 } + if (executor !== 'local') { console.error(`amico-run: only --executor local is supported in β`); return 64 } + + let handle + try { + handle = await new LocalExecutor().submit(script, opts) + } catch (e) { + if (e instanceof ConfigError) { console.error(`amico-run: ${e.message}`); return 64 } + throw e + } + + const onSignal = (): void => { void handle.abort() } + process.on('SIGINT', onSignal) + process.on('SIGTERM', onSignal) + + let fin: Finished | undefined + for await (const ev of handle.events) { + if (ev.kind === 'iter' || ev.kind === 'done') console.log(ev.raw) + else if (ev.kind === 'log') console.log(ev.line) + else fin = { status: ev.status, exitCode: ev.exitCode } + } + const f = fin ?? await handle.finished + + // FINISHED-write failure lane (spec §6 last row): verdict file must exist on disk + if (!existsSync(join(handle.runDir, 'FINISHED'))) { + console.error(`amico-run: FINISHED missing in ${handle.runDir} (write fault)`) + return 64 + } + // stdout protocol line — camelCase by design (spec §4) + console.log(`AMICODE_FINISHED status=${f.status} exitCode=${f.exitCode} runDir=${handle.runDir}`) + if (f.status === 'aborted') return 130 + if (f.status === 'completed') return 0 + return f.exitCode === 0 ? 1 : f.exitCode +} + +main(process.argv.slice(2)).then( + c => { process.exitCode = c }, + e => { + // Any unexpected throw is an orchestrator fault, not a solve failure → 64. + console.error(`amico-run: unexpected error: ${e instanceof Error ? e.stack ?? e.message : e}`) + process.exitCode = 64 + }, +) diff --git a/packages/amico-run/src/event_queue.ts b/packages/amico-run/src/event_queue.ts new file mode 100644 index 00000000..361b42f3 --- /dev/null +++ b/packages/amico-run/src/event_queue.ts @@ -0,0 +1,26 @@ +/** Push-based AsyncIterable: producer pushes, single consumer iterates. */ +export class EventQueue implements AsyncIterable { + private buf: T[] = [] + private waiters: Array<(r: IteratorResult) => void> = [] + private ended = false + + push(v: T): void { + if (this.ended) return // late producers (post-settle) are dropped, never buffered + const w = this.waiters.shift() + if (w) w({ value: v, done: false }) + else this.buf.push(v) + } + close(): void { + this.ended = true + for (const w of this.waiters.splice(0)) w({ value: undefined as never, done: true }) + } + [Symbol.asyncIterator](): AsyncIterator { + return { + next: (): Promise> => { + if (this.buf.length > 0) return Promise.resolve({ value: this.buf.shift()!, done: false }) + if (this.ended) return Promise.resolve({ value: undefined as never, done: true }) + return new Promise(res => this.waiters.push(res)) + }, + } + } +} diff --git a/packages/amico-run/src/index.ts b/packages/amico-run/src/index.ts new file mode 100644 index 00000000..f8af7d5f --- /dev/null +++ b/packages/amico-run/src/index.ts @@ -0,0 +1,6 @@ +export * from './types.js' +export * from './telemetry.js' +export * from './run_dir.js' +export * from './schemas.js' +export * from './event_queue.js' +export * from './local_executor.js' diff --git a/packages/amico-run/src/local_executor.ts b/packages/amico-run/src/local_executor.ts new file mode 100644 index 00000000..0ccba0fd --- /dev/null +++ b/packages/amico-run/src/local_executor.ts @@ -0,0 +1,136 @@ +import { spawn } from 'node:child_process' +import { accessSync, constants as fsConstants, createWriteStream, existsSync, mkdirSync } from 'node:fs' +import { constants as osConstants } from 'node:os' +import { delimiter, join, resolve } from 'node:path' +import * as readline from 'node:readline' +import { EventQueue } from './event_queue.js' +import { classifyLine } from './telemetry.js' +import { + appendIndex, defaultRunsRoot, deriveLabId, generateRunId, + updateLatest, writeFinished, writeManifest, +} from './run_dir.js' +import { + ConfigError, type Executor, type Finished, type RunEvent, type RunHandle, + type RunStatus, type SubmitOpts, +} from './types.js' + +import pkg from '../package.json' with { type: 'json' } +const ORCHESTRATOR_VERSION = pkg.version // single source of truth (esbuild inlines the JSON) + +function resolveExecutable(bin: string): void { + const candidates = bin.includes('/') + ? [resolve(bin)] + : (process.env.PATH ?? '').split(delimiter).filter(Boolean).map(d => join(d, bin)) + for (const c of candidates) { + try { accessSync(c, fsConstants.X_OK); return } catch { /* keep looking */ } + } + throw new ConfigError(`julia binary not found or not executable: ${bin}`) +} + +function signalCode(signal: NodeJS.Signals | null): number { + const n = signal ? (osConstants.signals as Record)[signal] : undefined + return 128 + (n ?? 1) +} + +export class LocalExecutor implements Executor { + async submit(scriptPath: string, opts: SubmitOpts = {}): Promise { + // ---- step 1 (spec §5): validate config; failures here create NO run dir ---- + const script = resolve(scriptPath) + if (!existsSync(script)) throw new ConfigError(`script not found: ${script}`) + const juliaBin = opts.julia?.julia ?? 'julia' + resolveExecutable(juliaBin) + const lab = opts.lab ?? 'default' + const labId = deriveLabId(lab) + const runsRoot = opts.runsRoot ?? defaultRunsRoot(labId) + try { mkdirSync(runsRoot, { recursive: true }) } catch (e) { + throw new ConfigError(`runs root not writable: ${runsRoot} (${(e as Error).message})`) + } + + // ---- steps 2–5: run dir, manifest FIRST, index, latest ---- + const runId = generateRunId(runsRoot) + const runDir = join(runsRoot, runId) + mkdirSync(runDir) + const createdAt = new Date().toISOString() + writeManifest(runDir, { + schema_version: '1', run_id: runId, script_path: script, + lab, lab_id: labId, created_at: createdAt, + orchestrator_version: ORCHESTRATOR_VERSION, + julia: { binary: juliaBin, project: opts.julia?.project, sysimage: opts.julia?.sysimage }, + }) + appendIndex(runsRoot, runId, createdAt, script) + updateLatest(runsRoot, runId) + + // ---- step 6: spawn julia, own process group, cwd = runDir ---- + const args: string[] = [] + if (opts.julia?.project) args.push(`--project=${opts.julia.project}`) + if (opts.julia?.sysimage) args.push(`--sysimage=${opts.julia.sysimage}`) + args.push(script) + + const events = new EventQueue() + const logStream = createWriteStream(join(runDir, 'run.log'), { flags: 'a' }) + let resolveFinished!: (f: Finished) => void + const finished = new Promise(r => { resolveFinished = r }) + + let settled = false + let aborting = false + const settle = (status: RunStatus, exitCode: number): void => { + if (settled) return + settled = true + try { + writeFinished(runDir, status, exitCode) // orchestrator verdict, atomic — overwrites + } catch (e) { // any FINISHED a script faked (spec §5 step 8) + process.stderr.write(`amico-run: failed to write FINISHED: ${(e as Error).message}\n`) + } + logStream.end() + events.push({ kind: 'finished', status, exitCode }) + events.close() + resolveFinished({ status, exitCode }) + } + + // stdbuf (spec §5 "where available") is deliberately omitted in β.1: the β.3 script + // convention prints with flush, and the fake-julia fixtures are node (line-flushed). + // If live ITER streaming degrades on a real lab machine, β.6's dry-run catches it. + const child = spawn(juliaBin, args, { + cwd: runDir, detached: true, stdio: ['ignore', 'pipe', 'pipe'], + }) + // spawn failure AFTER manifest exists → FINISHED{failed, 127} (spec §6) + child.on('error', () => settle('failed', 127)) + // 'close', NOT 'exit': close waits for stdout/stderr to drain, so every line event + // lands before settle() — the events stream must terminate ON the finished event (§3). + child.on('close', (code, signal) => { + const rc = code ?? signalCode(signal) + settle(aborting ? 'aborted' : rc === 0 ? 'completed' : 'failed', rc) + }) + + const onLine = (stream: 'stdout' | 'stderr') => (line: string): void => { + if (settled) return // belt-and-braces; 'close' ordering makes this rare + logStream.write(line + '\n') + events.push(classifyLine(line, stream)) + } + readline.createInterface({ input: child.stdout! }).on('line', onLine('stdout')) + readline.createInterface({ input: child.stderr! }).on('line', onLine('stderr')) + + const graceMs = opts.graceMs ?? 5000 + const abort = async (): Promise => { + if (settled) return + aborting = true + const killGroup = (sig: NodeJS.Signals): void => { + try { process.kill(-child.pid!, sig) } catch { /* already gone */ } + } + killGroup('SIGTERM') + const killer = setTimeout(() => killGroup('SIGKILL'), graceMs) + killer.unref() + await finished + clearTimeout(killer) + } + + return { runId, runDir, events, finished, abort } + } +} + +/** Spec §3: interface seam only — implementation is post-β. */ +export class RemoteExecutor implements Executor { + submit(): Promise { + return Promise.reject(new Error('RemoteExecutor: not implemented in β (D9 plan, Phase 2+)')) + } +} diff --git a/packages/amico-run/src/run_dir.ts b/packages/amico-run/src/run_dir.ts new file mode 100644 index 00000000..2c0f949c --- /dev/null +++ b/packages/amico-run/src/run_dir.ts @@ -0,0 +1,92 @@ +import { existsSync, writeFileSync, renameSync, appendFileSync, symlinkSync, rmSync } from 'node:fs' +import { randomBytes } from 'node:crypto' +import { homedir } from 'node:os' +import { join, dirname, basename, resolve } from 'node:path' +import { ConfigError, type RunStatus } from './types.js' + +const ID_RE = /^[a-z0-9][a-z0-9_-]*$/ + +/** Spec §3: id pointers verbatim; path pointers (contain "/" or end ".toml") + * derive the id from the parent directory name of the lab.toml. */ +export function deriveLabId(lab: string): string { + if (ID_RE.test(lab)) return lab + if (lab.includes('/') || lab.endsWith('.toml')) { + const id = basename(dirname(resolve(lab))) + if (ID_RE.test(id)) return id + throw new ConfigError(`cannot derive lab id from "${lab}": parent dir "${id}" is not a valid id`) + } + throw new ConfigError(`invalid lab pointer "${lab}" (want [a-z0-9][a-z0-9_-]* or a lab.toml path)`) +} + +export function defaultRunsRoot(labId: string): string { + return join(homedir(), '.amico', 'runs', labId) +} + +export function generateRunId(runsRoot: string, now = new Date()): string { + const p = (n: number, w = 2) => String(n).padStart(w, '0') + const ts = `${now.getUTCFullYear()}${p(now.getUTCMonth() + 1)}${p(now.getUTCDate())}` + + `-${p(now.getUTCHours())}${p(now.getUTCMinutes())}${p(now.getUTCSeconds())}Z` + for (;;) { + const id = `r${ts}-${randomBytes(2).toString('hex')}` + if (!existsSync(join(runsRoot, id))) return id + } +} + +/** Write-temp-then-rename in the same dir: a watcher can never observe a partial file. */ +export function atomicWriteFile(dir: string, name: string, content: string): void { + const tmp = join(dir, `.${name}.tmp-${process.pid}`) + writeFileSync(tmp, content) + renameSync(tmp, join(dir, name)) +} + +const ts = (s: string) => JSON.stringify(s) // JSON escaping is valid TOML basic-string + +export interface Manifest { + schema_version: '1' + run_id: string + script_path: string + lab: string + lab_id: string + created_at: string + orchestrator_version: string + julia: { binary: string; project?: string; sysimage?: string } +} + +export function writeManifest(runDir: string, m: Manifest): void { + const lines = [ + `schema_version = ${ts(m.schema_version)}`, + `run_id = ${ts(m.run_id)}`, + `script_path = ${ts(m.script_path)}`, + `lab = ${ts(m.lab)}`, + `lab_id = ${ts(m.lab_id)}`, + `created_at = ${ts(m.created_at)}`, + `orchestrator_version = ${ts(m.orchestrator_version)}`, + '', + '[julia]', + `binary = ${ts(m.julia.binary)}`, + ...(m.julia.project ? [`project = ${ts(m.julia.project)}`] : []), + ...(m.julia.sysimage ? [`sysimage = ${ts(m.julia.sysimage)}`] : []), + ] + atomicWriteFile(runDir, 'manifest.toml', lines.join('\n') + '\n') +} + +export function writeFinished(runDir: string, status: RunStatus, exitCode: number): void { + atomicWriteFile(runDir, 'FINISHED', `status = ${ts(status)}\nexit_code = ${exitCode}\n`) +} + +export function appendIndex(runsRoot: string, runId: string, createdAt: string, scriptPath: string): void { + // The index is a tab-separated, one-line-per-run log; a tab/newline in the + // (last-field) script path would corrupt it. Sanitize control chars to a + // space — manifest.toml holds the canonical, TOML-escaped script_path. + const safePath = scriptPath.replace(/[\t\r\n]/g, ' ') + appendFileSync(join(runsRoot, 'index'), `${runId}\t${createdAt}\t${safePath}\n`) +} + +export function updateLatest(runsRoot: string, runId: string): void { + // Scope the temp name to runId so concurrent same-lab submits don't race on + // a shared `.latest.tmp` (one would unlink the other's in-flight temp). + const tmp = join(runsRoot, `.latest.${runId}.tmp`) + rmSync(tmp, { force: true }) + symlinkSync(runId, tmp) + renameSync(tmp, join(runsRoot, 'latest')) +} diff --git a/packages/amico-run/src/schemas.ts b/packages/amico-run/src/schemas.ts new file mode 100644 index 00000000..53cc0ab3 --- /dev/null +++ b/packages/amico-run/src/schemas.ts @@ -0,0 +1,42 @@ +// PROVISIONAL shapes — superseded by Phase 0' SchemaPackage (plan task 0.1). +// Field names here ARE the contract and must survive that migration (spec §7). +export interface Validation { ok: boolean; errors: string[] } + +type Obj = Record +const isObj = (v: unknown): v is Obj => typeof v === 'object' && v !== null && !Array.isArray(v) + +function check(errors: string[], cond: boolean, path: string, want: string): void { + if (!cond) errors.push(`${path}: expected ${want}`) +} + +export function validateManifest(v: unknown): Validation { + const errors: string[] = [] + if (!isObj(v)) return { ok: false, errors: ['manifest: expected table'] } + check(errors, v.schema_version === '1', 'schema_version', '"1"') + for (const k of ['run_id', 'script_path', 'lab', 'lab_id', 'created_at', 'orchestrator_version']) + check(errors, typeof v[k] === 'string' && (v[k] as string).length > 0, k, 'non-empty string') + check(errors, isObj(v.julia), 'julia', 'table') + if (isObj(v.julia)) { + check(errors, typeof v.julia.binary === 'string', 'julia.binary', 'string') + for (const k of ['project', 'sysimage'] as const) + check(errors, v.julia[k] === undefined || typeof v.julia[k] === 'string', `julia.${k}`, 'string if present') + } + return { ok: errors.length === 0, errors } +} + +export function validateFinished(v: unknown): Validation { + const errors: string[] = [] + if (!isObj(v)) return { ok: false, errors: ['FINISHED: expected table'] } + check(errors, v.status === 'completed' || v.status === 'failed' || v.status === 'aborted', + 'status', 'completed|failed|aborted') + check(errors, Number.isInteger(v.exit_code), 'exit_code', 'integer') + return { ok: errors.length === 0, errors } +} + +export function validateResult(v: unknown): Validation { + const errors: string[] = [] + if (!isObj(v)) return { ok: false, errors: ['result: expected table'] } + check(errors, typeof v.fidelity === 'number', 'fidelity', 'number') + check(errors, Number.isInteger(v.iterations), 'iterations', 'integer') + return { ok: errors.length === 0, errors } +} diff --git a/packages/amico-run/src/telemetry.ts b/packages/amico-run/src/telemetry.ts new file mode 100644 index 00000000..b88159f9 --- /dev/null +++ b/packages/amico-run/src/telemetry.ts @@ -0,0 +1,14 @@ +import type { RunEvent } from './types.js' + +export function classifyLine(line: string, stream: 'stdout' | 'stderr'): RunEvent { + if (stream === 'stdout' && line.startsWith('AMICODE_ITER')) { + const fields: Record = {} + for (const tok of line.slice('AMICODE_ITER'.length).trim().split(/\s+/)) { + const eq = tok.indexOf('=') + if (eq > 0) fields[tok.slice(0, eq)] = tok.slice(eq + 1) + } + return { kind: 'iter', raw: line, fields } + } + if (stream === 'stdout' && /^DONE(\s|$)/.test(line)) return { kind: 'done', raw: line } + return { kind: 'log', stream, line } +} diff --git a/packages/amico-run/src/types.ts b/packages/amico-run/src/types.ts new file mode 100644 index 00000000..069d7561 --- /dev/null +++ b/packages/amico-run/src/types.ts @@ -0,0 +1,37 @@ +export type RunStatus = 'completed' | 'failed' | 'aborted' + +export interface JuliaOpts { + julia?: string // julia binary path; default "julia" from PATH + project?: string // --project= + sysimage?: string // --sysimage= +} + +export interface SubmitOpts { + lab?: string // lab POINTER (id or lab.toml path), passed through verbatim; default "default" + runsRoot?: string // default: ~/.amico/runs// + julia?: JuliaOpts + graceMs?: number // abort SIGTERM→SIGKILL grace; default 5000. Test knob, NOT exposed in the CLI. +} + +export type RunEvent = + | { kind: 'iter'; raw: string; fields: Record } + | { kind: 'done'; raw: string } + | { kind: 'log'; stream: 'stdout' | 'stderr'; line: string } + | { kind: 'finished'; status: RunStatus; exitCode: number } + +export interface Finished { status: RunStatus; exitCode: number } + +export interface RunHandle { + runId: string + runDir: string + events: AsyncIterable // terminates after the 'finished' event + finished: Promise // never rejects + abort(): Promise // idempotent +} + +export interface Executor { + submit(scriptPath: string, opts?: SubmitOpts): Promise +} + +/** Exit-64-class fault: bad config, nothing solver-related ran. */ +export class ConfigError extends Error {} diff --git a/packages/amico-run/test/abort.test.ts b/packages/amico-run/test/abort.test.ts new file mode 100644 index 00000000..8946c0b1 --- /dev/null +++ b/packages/amico-run/test/abort.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect } from 'vitest' +import { join } from 'node:path' +import { tmpRoot, fakeJulia, readToml } from './helpers.js' +import { LocalExecutor } from '../src/local_executor.js' + +const HANG = `setInterval(() => {}, 1000)` // dies on SIGTERM → 143 +// prints READY only after the SIGTERM handler is installed — the test must not abort +// before then, or the signal hits node's default disposition during interpreter boot (→ 143) +const HANG_IGNORE = `process.on('SIGTERM', () => {}); console.log('READY'); setInterval(() => {}, 1000)` + +describe('abort lane (spec §3/§6)', () => { + it('abort() on a hanging run → FINISHED{aborted, 143} (SIGTERM)', async () => { + const root = tmpRoot() + const h = await new LocalExecutor().submit(fakeJulia(root, 's.jl', ''), { + runsRoot: join(root, 'runs'), julia: { julia: fakeJulia(root, 'j', HANG) }, + }) + await h.abort() + expect(await h.finished).toEqual({ status: 'aborted', exitCode: 143 }) + expect(readToml(join(h.runDir, 'FINISHED'))).toEqual({ status: 'aborted', exit_code: 143 }) + }) + + it('SIGTERM-ignoring script is SIGKILLed after grace → FINISHED{aborted, 137}', async () => { + const root = tmpRoot() + const h = await new LocalExecutor().submit(fakeJulia(root, 's.jl', ''), { + runsRoot: join(root, 'runs'), + julia: { julia: fakeJulia(root, 'j', HANG_IGNORE) }, + graceMs: 200, // test knob — spec default is 5000 + }) + for await (const e of h.events) { + if (e.kind === 'log' && e.line === 'READY') void h.abort() // handler installed — now abort + } + expect(await h.finished).toEqual({ status: 'aborted', exitCode: 137 }) + }, 15000) + + it('abort() is idempotent and a no-op after completion', async () => { + const root = tmpRoot() + const h = await new LocalExecutor().submit(fakeJulia(root, 's.jl', ''), { + runsRoot: join(root, 'runs'), julia: { julia: fakeJulia(root, 'j', 'process.exit(0)') }, + }) + await h.finished + await expect(h.abort()).resolves.toBeUndefined() + expect(readToml(join(h.runDir, 'FINISHED')).status).toBe('completed') + }) +}) diff --git a/packages/amico-run/test/cli.test.ts b/packages/amico-run/test/cli.test.ts new file mode 100644 index 00000000..892cc966 --- /dev/null +++ b/packages/amico-run/test/cli.test.ts @@ -0,0 +1,66 @@ +import { describe, it, expect, beforeAll } from 'vitest' +import { execFileSync, execFile } from 'node:child_process' +import { join } from 'node:path' +import { tmpRoot, fakeJulia } from './helpers.js' + +const BUNDLE = join(__dirname, '..', 'dist', 'amico-run.js') +beforeAll(() => { + execFileSync('node', [join(__dirname, '..', 'esbuild.config.mjs')], { cwd: join(__dirname, '..') }) +}) + +function run(args: string[]): { code: number; stdout: string; stderr: string } { + try { + const stdout = execFileSync('node', [BUNDLE, ...args], { encoding: 'utf8' }) + return { code: 0, stdout, stderr: '' } + } catch (e) { + const err = e as { status?: number; stdout?: string; stderr?: string } + return { code: err.status ?? -1, stdout: err.stdout ?? '', stderr: err.stderr ?? '' } + } +} + +describe('amico-run CLI', () => { + it('clean solve: relays iter lines, prints AMICODE_FINISHED, exits 0', () => { + const root = tmpRoot() + const julia = fakeJulia(root, 'j', `console.log('AMICODE_ITER iter=1 f=0.5'); console.log('DONE f=0.99')`) + const script = fakeJulia(root, 's.jl', '') + const r = run([script, '--runs-root', join(root, 'runs'), '--julia', julia]) + expect(r.code).toBe(0) + expect(r.stdout).toContain('AMICODE_ITER iter=1 f=0.5') + expect(r.stdout).toMatch(/AMICODE_FINISHED status=completed exitCode=0 runDir=.+/) + }) + it('julia rc 7 passes through as exit 7', () => { + const root = tmpRoot() + const r = run([fakeJulia(root, 's.jl', ''), '--runs-root', join(root, 'runs'), + '--julia', fakeJulia(root, 'j', 'process.exit(7)')]) + expect(r.code).toBe(7) + expect(r.stdout).toContain('status=failed exitCode=7') + }) + it('missing script → 64, stderr one-liner, no run dir', () => { + const root = tmpRoot() + const r = run([join(root, 'nope.jl'), '--runs-root', join(root, 'runs')]) + expect(r.code).toBe(64) + expect(r.stderr).toMatch(/script not found/) + }) + it('unknown flag → 64 (never silently swallowed, spec Q68)', () => { + const root = tmpRoot() + const r = run([fakeJulia(root, 's.jl', ''), '--gates', 'X']) + expect(r.code).toBe(64) + expect(r.stderr).toMatch(/unknown flag/) + }) + it('--executor remote → 64 (only local in β)', () => { + const root = tmpRoot() + const r = run([fakeJulia(root, 's.jl', ''), '--executor', 'remote']) + expect(r.code).toBe(64) + }) + it('SIGTERM to the CLI → abort lane, exit 130', async () => { + const root = tmpRoot() + const julia = fakeJulia(root, 'j', `console.log('READY'); setInterval(() => {}, 1000)`) + const script = fakeJulia(root, 's.jl', '') + const code: number = await new Promise(resolveP => { + const child = execFile('node', [BUNDLE, script, '--runs-root', join(root, 'runs'), '--julia', julia]) + child.stdout!.on('data', (d: string) => { if (d.includes('READY')) child.kill('SIGTERM') }) + child.on('exit', c => resolveP(c ?? -1)) + }) + expect(code).toBe(130) + }, 15000) +}) diff --git a/packages/amico-run/test/failure_lanes.test.ts b/packages/amico-run/test/failure_lanes.test.ts new file mode 100644 index 00000000..03112dc6 --- /dev/null +++ b/packages/amico-run/test/failure_lanes.test.ts @@ -0,0 +1,128 @@ +import { describe, it, expect } from 'vitest' +import { chmodSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' +import { tmpRoot, fakeJulia, readToml } from './helpers.js' +import { LocalExecutor } from '../src/local_executor.js' + +const sub = (root: string, julia: string, script: string) => + new LocalExecutor().submit(script, { runsRoot: join(root, 'runs'), julia: { julia } }) + +describe('§6 failure matrix', () => { + it('nonzero exit → FINISHED{failed, rc}', async () => { + const root = tmpRoot() + const h = await sub(root, fakeJulia(root, 'j', 'process.exit(3)'), fakeJulia(root, 's.jl', '')) + expect(await h.finished).toEqual({ status: 'failed', exitCode: 3 }) + expect(readToml(join(h.runDir, 'FINISHED'))).toEqual({ status: 'failed', exit_code: 3 }) + }) + + it('crash before any output → FINISHED{failed}, manifest still valid', async () => { + const root = tmpRoot() + const h = await sub(root, fakeJulia(root, 'j', 'throw new Error("boom")'), fakeJulia(root, 's.jl', '')) + const f = await h.finished + expect(f.status).toBe('failed') + expect(readToml(join(h.runDir, 'manifest.toml')).run_id).toBe(h.runId) + }) + + it('spawn failure after manifest (X_OK dir → spawn error) → FINISHED{failed, 127}', async () => { + const root = tmpRoot() + // a directory passes step-1 X_OK validation, but spawn() itself errors → child.on('error') + const dirAsJulia = join(root, 'julia-dir') + mkdirSync(dirAsJulia, { mode: 0o755 }) + const h = await sub(root, dirAsJulia, fakeJulia(root, 's.jl', '')) + expect(await h.finished).toEqual({ status: 'failed', exitCode: 127 }) + expect(readToml(join(h.runDir, 'manifest.toml')).run_id).toBe(h.runId) // manifest survived + }) + + it('shell exec-failure rc passes through verbatim (wrapper execs missing target)', async () => { + const root = tmpRoot() + const wrapper = join(root, 'julia-wrapper') + writeFileSync(wrapper, '#!/usr/bin/env bash\nexec /nonexistent/amico-test-julia "$@"\n') + chmodSync(wrapper, 0o755) + const h = await sub(root, wrapper, fakeJulia(root, 's.jl', '')) + const f = await h.finished + expect(f.status).toBe('failed') + expect([126, 127]).toContain(f.exitCode) // bash version dependent; both are julia-rc passthrough + }) + + it('crash mid-stream: iter events delivered, then FINISHED{failed}', async () => { + const root = tmpRoot() + const julia = fakeJulia(root, 'j', ` + console.log('AMICODE_ITER iter=1 f=0.5') + console.log('AMICODE_ITER iter=2 f=0.1') + process.exit(3)`) + const h = await sub(root, julia, fakeJulia(root, 's.jl', '')) + const evs: string[] = [] + for await (const e of h.events) evs.push(e.kind) + expect(evs.filter(k => k === 'iter')).toHaveLength(2) + expect(evs.at(-1)).toBe('finished') + expect((await h.finished).exitCode).toBe(3) + }) + + it('julia killed by an EXTERNAL signal (not abort) → FINISHED{failed, 128+sig}', async () => { + const root = tmpRoot() + // self-inflicted SIGTERM stands in for an external kill: abort() was never called, + // so status must be failed (143), not aborted + const julia = fakeJulia(root, 'j', `process.kill(process.pid, 'SIGTERM')`) + const h = await sub(root, julia, fakeJulia(root, 's.jl', '')) + expect(await h.finished).toEqual({ status: 'failed', exitCode: 143 }) + }) + + it('manifest is on disk BEFORE julia spawns (script observes it in cwd at startup)', async () => { + const root = tmpRoot() + const julia = fakeJulia(root, 'j', + `process.exit(require('node:fs').existsSync('manifest.toml') ? 0 : 7)`) + const h = await sub(root, julia, fakeJulia(root, 's.jl', '')) + expect(await h.finished).toEqual({ status: 'completed', exitCode: 0 }) + }) + + it('garbage / binary stdout never crashes the parser; classified as log', async () => { + const root = tmpRoot() + const h = await sub(root, + fakeJulia(root, 'j', `process.stdout.write(Buffer.from([0xff, 0xfe, 0x0a])); console.log('ok')`), + fakeJulia(root, 's.jl', '')) + expect((await h.finished).status).toBe('completed') + }) + + it('script that writes nothing at all still yields manifest + FINISHED', async () => { + const root = tmpRoot() + const h = await sub(root, fakeJulia(root, 'j', ''), fakeJulia(root, 's.jl', '')) + await h.finished + expect(readToml(join(h.runDir, 'FINISHED')).status).toBe('completed') + }) + + it("script's own bogus FINISHED is overwritten by the orchestrator verdict", async () => { + const root = tmpRoot() + const julia = fakeJulia(root, 'j', ` + require('node:fs').writeFileSync('FINISHED', 'status = "completed"\\nexit_code = 0\\n') + process.exit(9)`) + const h = await sub(root, julia, fakeJulia(root, 's.jl', '')) + await h.finished + expect(readToml(join(h.runDir, 'FINISHED'))).toEqual({ status: 'failed', exit_code: 9 }) + }) + + it('exactly one finished event; events iterator terminates', async () => { + const root = tmpRoot() + const h = await sub(root, fakeJulia(root, 'j', 'process.exit(0)'), fakeJulia(root, 's.jl', '')) + let n = 0 + for await (const e of h.events) if (e.kind === 'finished') n++ + expect(n).toBe(1) + }) + + it('no partial orchestrator file is ever observable (tight-loop reader, spec §8)', async () => { + const root = tmpRoot() + const julia = fakeJulia(root, 'j', ` + let i = 0 + const t = setInterval(() => { console.log('AMICODE_ITER iter=' + ++i + ' f=0.1') + if (i >= 20) { clearInterval(t) } }, 10)`) + const h = await sub(root, julia, fakeJulia(root, 's.jl', '')) + let sawTmp = false + let done = false + void h.finished.then(() => { done = true }) + while (!done) { + if (readdirSync(h.runDir).some(f => f.includes('.tmp-'))) sawTmp = true + await new Promise(r => setTimeout(r, 2)) + } + expect(sawTmp).toBe(false) + expect(readToml(join(h.runDir, 'FINISHED')).status).toBe('completed') + }) +}) diff --git a/packages/amico-run/test/helpers.ts b/packages/amico-run/test/helpers.ts new file mode 100644 index 00000000..ff491074 --- /dev/null +++ b/packages/amico-run/test/helpers.ts @@ -0,0 +1,21 @@ +import { mkdtempSync, readFileSync, writeFileSync, chmodSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { parse } from 'smol-toml' + +export function tmpRoot(): string { + return mkdtempSync(join(tmpdir(), 'amico-run-test-')) +} + +export function readToml(path: string): Record { + return parse(readFileSync(path, 'utf8')) as Record +} + +/** Create an executable fake-julia "binary" (node script via shebang). It receives + * the julia argv (flags + script path) and ignores it unless the body uses it. */ +export function fakeJulia(dir: string, name: string, body: string): string { + const p = join(dir, name) + writeFileSync(p, `#!/usr/bin/env node\n${body}\n`) + chmodSync(p, 0o755) + return p +} diff --git a/packages/amico-run/test/local_executor.test.ts b/packages/amico-run/test/local_executor.test.ts new file mode 100644 index 00000000..f850cec2 --- /dev/null +++ b/packages/amico-run/test/local_executor.test.ts @@ -0,0 +1,75 @@ +import { describe, it, expect } from 'vitest' +import { existsSync, readFileSync, readdirSync } from 'node:fs' +import { join } from 'node:path' +import { tmpRoot, fakeJulia, readToml } from './helpers.js' +import { LocalExecutor } from '../src/local_executor.js' +import { validateManifest, validateFinished } from '../src/schemas.js' +import type { RunEvent } from '../src/types.js' + +const CLEAN = ` +console.log('AMICODE_ITER iter=1 f=1.0e-2') +console.log('AMICODE_ITER iter=2 f=3.0e-4') +console.log('DONE fidelity=0.9999') +` + +async function collect(events: AsyncIterable): Promise { + const out: RunEvent[] = [] + for await (const e of events) out.push(e) + return out +} + +describe('LocalExecutor happy path', () => { + it('produces a conforming run dir and ordered event stream', async () => { + const root = tmpRoot() + const julia = fakeJulia(root, 'julia-clean', CLEAN) + const script = fakeJulia(root, 'solve.jl', '') // content irrelevant; must exist + const h = await new LocalExecutor().submit(script, { + lab: 'testlab', runsRoot: join(root, 'runs'), julia: { julia }, + }) + + // manifest observable before events finish — submit() resolved, so it must exist NOW + const manifest = readToml(join(h.runDir, 'manifest.toml')) + expect(validateManifest(manifest).ok).toBe(true) + expect(manifest.lab_id).toBe('testlab') + + const evs = await collect(h.events) + expect(evs.filter(e => e.kind === 'iter')).toHaveLength(2) + expect(evs.filter(e => e.kind === 'done')).toHaveLength(1) + const fin = evs.at(-1)! + expect(fin).toEqual({ kind: 'finished', status: 'completed', exitCode: 0 }) + expect(await h.finished).toEqual({ status: 'completed', exitCode: 0 }) + + const finished = readToml(join(h.runDir, 'FINISHED')) + expect(validateFinished(finished).ok).toBe(true) + expect(finished.status).toBe('completed') + + // run.log mirrors stdout verbatim; index has exactly one line; latest points at the run + expect(readFileSync(join(h.runDir, 'run.log'), 'utf8')).toContain('AMICODE_ITER iter=2') + expect(readFileSync(join(root, 'runs', 'index'), 'utf8').trim().split('\n')).toHaveLength(1) + // no temp files left anywhere in the run dir + expect(readdirSync(h.runDir).filter(f => f.includes('.tmp-'))).toHaveLength(0) + }) + + it('config errors reject BEFORE any run dir exists (exit-64 class)', async () => { + const root = tmpRoot() + await expect(new LocalExecutor().submit(join(root, 'nope.jl'), { runsRoot: join(root, 'runs') })) + .rejects.toThrow(/script not found/) + expect(existsSync(join(root, 'runs'))).toBe(false) + }) + + it('passes --project/--sysimage through and runs with cwd = runDir', async () => { + const root = tmpRoot() + const julia = fakeJulia(root, 'julia-echo', + `console.log('ARGS ' + process.argv.slice(2).join(' ')); console.log('CWD ' + process.cwd())`) + const script = fakeJulia(root, 's.jl', '') + const h = await new LocalExecutor().submit(script, { + runsRoot: join(root, 'runs'), julia: { julia, project: '/proj', sysimage: '/img.so' }, + }) + const evs = await collect(h.events) + const argLine = evs.find(e => e.kind === 'log' && e.line.startsWith('ARGS')) as Extract + expect(argLine.line).toContain('--project=/proj') + expect(argLine.line).toContain('--sysimage=/img.so') + const cwdLine = evs.find(e => e.kind === 'log' && e.line.startsWith('CWD')) as Extract + expect(cwdLine.line).toContain(h.runDir) + }) +}) diff --git a/packages/amico-run/test/run_dir.test.ts b/packages/amico-run/test/run_dir.test.ts new file mode 100644 index 00000000..2e6a4a6d --- /dev/null +++ b/packages/amico-run/test/run_dir.test.ts @@ -0,0 +1,71 @@ +import { describe, it, expect } from 'vitest' +import { existsSync, readFileSync, readlinkSync, mkdirSync } from 'node:fs' +import { join } from 'node:path' +import { tmpRoot, readToml } from './helpers.js' +import { + deriveLabId, generateRunId, atomicWriteFile, + writeManifest, writeFinished, appendIndex, updateLatest, +} from '../src/run_dir.js' +import { ConfigError } from '../src/types.js' + +describe('deriveLabId', () => { + it('uses id pointers verbatim', () => expect(deriveLabId('schuster')).toBe('schuster')) + it('derives from parent dir of a lab.toml path', () => + expect(deriveLabId('/labs/schuster/lab.toml')).toBe('schuster')) + it('rejects pointers that fit neither rule', () => + expect(() => deriveLabId('Bad Lab!')).toThrow(ConfigError)) +}) + +describe('generateRunId', () => { + it('matches r-<4hex> and avoids collisions', () => { + const root = tmpRoot() + const id = generateRunId(root, new Date('2026-06-10T10:12:45.678Z')) + expect(id).toMatch(/^r20260610-101245Z-[0-9a-f]{4}$/) + mkdirSync(join(root, id)) + const id2 = generateRunId(root, new Date('2026-06-10T10:12:45.678Z')) + expect(id2).not.toBe(id) + }) +}) + +describe('writers', () => { + it('manifest round-trips through a TOML parser with exact snake_case keys', () => { + const root = tmpRoot() + writeManifest(root, { + schema_version: '1', run_id: 'r1', script_path: '/s.jl', + lab: '/labs/x/lab.toml', lab_id: 'x', + created_at: '2026-06-10T10:12:45Z', orchestrator_version: '0.1.0', + julia: { binary: 'julia', project: '/proj' }, + }) + const m = readToml(join(root, 'manifest.toml')) + expect(m.schema_version).toBe('1') + expect(m.lab_id).toBe('x') + expect((m.julia as Record).project).toBe('/proj') + expect(m).not.toHaveProperty('sizeClass') // spec §5: intentionally absent + }) + it('FINISHED carries status + exit_code (snake_case)', () => { + const root = tmpRoot() + writeFinished(root, 'failed', 7) + expect(readToml(join(root, 'FINISHED'))).toEqual({ status: 'failed', exit_code: 7 }) + }) + it('atomicWriteFile leaves no temp file behind', () => { + const root = tmpRoot() + atomicWriteFile(root, 'f.toml', 'a = 1\n') + expect(readFileSync(join(root, 'f.toml'), 'utf8')).toBe('a = 1\n') + expect(existsSync(join(root, `.f.toml.tmp-${process.pid}`))).toBe(false) + }) + it('index appends one tab-separated line per run; latest symlink swings', () => { + const root = tmpRoot() + appendIndex(root, 'r1', 't1', '/a.jl'); appendIndex(root, 'r2', 't2', '/b.jl') + expect(readFileSync(join(root, 'index'), 'utf8')).toBe('r1\tt1\t/a.jl\nr2\tt2\t/b.jl\n') + mkdirSync(join(root, 'r2')) + updateLatest(root, 'r2') + expect(readlinkSync(join(root, 'latest'))).toBe('r2') + }) + it('sanitizes tab/newline in the script path so the TSV index stays one line per run', () => { + const root = tmpRoot() + appendIndex(root, 'r1', 't1', '/weird\tpath\nwith/ctrl.jl') + const lines = readFileSync(join(root, 'index'), 'utf8').trimEnd().split('\n') + expect(lines).toHaveLength(1) // not corrupted into multiple rows + expect(lines[0].split('\t')).toHaveLength(3) // exactly runId/createdAt/path fields + }) +}) diff --git a/packages/amico-run/test/s31.test.ts b/packages/amico-run/test/s31.test.ts new file mode 100644 index 00000000..0567ff55 --- /dev/null +++ b/packages/amico-run/test/s31.test.ts @@ -0,0 +1,19 @@ +import { describe, it, expect } from 'vitest' +import { readFileSync, readdirSync } from 'node:fs' +import { join } from 'node:path' + +// S31 / spec §4: no physics flag parsing, no SolveSpec, no MCP, no HTTP in the orchestrator. +const FORBIDDEN = [/SolveSpec/, /--gate\b/, /--system\b/, /--pulse\b/, + /modelcontextprotocol/i, /node:https?\b/, /\bfetch\s*\(/] + +describe('S31 grep rule', () => { + it('src/ contains no forbidden tool-layer patterns', () => { + const srcDir = join(__dirname, '..', 'src') + for (const f of readdirSync(srcDir)) { + const text = readFileSync(join(srcDir, f), 'utf8') + for (const re of FORBIDDEN) { + expect(text, `${f} matches forbidden ${re}`).not.toMatch(re) + } + } + }) +}) diff --git a/packages/amico-run/test/schemas.test.ts b/packages/amico-run/test/schemas.test.ts new file mode 100644 index 00000000..14ac2434 --- /dev/null +++ b/packages/amico-run/test/schemas.test.ts @@ -0,0 +1,37 @@ +import { describe, it, expect } from 'vitest' +import { validateManifest, validateFinished, validateResult } from '../src/schemas.js' + +const goodManifest = { + schema_version: '1', run_id: 'r20260610-101245Z-ab12', script_path: '/s.jl', + lab: 'default', lab_id: 'default', created_at: '2026-06-10T10:12:45Z', + orchestrator_version: '0.1.0', julia: { binary: 'julia' }, +} + +describe('validateManifest', () => { + it('accepts a conforming manifest', () => + expect(validateManifest(goodManifest)).toEqual({ ok: true, errors: [] })) + it('reports each missing/mistyped field by path', () => { + const r = validateManifest({ ...goodManifest, run_id: 42, julia: {} }) + expect(r.ok).toBe(false) + expect(r.errors.join(' ')).toContain('run_id') + expect(r.errors.join(' ')).toContain('julia.binary') + }) + it('rejects unknown schema_version', () => + expect(validateManifest({ ...goodManifest, schema_version: '2' }).ok).toBe(false)) +}) + +describe('validateFinished', () => { + it('accepts {status, exit_code}', () => + expect(validateFinished({ status: 'aborted', exit_code: 143 }).ok).toBe(true)) + it('rejects bad status and non-integer exit_code', () => { + expect(validateFinished({ status: 'ok', exit_code: 0 }).ok).toBe(false) + expect(validateFinished({ status: 'failed', exit_code: 1.5 }).ok).toBe(false) + }) +}) + +describe('validateResult (reader-side)', () => { + it('requires fidelity number, iterations integer', () => { + expect(validateResult({ fidelity: 0.999, iterations: 200, wall_seconds: 12.5 }).ok).toBe(true) + expect(validateResult({ iterations: 200 }).ok).toBe(false) + }) +}) diff --git a/packages/amico-run/test/slow/integration.test.ts b/packages/amico-run/test/slow/integration.test.ts new file mode 100644 index 00000000..60051da5 --- /dev/null +++ b/packages/amico-run/test/slow/integration.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect } from 'vitest' +import { execFileSync } from 'node:child_process' +import { join } from 'node:path' +import { tmpRoot, readToml } from '../helpers.js' +import { validateManifest, validateFinished, validateResult } from '../../src/schemas.js' + +// Slow tier (spec §8): real Piccolo solves through the real CLI. Dev machine only — not CI. +// Requires: julia on PATH + a Piccolo project (pass via AMICO_TEST_JULIA_PROJECT to *the test*, +// which forwards it as an explicit --project flag — the orchestrator itself stays env-free). +const PROJECT = process.env.AMICO_TEST_JULIA_PROJECT +const BUNDLE = join(__dirname, '..', '..', 'dist', 'amico-run.js') + +function solveAndValidate(script: string): void { + const root = tmpRoot() + const stdout = execFileSync('node', [ + BUNDLE, join(__dirname, script), + '--runs-root', join(root, 'runs'), '--project', PROJECT!, '--lab', 'devlab', + ], { encoding: 'utf8', timeout: 600_000 }) + + expect(stdout).toMatch(/AMICODE_ITER iter=/) + expect(stdout).toMatch(/AMICODE_FINISHED status=completed exitCode=0 runDir=(.+)/) + const runDir = stdout.match(/runDir=(.+)/)![1].trim() + expect(validateManifest(readToml(join(runDir, 'manifest.toml'))).ok).toBe(true) + expect(validateFinished(readToml(join(runDir, 'FINISHED'))).ok).toBe(true) + const result = readToml(join(runDir, 'result.toml')) + expect(validateResult(result).ok).toBe(true) + expect(result.fidelity as number).toBeGreaterThan(0.99) +} + +describe.skipIf(!PROJECT)('slow: real Piccolo solves through amico-run', () => { + it('x-gate solve produces a fully conforming run dir', () => { + solveAndValidate('solve_x_gate.jl') + }, 600_000) + + it('h-gate solve produces a fully conforming run dir', () => { + solveAndValidate('solve_h_gate.jl') + }, 600_000) +}) diff --git a/packages/amico-run/test/slow/solve_common.jl b/packages/amico-run/test/slow/solve_common.jl new file mode 100644 index 00000000..f4e865e3 --- /dev/null +++ b/packages/amico-run/test/slow/solve_common.jl @@ -0,0 +1,61 @@ +# Single-qubit gate solve (gate chosen by the including script via GATES[GATE_TARGET_NAME]) exercising the run-dir contract end-to-end. +# Runs with cwd = run dir (amico-run sets that); writes result.toml + pulse.jld2 here. +# Follows the conventions the β.3 AGENTS.md template teaches the LLM: +# - AMICODE_ITER key=value lines on stdout, flushed per iteration +# - result.toml written atomically (temp + rename) +# - final DONE line +# Vetted against Piccolo 1.19 (the version `Pkg.add Piccolo` installs today). +using Piccolo +using JLD2 +using TOML + +# Per-iter callbacks via Piccolo's PUBLIC `Callbacks` module (Piccolo 1.19) — +# no `methods(solve!)` reach-through into solver internals. +const Callbacks = Piccolo.Callbacks + +# Toy single-qubit system (matches the Piccolo docs intro + the frozen spike). +sys = QuantumSystem(0.5 * PAULIS[:Z], [PAULIS[:X], PAULIS[:Y]], [1.0, 1.0]) + +N = 51 +T = 10.0 +times = collect(range(0.0, T, length = N)) +initial = 0.1 * randn(sys.n_drives, N) + +pulse = ZeroOrderPulse(initial, times) +qtraj = UnitaryTrajectory(sys, pulse, GATES[GATE_TARGET_NAME]) +qcp = SmoothPulseProblem(qtraj, N; + piccolo_options = PiccoloOptions(timesteps_all_equal = true), + ddu_bound = 1.0, Q = 100.0, R = 1e-2) + +dto_prob = hasproperty(qcp, :prob) ? qcp.prob : qcp +iters = Ref(0) +cb_update = Callbacks.callback_update_trajectory_factory(dto_prob) +function cb_log(optimizer, optimizer_state; kwargs...) + iters[] = Int(optimizer_state.iter_count) + println("AMICODE_ITER iter=$(optimizer_state.iter_count) f=$(optimizer_state.obj_value) " * + "inf_pr=$(optimizer_state.inf_pr) inf_du=$(optimizer_state.inf_du)") + flush(stdout) + return true +end + +t0 = time() +solve!(qcp; + max_iter = 150, print_level = 1, + callback = Callbacks.callback_factory(cb_update, cb_log)) +wall = time() - t0 + +fid = fidelity(qcp) + +JLD2.save("pulse.jld2", "trajectory", dto_prob.trajectory) + +open("result.toml.tmp", "w") do io + TOML.print(io, Dict( + "fidelity" => fid, + "iterations" => iters[], + "wall_seconds" => wall, + )) +end +mv("result.toml.tmp", "result.toml"; force = true) + +println("DONE fidelity=$(fid)") +flush(stdout) diff --git a/packages/amico-run/test/slow/solve_h_gate.jl b/packages/amico-run/test/slow/solve_h_gate.jl new file mode 100644 index 00000000..624f20d0 --- /dev/null +++ b/packages/amico-run/test/slow/solve_h_gate.jl @@ -0,0 +1,3 @@ +# Slow-tier solve #2: H gate on the toy qubit. See solve_common.jl. +const GATE_TARGET_NAME = :H +include(joinpath(@__DIR__, "solve_common.jl")) diff --git a/packages/amico-run/test/slow/solve_x_gate.jl b/packages/amico-run/test/slow/solve_x_gate.jl new file mode 100644 index 00000000..fc448cc5 --- /dev/null +++ b/packages/amico-run/test/slow/solve_x_gate.jl @@ -0,0 +1,3 @@ +# Slow-tier solve #1: X gate on the toy qubit. See solve_common.jl. +const GATE_TARGET_NAME = :X +include(joinpath(@__DIR__, "solve_common.jl")) diff --git a/packages/amico-run/test/telemetry.test.ts b/packages/amico-run/test/telemetry.test.ts new file mode 100644 index 00000000..126489ff --- /dev/null +++ b/packages/amico-run/test/telemetry.test.ts @@ -0,0 +1,28 @@ +import { describe, it, expect } from 'vitest' +import { classifyLine } from '../src/telemetry.js' + +describe('classifyLine', () => { + it('parses AMICODE_ITER key=value fields', () => { + const ev = classifyLine('AMICODE_ITER iter=12 f=3.4e-5 inf_pr=1.2e-8', 'stdout') + expect(ev).toEqual({ + kind: 'iter', + raw: 'AMICODE_ITER iter=12 f=3.4e-5 inf_pr=1.2e-8', + fields: { iter: '12', f: '3.4e-5', inf_pr: '1.2e-8' }, + }) + }) + it('classifies DONE as done', () => { + expect(classifyLine('DONE fidelity=0.9999', 'stdout').kind).toBe('done') + }) + it('AMICODE_ITER on stderr is just log (convention is stdout-only)', () => { + expect(classifyLine('AMICODE_ITER iter=1', 'stderr').kind).toBe('log') + }) + it('malformed tokens are skipped, never throw', () => { + const ev = classifyLine('AMICODE_ITER iter=1 ====garbage', 'stdout') + expect(ev.kind).toBe('iter') + }) + it('everything else is log with stream tagged', () => { + expect(classifyLine('Ipopt banner', 'stderr')).toEqual({ + kind: 'log', stream: 'stderr', line: 'Ipopt banner', + }) + }) +}) diff --git a/packages/amico-run/tsconfig.json b/packages/amico-run/tsconfig.json new file mode 100644 index 00000000..b9ee75a8 --- /dev/null +++ b/packages/amico-run/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "resolveJsonModule": true, + "types": ["node"] + }, + "include": ["src", "test"] +} diff --git a/esbuild.config.mjs b/packages/extension/esbuild.config.mjs similarity index 100% rename from esbuild.config.mjs rename to packages/extension/esbuild.config.mjs diff --git a/media/amico.svg b/packages/extension/media/amico.svg similarity index 100% rename from media/amico.svg rename to packages/extension/media/amico.svg diff --git a/packages/extension/package.json b/packages/extension/package.json new file mode 100644 index 00000000..516206b0 --- /dev/null +++ b/packages/extension/package.json @@ -0,0 +1,87 @@ +{ + "name": "amicode-v2", + "displayName": "Amicode v2", + "description": "Amico research IDE — opencode-backed chat + native VS Code surfaces for vault, catalog, and live solve inspection.", + "version": "0.0.1", + "publisher": "harmoniqs", + "license": "Apache-2.0", + "private": true, + "engines": { + "vscode": "^1.95.0" + }, + "categories": ["Other"], + "activationEvents": [ + "onCommand:amicode.openChat", + "onCommand:amicode.openInspector", + "onView:amicode.runInspector", + "onView:amicode.vault", + "onStartupFinished" + ], + "main": "./dist/extension.js", + "contributes": { + "viewsContainers": { + "activitybar": [ + { + "id": "amicode", + "title": "Amicode", + "icon": "media/amico.svg" + } + ], + "panel": [ + { + "id": "amicode-panel", + "title": "Amicode", + "icon": "media/amico.svg" + } + ] + }, + "views": { + "amicode": [ + { "id": "amicode.vault", "name": "Vault", "type": "tree" }, + { "id": "amicode.catalog", "name": "Catalog", "type": "tree" }, + { "id": "amicode.armonia", "name": "Armonia", "type": "tree" } + ], + "amicode-panel": [ + { "id": "amicode.runInspector", "name": "Run Inspector", "type": "webview" } + ] + }, + "commands": [ + { "command": "amicode.openChat", "title": "Amicode: Open Chat" }, + { "command": "amicode.openInspector", "title": "Amicode: Open Run Inspector" }, + { "command": "amicode.restartServer", "title": "Amicode: Restart opencode server" } + ], + "configuration": { + "title": "Amicode", + "properties": { + "amicode.opencodeBinary": { + "type": "string", + "default": "opencode", + "description": "Path or command name for the opencode CLI." + }, + "amicode.juliaProject": { + "type": "string", + "default": "/tmp/amicode-spike-julia", + "description": "Julia project root (--project=...) the run_julia tool will use." + }, + "amicode.juliaScript": { + "type": "string", + "default": "", + "description": "Absolute path to spike_solve.jl. If empty, looks up via the extensionPath/../amicode/julia/spike_solve.jl convention." + } + } + } + }, + "scripts": { + "build": "node esbuild.config.mjs", + "watch": "node esbuild.config.mjs --watch", + "typecheck": "tsc --noEmit", + "test": "echo 'extension: spike smoke tests in test/ need opencode+julia (not headless); skipped'" + }, + "devDependencies": { + "@amicode/amico-run": "workspace:*", + "@types/node": "^22.0.0", + "@types/vscode": "^1.95.0", + "esbuild": "^0.24.0", + "typescript": "^5.6.0" + } +} diff --git a/src/chat_panel.ts b/packages/extension/src/chat_panel.ts similarity index 100% rename from src/chat_panel.ts rename to packages/extension/src/chat_panel.ts diff --git a/packages/extension/src/executor_check.ts b/packages/extension/src/executor_check.ts new file mode 100644 index 00000000..ebf5abea --- /dev/null +++ b/packages/extension/src/executor_check.ts @@ -0,0 +1,9 @@ +// Type-level contract check (spec §9 AC): the extension consumes the β.1 library API. +// β.5 replaces this with the real RunsManager integration. +import type { Executor, RunHandle, RunEvent } from '@amicode/amico-run' + +export type _ExecutorContract = { + submit: Executor['submit'] + handle: Pick + event: RunEvent['kind'] +} diff --git a/src/extension.ts b/packages/extension/src/extension.ts similarity index 100% rename from src/extension.ts rename to packages/extension/src/extension.ts diff --git a/src/file_watcher.ts b/packages/extension/src/file_watcher.ts similarity index 100% rename from src/file_watcher.ts rename to packages/extension/src/file_watcher.ts diff --git a/src/inspector_webview.ts b/packages/extension/src/inspector_webview.ts similarity index 100% rename from src/inspector_webview.ts rename to packages/extension/src/inspector_webview.ts diff --git a/src/opencode_config.ts b/packages/extension/src/opencode_config.ts similarity index 100% rename from src/opencode_config.ts rename to packages/extension/src/opencode_config.ts diff --git a/src/run_inspector.ts b/packages/extension/src/run_inspector.ts similarity index 100% rename from src/run_inspector.ts rename to packages/extension/src/run_inspector.ts diff --git a/src/server_manager.ts b/packages/extension/src/server_manager.ts similarity index 100% rename from src/server_manager.ts rename to packages/extension/src/server_manager.ts diff --git a/src/sse_client.ts b/packages/extension/src/sse_client.ts similarity index 100% rename from src/sse_client.ts rename to packages/extension/src/sse_client.ts diff --git a/src/status_bar.ts b/packages/extension/src/status_bar.ts similarity index 100% rename from src/status_bar.ts rename to packages/extension/src/status_bar.ts diff --git a/src/trees.ts b/packages/extension/src/trees.ts similarity index 100% rename from src/trees.ts rename to packages/extension/src/trees.ts diff --git a/src/types.ts b/packages/extension/src/types.ts similarity index 100% rename from src/types.ts rename to packages/extension/src/types.ts diff --git a/test/opencode_boot_harness.mjs b/packages/extension/test/opencode_boot_harness.mjs similarity index 100% rename from test/opencode_boot_harness.mjs rename to packages/extension/test/opencode_boot_harness.mjs diff --git a/test/smoke_cli.mjs b/packages/extension/test/smoke_cli.mjs similarity index 100% rename from test/smoke_cli.mjs rename to packages/extension/test/smoke_cli.mjs diff --git a/test/smoke_opencode_boot.mjs b/packages/extension/test/smoke_opencode_boot.mjs similarity index 100% rename from test/smoke_opencode_boot.mjs rename to packages/extension/test/smoke_opencode_boot.mjs diff --git a/test/vscode_shim.mjs b/packages/extension/test/vscode_shim.mjs similarity index 100% rename from test/vscode_shim.mjs rename to packages/extension/test/vscode_shim.mjs diff --git a/tsconfig.json b/packages/extension/tsconfig.json similarity index 100% rename from tsconfig.json rename to packages/extension/tsconfig.json diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e3468c8..aa0a6a85 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,8 +6,31 @@ settings: importers: - .: + .: {} + + packages/amico-run: + devDependencies: + '@types/node': + specifier: ^22.0.0 + version: 22.19.19 + esbuild: + specifier: ^0.24.0 + version: 0.24.2 + smol-toml: + specifier: ^1.3.0 + version: 1.6.1 + typescript: + specifier: ^5.6.0 + version: 5.9.3 + vitest: + specifier: ^2.1.0 + version: 2.1.9(@types/node@22.19.19) + + packages/extension: devDependencies: + '@amicode/amico-run': + specifier: workspace:* + version: link:../amico-run '@types/node': specifier: ^22.0.0 version: 22.19.19 @@ -23,102 +46,204 @@ importers: packages: + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.24.2': resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.24.2': resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.24.2': resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.24.2': resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.24.2': resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.24.2': resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.24.2': resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.24.2': resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.24.2': resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.24.2': resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.24.2': resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.24.2': resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.24.2': resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.24.2': resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.24.2': resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.24.2': resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.24.2': resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} engines: {node: '>=18'} @@ -131,6 +256,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.24.2': resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} engines: {node: '>=18'} @@ -143,47 +274,354 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.24.2': resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.24.2': resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.24.2': resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.24.2': resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.24.2': resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} engines: {node: '>=18'} cpu: [x64] os: [win32] + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@rollup/rollup-android-arm-eabi@4.61.1': + resolution: {integrity: sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.61.1': + resolution: {integrity: sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.61.1': + resolution: {integrity: sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.61.1': + resolution: {integrity: sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.61.1': + resolution: {integrity: sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.61.1': + resolution: {integrity: sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.61.1': + resolution: {integrity: sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.61.1': + resolution: {integrity: sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.61.1': + resolution: {integrity: sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.61.1': + resolution: {integrity: sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.61.1': + resolution: {integrity: sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.61.1': + resolution: {integrity: sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.61.1': + resolution: {integrity: sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.61.1': + resolution: {integrity: sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.61.1': + resolution: {integrity: sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.61.1': + resolution: {integrity: sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.61.1': + resolution: {integrity: sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.61.1': + resolution: {integrity: sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.61.1': + resolution: {integrity: sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.61.1': + resolution: {integrity: sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.61.1': + resolution: {integrity: sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.61.1': + resolution: {integrity: sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.61.1': + resolution: {integrity: sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.61.1': + resolution: {integrity: sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.61.1': + resolution: {integrity: sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==} + cpu: [x64] + os: [win32] + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/node@22.19.19': resolution: {integrity: sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==} '@types/vscode@1.120.0': resolution: {integrity: sha512-feaT4Rst+FkTch5zz/ZbNCxoIvo55YU80Be2kiL7OJcod4+CUYf2lUBPdIJzozNnSEMq1VRTGrWEcCGFB3fBmA==} + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + esbuild@0.24.2: resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} engines: {node: '>=18'} hasBin: true + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + rollup@4.61.1: + resolution: {integrity: sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + smol-toml@1.6.1: + resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} + engines: {node: '>= 18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -192,89 +630,391 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + snapshots: + '@esbuild/aix-ppc64@0.21.5': + optional: true + '@esbuild/aix-ppc64@0.24.2': optional: true + '@esbuild/android-arm64@0.21.5': + optional: true + '@esbuild/android-arm64@0.24.2': optional: true + '@esbuild/android-arm@0.21.5': + optional: true + '@esbuild/android-arm@0.24.2': optional: true + '@esbuild/android-x64@0.21.5': + optional: true + '@esbuild/android-x64@0.24.2': optional: true + '@esbuild/darwin-arm64@0.21.5': + optional: true + '@esbuild/darwin-arm64@0.24.2': optional: true + '@esbuild/darwin-x64@0.21.5': + optional: true + '@esbuild/darwin-x64@0.24.2': optional: true + '@esbuild/freebsd-arm64@0.21.5': + optional: true + '@esbuild/freebsd-arm64@0.24.2': optional: true + '@esbuild/freebsd-x64@0.21.5': + optional: true + '@esbuild/freebsd-x64@0.24.2': optional: true + '@esbuild/linux-arm64@0.21.5': + optional: true + '@esbuild/linux-arm64@0.24.2': optional: true + '@esbuild/linux-arm@0.21.5': + optional: true + '@esbuild/linux-arm@0.24.2': optional: true + '@esbuild/linux-ia32@0.21.5': + optional: true + '@esbuild/linux-ia32@0.24.2': optional: true + '@esbuild/linux-loong64@0.21.5': + optional: true + '@esbuild/linux-loong64@0.24.2': optional: true + '@esbuild/linux-mips64el@0.21.5': + optional: true + '@esbuild/linux-mips64el@0.24.2': optional: true + '@esbuild/linux-ppc64@0.21.5': + optional: true + '@esbuild/linux-ppc64@0.24.2': optional: true + '@esbuild/linux-riscv64@0.21.5': + optional: true + '@esbuild/linux-riscv64@0.24.2': optional: true + '@esbuild/linux-s390x@0.21.5': + optional: true + '@esbuild/linux-s390x@0.24.2': optional: true + '@esbuild/linux-x64@0.21.5': + optional: true + '@esbuild/linux-x64@0.24.2': optional: true '@esbuild/netbsd-arm64@0.24.2': optional: true + '@esbuild/netbsd-x64@0.21.5': + optional: true + '@esbuild/netbsd-x64@0.24.2': optional: true '@esbuild/openbsd-arm64@0.24.2': optional: true + '@esbuild/openbsd-x64@0.21.5': + optional: true + '@esbuild/openbsd-x64@0.24.2': optional: true + '@esbuild/sunos-x64@0.21.5': + optional: true + '@esbuild/sunos-x64@0.24.2': optional: true + '@esbuild/win32-arm64@0.21.5': + optional: true + '@esbuild/win32-arm64@0.24.2': optional: true + '@esbuild/win32-ia32@0.21.5': + optional: true + '@esbuild/win32-ia32@0.24.2': optional: true + '@esbuild/win32-x64@0.21.5': + optional: true + '@esbuild/win32-x64@0.24.2': optional: true + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@rollup/rollup-android-arm-eabi@4.61.1': + optional: true + + '@rollup/rollup-android-arm64@4.61.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.61.1': + optional: true + + '@rollup/rollup-darwin-x64@4.61.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.61.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.61.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.61.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.61.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.61.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.61.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.61.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.61.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.61.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.61.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.61.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.61.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.61.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.61.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.61.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.61.1': + optional: true + + '@types/estree@1.0.9': {} + '@types/node@22.19.19': dependencies: undici-types: 6.21.0 '@types/vscode@1.120.0': {} + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.19.19))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.21(@types/node@22.19.19) + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.21 + pathe: 1.1.2 + + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + + assertion-error@2.0.1: {} + + cac@6.7.14: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + es-module-lexer@1.7.0: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + esbuild@0.24.2: optionalDependencies: '@esbuild/aix-ppc64': 0.24.2 @@ -303,6 +1043,155 @@ snapshots: '@esbuild/win32-ia32': 0.24.2 '@esbuild/win32-x64': 0.24.2 + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.3.0: {} + + fsevents@2.3.3: + optional: true + + loupe@3.2.1: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + ms@2.1.3: {} + + nanoid@3.3.12: {} + + pathe@1.1.2: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + rollup@4.61.1: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.61.1 + '@rollup/rollup-android-arm64': 4.61.1 + '@rollup/rollup-darwin-arm64': 4.61.1 + '@rollup/rollup-darwin-x64': 4.61.1 + '@rollup/rollup-freebsd-arm64': 4.61.1 + '@rollup/rollup-freebsd-x64': 4.61.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.61.1 + '@rollup/rollup-linux-arm-musleabihf': 4.61.1 + '@rollup/rollup-linux-arm64-gnu': 4.61.1 + '@rollup/rollup-linux-arm64-musl': 4.61.1 + '@rollup/rollup-linux-loong64-gnu': 4.61.1 + '@rollup/rollup-linux-loong64-musl': 4.61.1 + '@rollup/rollup-linux-ppc64-gnu': 4.61.1 + '@rollup/rollup-linux-ppc64-musl': 4.61.1 + '@rollup/rollup-linux-riscv64-gnu': 4.61.1 + '@rollup/rollup-linux-riscv64-musl': 4.61.1 + '@rollup/rollup-linux-s390x-gnu': 4.61.1 + '@rollup/rollup-linux-x64-gnu': 4.61.1 + '@rollup/rollup-linux-x64-musl': 4.61.1 + '@rollup/rollup-openbsd-x64': 4.61.1 + '@rollup/rollup-openharmony-arm64': 4.61.1 + '@rollup/rollup-win32-arm64-msvc': 4.61.1 + '@rollup/rollup-win32-ia32-msvc': 4.61.1 + '@rollup/rollup-win32-x64-gnu': 4.61.1 + '@rollup/rollup-win32-x64-msvc': 4.61.1 + fsevents: 2.3.3 + + siginfo@2.0.0: {} + + smol-toml@1.6.1: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + typescript@5.9.3: {} undici-types@6.21.0: {} + + vite-node@2.1.9(@types/node@22.19.19): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.21(@types/node@22.19.19) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.21(@types/node@22.19.19): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.15 + rollup: 4.61.1 + optionalDependencies: + '@types/node': 22.19.19 + fsevents: 2.3.3 + + vitest@2.1.9(@types/node@22.19.19): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.19.19)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 1.1.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.21(@types/node@22.19.19) + vite-node: 2.1.9(@types/node@22.19.19) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.19 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 00000000..924b55f4 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - packages/*