From 7e80099b00d64eb9d210358a85fe33cfca034678 Mon Sep 17 00:00:00 2001 From: prode Date: Sat, 1 Aug 2026 16:35:38 -0300 Subject: [PATCH 1/3] feat(dist): the installer, the two channels, the plugin, and the socket MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan 9.14 and 10.1 to 10.6 — the last of the plan. **9.14 is two halves and both are here.** The bundle, because an unbundled CLI pays Node's module resolution on every invocation and a `PreToolUse` hook fires it on *every page write* — so the cost is not paid once when a person types a command, it is paid every time an agent touches a page. And the socket: when the desktop application already has the project open, a query is a round trip on a named pipe rather than a process start. The socket carries read and validate and **never** write. That is not scope, it is the safety of the idea: a write over the socket would be a second writer into a project the application has open, with no snapshot of its own, and the operation log would record it as something the application did rather than something an agent did. Both paths call the same functions, so "both paths produce the same answer" is true by construction rather than by two implementations agreeing — and absence is not failure, because a CLI that needed the application running would be the opposite of what `adr:0013` says. Red observed first: 7 assertion failures against signature-only stubs. The installer embeds ffmpeg and `recorder.exe` and puts `ow` on PATH, because `adr:0013` makes `ow` in a directory the way a project is opened — an application the user has to add to PATH by hand is one whose main entrance is undocumented. Two artifacts ship from one tag and are checked to agree *before* anything is published. `adr:0014` named the cost it accepted — a skew "fails looking like corrupted state rather than a bad install" — so the check is a tested module rather than a shell snippet, and it names every artifact that disagrees rather than the first. The winget and Scoop manifests are generated from the tag and the hash that was just published, and both quote it. A package manager that downloads without verifying is the fetch-and-execute this product refuses to ship, which is the same argument `fetch-ffmpeg.mjs` already makes about its own download. Generated rather than hand-maintained because a manifest carrying last release's hash fails as "the download is corrupt" rather than "somebody forgot to update a file". The plugin ships the hooks and the scaffolding command and deliberately not the skills — `adr:0015` gives the convention one home and it is the project, so a copy here would be a second one and two copies of a convention drift. Not a `.mcp.json` either: its contents name *other* projects on that person's machine. `check-plugin.mjs` enforces both, because `claude plugin validate` needs the CLI installed and a check that silently passes when its tool is missing is not a check. The README carries what the plan asked it to and does not soften any of it: that the installer is unsigned and what SmartScreen will therefore say, that telling the other people in a call they are being recorded is the user's responsibility and in many places the law, and that committing a wiki puts `raw/` and `wiki/` in front of everyone with repository access — including `.state/`, which is where a redaction survives the redaction. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01D3VYWWTZtEE2NxPiksKsAK --- .claude-plugin/marketplace.json | 19 + .github/workflows/ci.yml | 25 +- .github/workflows/release.yml | 49 +- .gitignore | 7 + README.md | 110 + apps/desktop/build-resources/installer.nsh | 38 + apps/desktop/package.json | 54 +- apps/desktop/src/main/index.ts | 7 + eslint.config.js | 4 + package.json | 1 + packages/access/package.json | 3 +- packages/access/src/socket.ts | 190 ++ packages/access/tests/socket.spec.ts | 134 ++ packages/cli/package.json | 28 +- packages/cli/src/main.ts | 15 + packages/cli/tests/release.spec.ts | 202 ++ packages/cli/tsconfig.json | 2 +- packages/cli/types/release-scripts.d.ts | 33 + plans/open-wiki.md | 14 +- plugins/open-wiki/.claude-plugin/plugin.json | 9 + plugins/open-wiki/README.md | 36 + plugins/open-wiki/commands/ow-init.md | 19 + plugins/open-wiki/hooks/hooks.json | 16 + pnpm-lock.yaml | 2039 +++++++++++++++++- scripts/build-cli.mjs | 42 + scripts/build-main.mjs | 44 + scripts/ci/check-plugin.mjs | 92 + scripts/ci/package-manifests.mjs | 156 ++ scripts/ci/release-version.mjs | 84 + 29 files changed, 3434 insertions(+), 38 deletions(-) create mode 100644 .claude-plugin/marketplace.json create mode 100644 README.md create mode 100644 apps/desktop/build-resources/installer.nsh create mode 100644 packages/access/src/socket.ts create mode 100644 packages/access/tests/socket.spec.ts create mode 100644 packages/cli/tests/release.spec.ts create mode 100644 packages/cli/types/release-scripts.d.ts create mode 100644 plugins/open-wiki/.claude-plugin/plugin.json create mode 100644 plugins/open-wiki/README.md create mode 100644 plugins/open-wiki/commands/ow-init.md create mode 100644 plugins/open-wiki/hooks/hooks.json create mode 100644 scripts/build-cli.mjs create mode 100644 scripts/build-main.mjs create mode 100644 scripts/ci/check-plugin.mjs create mode 100644 scripts/ci/package-manifests.mjs create mode 100644 scripts/ci/release-version.mjs diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..2637c92 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,19 @@ +{ + "name": "protonspy", + "owner": { + "name": "protonspy", + "url": "https://github.com/protonspy" + }, + "plugins": [ + { + "name": "open-wiki", + "source": "./plugins/open-wiki", + "description": "The write gate and the scaffolding command for an open-wiki project: PreToolUse validates a page before it exists and PostToolUse records it.", + "version": "0.1.0", + "author": { "name": "protonspy" }, + "homepage": "https://github.com/protonspy/open-wiki", + "license": "Apache-2.0", + "keywords": ["wiki", "documentation", "provenance"] + } + ] +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05875cb..7180537 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,12 +126,34 @@ jobs: - run: cargo test --locked --all-features + # 10.6 — the plugin is a manifest and a hooks file, and a malformed one + # fails at `/plugin install` on somebody else's machine. Ubuntu because it + # only reads JSON. + plugin: + name: plugin + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: The marketplace and the plugin manifest are well formed + run: node scripts/ci/check-plugin.mjs + + - name: claude plugin validate --strict + continue-on-error: true + run: | + npm i -g @anthropic-ai/claude-code >/dev/null 2>&1 || exit 0 + claude plugin validate --strict . || exit 1 + # The single check to require on the branch. Skipped jobs are fine — an empty # workspace has nothing to test — but a failed or cancelled one is not. ci: name: CI if: always() - needs: [discover, test, checks, rust] + needs: [discover, test, checks, rust, plugin] runs-on: ubuntu-latest steps: - name: Fail if any job failed or was cancelled @@ -144,3 +166,4 @@ jobs: echo "test: ${{ needs.test.result }}" echo "checks: ${{ needs.checks.result }}" echo "rust: ${{ needs.rust.result }}" + echo "plugin: ${{ needs.plugin.result }}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3b14c2f..b59a200 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,17 +30,13 @@ jobs: exit 1 } - - name: Check the tag matches the app version + # Both artifacts, one version. The installer and the npm package ship + # from this tag, and `adr:0014` names what a skew costs: it "fails + # looking like corrupted state rather than a bad install". + - name: Check the tag and both artifacts agree if: startsWith(github.ref, 'refs/tags/v') shell: pwsh - run: | - $tag = "${{ github.ref_name }}".TrimStart("v") - $version = (Get-Content "apps/desktop/package.json" -Raw | ConvertFrom-Json).version - if ($tag -ne $version) { - Write-Error "tag v$tag does not match apps/desktop/package.json version $version" - exit 1 - } - Write-Host "releasing $version" + run: node scripts/ci/release-version.mjs "${{ github.ref_name }}" - name: Refuse to republish an existing release if: startsWith(github.ref, 'refs/tags/v') @@ -65,6 +61,18 @@ jobs: - run: pnpm install --frozen-lockfile + # The installer carries them; `vendor/ffmpeg/` is gitignored and fetched + # with hash verification, and the recorder is the one Rust crate. + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - run: cargo build --release --locked + + - name: Fetch and verify ffmpeg + shell: pwsh + env: + FFMPEG_SHA256: ${{ vars.FFMPEG_SHA256 }} + run: node scripts/fetch-ffmpeg.mjs + - name: Build the installer shell: pwsh env: @@ -92,6 +100,29 @@ jobs: Out-File -FilePath "$dir/SHA256SUMS.txt" -Encoding utf8 Get-Content "$dir/SHA256SUMS.txt" + # 10.3 — the second artifact, from the same tag. Provenance is on, and + # it is not decoration: a product that verifies the SHA256 of its own + # ffmpeg cannot ship a `npx` entry point that verifies nothing. + - name: Publish the CLI to npm + if: startsWith(github.ref, 'refs/tags/v') + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + pnpm --filter open-wiki run build + pnpm --filter open-wiki publish --access public --no-git-checks + + # 10.4 — the manifests, generated from the tag and the hash that was just + # published. A manifest carrying last release's hash fails as "the + # download is corrupt" rather than "somebody forgot to update a file". + - name: Generate the winget and Scoop manifests + if: startsWith(github.ref, 'refs/tags/v') + shell: pwsh + run: | + $line = (Get-Content "apps/desktop/release/SHA256SUMS.txt" | Select-Object -First 1) + $sha = $line.Split(" ")[0] + node scripts/ci/package-manifests.mjs "${{ github.ref_name }}" $sha "dist/manifests" + Compress-Archive -Path "dist/manifests/*" -DestinationPath "apps/desktop/release/manifests.zip" + - name: Publish the release if: startsWith(github.ref, 'refs/tags/v') uses: softprops/action-gh-release@v2 diff --git a/.gitignore b/.gitignore index da3ad22..2a0e79e 100644 --- a/.gitignore +++ b/.gitignore @@ -146,3 +146,10 @@ vite.config.ts.timestamp-* vendor/ffmpeg/ # Rust build output (crates/) /target/ + +# Bundled output. `build-cli.mjs` and `build-main.mjs` produce these; the +# installer and the npm package carry them, and neither is committed. +packages/cli/build/ +apps/desktop/build/ +apps/desktop/release/ +dist/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..a548109 --- /dev/null +++ b/README.md @@ -0,0 +1,110 @@ +# open-wiki + +A project's documentation, as a wiki the AI agent already has open. + +Today a project's documentation is scattered: an architecture PDF, a +requirements `.docx`, decisions that exist only in a recorded meeting. None of +it answers _what is the current state of project X and how did we get here_, and +none of it is read by an agent without somebody pasting it into a prompt by +hand. + +open-wiki does three things and refuses the rest. It **takes in sources** — a +file or a recording — and reduces them to text with provenance anchors. It +**stores the wiki** as validated markdown. And it **lives inside the project +directory**, which is where the harness is already working, so reading it needs +no protocol at all. + +**The application calls no LLM.** Reading the source text, applying the +convention and writing the pages is the agent's job. The application does not +write content; it validates what comes in and records everything that changes. + +Windows 10/11. Apache-2.0. + +## Install + +```powershell +winget install protonspy.open-wiki +``` + +or `scoop install open-wiki`, or the installer from +[Releases](https://github.com/protonspy/open-wiki/releases). Every release +publishes a `SHA256SUMS.txt` beside the installer — check it. + +For the CLI alone, with nothing installed: + +``` +npx open-wiki init +``` + +### The SmartScreen warning + +The installer is **not code-signed**, so Windows SmartScreen will say +"Windows protected your PC" and name an unknown publisher. That is accurate: +there is no certificate behind this build. Verify the SHA256 against +`SHA256SUMS.txt` on the release page before choosing **More info → Run anyway**, +and if the hash does not match, do not run it. A certificate costs money and +proves the publisher is _someone_, not that the software is safe; the hash is +the thing that actually tells you the bytes are the ones that were built. + +## Using it + +`ow` in a project directory opens the application scoped to it, the way `code .` +does. `ow init` scaffolds `raw/`, `wiki/`, `.state/`, the convention as skills, +and a short `CLAUDE.md`. Then you talk to your agent in that same directory. + +## Recording a meeting + +This application records audio, from your microphone **and** from what your +computer is playing — which in a call is everybody else. + +**Telling the other people in the call that you are recording is your +responsibility, and in many places it is the law.** Recording a conversation +without consent is a criminal offence in a number of jurisdictions and grounds +for a civil claim in more; where one-party consent is enough it is still, at +minimum, a thing people are entitled to know. This software will not ask them +for you, and it has no way to tell whether you did. + +A recording indicator is in the window whenever capture is running, and it is +deliberately hard to miss — the failure it exists to prevent is somebody +forgetting it is on and ending up with a recording of a conversation the other +people in it believe ended. + +## Committing a wiki + +The project directory is usually a git repository, and that is your business — +this application neither reads nor writes one. But it is worth saying plainly +what committing a wiki does: + +**Everything in `wiki/` goes to everyone with repository access.** So does +everything in `raw/` except what the generated `.gitignore` excludes — recorded +audio and `.state/` are out by default, and committing them is opting in. That +default is not tidiness: `.state/` holds every page as it was before each write, +which is where a redaction survives the redaction. + +A meeting transcript is a verbatim record of what people said, including the +part they would not have written down. Read a source before you commit it. + +**The transcription credential is never in the project directory.** It lives in +the application's own data directory, keyed by project path, unconditionally — +because `git init` a week later turns a conditional rule into a leak. + +## What it does not do + +Extraction or page-writing by the application; chat inside the application; a +hosted service, accounts or telemetry; a block editor; real-time collaboration; +embeddings or a vector store; versioning (your git is welcome to it); macOS and +Linux; real-time transcription or a bot that joins the meeting. + +The reasoning for each is in [`docs/adr/`](docs/adr/), and the shape of the +whole thing is in [`plans/open-wiki.md`](plans/open-wiki.md). + +## Building it + +See [`.claude/rules/project.md`](.claude/rules/project.md) for the commands. +It is a pnpm workspace plus one Rust crate — the audio recorder, which is the +only thing here not written in TypeScript +(`adr:0014-typescript-everywhere-except-audio-capture`). + +## Licence + +Apache-2.0. See [LICENSE](LICENSE). diff --git a/apps/desktop/build-resources/installer.nsh b/apps/desktop/build-resources/installer.nsh new file mode 100644 index 0000000..cb263af --- /dev/null +++ b/apps/desktop/build-resources/installer.nsh @@ -0,0 +1,38 @@ +; The `ow` shim, and putting it on PATH (plan 10.1). +; +; `adr:0013-the-project-directory-is-the-unit` makes `ow` in a directory the +; way a project is opened — "the way `code .` does". That only works if `ow` +; is on PATH, so the installer is what puts it there; an application the user +; has to add to PATH by hand is an application whose main entrance is +; undocumented. +; +; The shim is a `.cmd` rather than a copy of the exe because it has to do two +; different things: with no arguments it opens the application scoped to the +; current directory, and with a subcommand it runs headless (9.2). + +!macro customInstall + ; A one-line launcher beside the application. + FileOpen $0 "$INSTDIR\ow.cmd" w + FileWrite $0 "@echo off$\r$\n" + FileWrite $0 "rem open-wiki launcher. Generated by the installer; do not edit.$\r$\n" + FileWrite $0 'if "%~1"=="" ($\r$\n' + FileWrite $0 ' start "" "%~dp0open-wiki.exe" --project "%CD%"$\r$\n' + FileWrite $0 ") else ($\r$\n" + FileWrite $0 ' "%~dp0resources\app.asar.unpacked\ow.mjs" %*$\r$\n' + FileWrite $0 ")$\r$\n" + FileClose $0 + + ; Per-user PATH, because the installer is per-user (`nsis.perMachine: false`). + ; `EnVar` writes the registry value and broadcasts the change, so a shell + ; opened after the install sees it without a reboot. + EnVar::SetHKCU + EnVar::AddValue "PATH" "$INSTDIR" + Pop $0 +!macroend + +!macro customUnInstall + EnVar::SetHKCU + EnVar::DeleteValue "PATH" "$INSTDIR" + Pop $0 + Delete "$INSTDIR\ow.cmd" +!macroend diff --git a/apps/desktop/package.json b/apps/desktop/package.json index e11ac42..498dd98 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,16 +1,19 @@ { "name": "@open-wiki/desktop", - "version": "0.0.0", + "version": "0.1.0", "private": true, "type": "module", - "main": "src/main/index.ts", + "main": "build/main/index.js", "scripts": { "dev": "vite", "build:renderer": "vite build", "test": "vitest run", "test:coverage": "vitest run --coverage", "typecheck": "tsc --noEmit -p tsconfig.json", - "lint": "eslint ." + "lint": "eslint .", + "build:main": "node ../../scripts/build-main.mjs", + "build": "pnpm run build:main && pnpm run build:renderer", + "package": "pnpm run build && electron-builder --win --publish never" }, "dependencies": { "@open-wiki/access": "workspace:*", @@ -26,6 +29,49 @@ "electron": "^38.0.0", "react": "^19.0.0", "react-dom": "^19.0.0", - "vite": "^6.0.0" + "vite": "^6.0.0", + "electron-builder": "^25.1.8" + }, + "build": { + "appId": "dev.protonspy.openwiki", + "productName": "open-wiki", + "copyright": "Copyright (c) protonspy. Apache-2.0.", + "directories": { + "output": "release", + "buildResources": "build-resources" + }, + "files": [ + "build/**/*", + "package.json" + ], + "extraResources": [ + { + "from": "../../vendor/ffmpeg/ffmpeg.exe", + "to": "ffmpeg.exe" + }, + { + "from": "../../target/release/recorder.exe", + "to": "recorder.exe" + } + ], + "win": { + "target": [ + { + "target": "nsis", + "arch": [ + "x64" + ] + } + ], + "artifactName": "open-wiki-Setup-${version}.exe" + }, + "nsis": { + "oneClick": false, + "perMachine": false, + "allowToChangeInstallationDirectory": true, + "createDesktopShortcut": true, + "include": "build-resources/installer.nsh" + }, + "publish": null } } diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 1da08a4..76a1ea5 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -4,6 +4,7 @@ import { fileURLToPath, pathToFileURL } from "node:url"; import { CHANNELS, createApi, dispatch } from "./ipc.js"; import { resolveProject } from "./project.js"; import { RecorderSession, resolveRecorder, spawnTransport } from "./recorder.js"; +import { serveQueries } from "@open-wiki/access/socket"; import { isOpenableExternally } from "../renderer/navigation.js"; import { watchProject } from "./watcher.js"; @@ -66,6 +67,11 @@ function createWindow(projectRoot: string | null): BrowserWindow { ipcMain.handle(channel, (_event, ...args: unknown[]) => dispatch(api, channel, args)); } + // 9.14 — the CLI asks here rather than starting a process, when this + // window already has the project open. Read and validate only; the socket + // never carries a write. + const queries = projectRoot ? serveQueries(projectRoot) : null; + // 8.10 — whoever wrote it, the screen follows. A launcher window has no // project to watch. const watcher = projectRoot @@ -76,6 +82,7 @@ function createWindow(projectRoot: string | null): BrowserWindow { window.on("closed", () => { void watcher?.close(); + queries?.close(); session?.dispose(); for (const channel of Object.values(CHANNELS)) ipcMain.removeHandler(channel); }); diff --git a/eslint.config.js b/eslint.config.js index d857858..f53ad85 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -17,6 +17,10 @@ export default tseslint.config( { ignores: [ "**/dist/**", + // Generated bundles. `build-cli.mjs` and `build-main.mjs` produce these; + // they are what the installer and the npm package carry, and linting a + // bundle reports on somebody else's source. + "**/build/**", "**/coverage/**", "**/node_modules/**", "**/release/**", diff --git a/package.json b/package.json index c80eaf2..cc0d5f8 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@eslint/js": "^9.20.0", "@types/node": "^22.15.3", "@vitest/coverage-v8": "^3.2.4", + "esbuild": "^0.24.0", "eslint": "^9.20.0", "eslint-config-prettier": "^10.0.1", "globals": "^16.0.0", diff --git a/packages/access/package.json b/packages/access/package.json index 5aa67b6..d0ad3d1 100644 --- a/packages/access/package.json +++ b/packages/access/package.json @@ -7,7 +7,8 @@ "exports": { ".": "./src/index.ts", "./read": "./src/read.ts", - "./secrets": "./src/config/secrets.ts" + "./secrets": "./src/config/secrets.ts", + "./socket": "./src/socket.ts" }, "scripts": { "test": "vitest run", diff --git a/packages/access/src/socket.ts b/packages/access/src/socket.ts new file mode 100644 index 0000000..4ee4ffe --- /dev/null +++ b/packages/access/src/socket.ts @@ -0,0 +1,190 @@ +import { createHash } from "node:crypto"; +import { createConnection, createServer, type Server } from "node:net"; +import { existsSync, readFileSync, unlinkSync } from "node:fs"; +import { join } from "node:path"; +import { checkProject } from "./check/checks.js"; +import { assertWithin } from "./paths.js"; +import { listPages } from "./store/index.js"; + +/** + * Talking to the running application (plan 9.14, second half). + * + * The first half is the bundle: an unbundled CLI pays Node's module resolution + * on every invocation, and a `PreToolUse` hook fires it on *every page write*. + * This is the rest — when the desktop application already has the project + * open, it has the answer in memory, and a query can be a round trip on a + * named pipe instead of a process start. + * + * **It carries read and validate and never write.** That is not a scope + * decision, it is the safety of the whole idea: a write over the socket would + * be a second writer into a project the application has open, with no snapshot + * of its own, and the operation log would record it as something the + * application did rather than something an agent did. So the write verb always + * pays the standalone path, and both paths produce the same answer — which is + * the property that makes an optimisation safe to have at all. + * + * **It is an optimisation, so absence is not failure.** `askRunningApp` + * answers `null` when nothing is listening, and the caller runs standalone. A + * CLI that failed because the application was not running would be a CLI that + * needs the application, and `adr:0013-the-project-directory-is-the-unit` says + * the opposite. + * + * Reached as `@open-wiki/access/socket`, not from the barrel: this module + * opens a listening socket, and the MCP process's read surface has no business + * being able to. + */ + +export const SOCKET_VERBS = ["read", "validate"] as const; +export type SocketVerb = (typeof SOCKET_VERBS)[number]; + +export interface SocketRequest { + verb: string; + args: string[]; +} + +export type SocketResponse = { ok: true; result: unknown } | { ok: false; error: string }; + +/** + * The pipe a project's application listens on. + * + * A hash of the path rather than the path itself, for the reason `secrets.ts` + * hashes it too: a pipe name is visible to every process on the machine, and + * the path carries somebody's username and the name of what they are working + * on. Windows named pipes are the only form that works on the platform this + * product supports; the same name is a filesystem socket elsewhere, which is + * what makes this testable off Windows. + */ +export function socketPath(projectRoot: string): string { + const id = createHash("sha256").update(projectRoot).digest("hex").slice(0, 16); + return process.platform === "win32" + ? `\\\\.\\pipe\\open-wiki-${id}` + : join(process.env["TMPDIR"] ?? "/tmp", `open-wiki-${id}.sock`); +} + +/** Whether a verb is one the socket may answer. Everything else writes. */ +export function isQueryVerb(verb: string): verb is SocketVerb { + return (SOCKET_VERBS as readonly string[]).includes(verb); +} + +/** + * Answer one request. + * + * The same functions the standalone path calls, so "both paths produce the + * same answer" is true by construction rather than by two implementations + * agreeing. + */ +export function handleRequest(projectRoot: string, request: SocketRequest): SocketResponse { + if (!isQueryVerb(request.verb)) { + return { + ok: false, + error: `the socket carries read and validate and never write — "${request.verb}" runs standalone`, + }; + } + try { + if (request.verb === "validate") { + return { ok: true, result: checkProject(projectRoot) }; + } + const slug = request.args[0] ?? ""; + // Resolved through the index, never by joining the slug onto a path: a + // slug arrives over a socket, which is the least trusted place it comes + // from (`adr:0016` makes the index the only thing that knows where a page + // sits, so the correct implementation is also the confined one). + const ref = listPages(projectRoot).find((p) => p.slug === slug); + if (!ref) return { ok: false, error: `no page "${slug}" in this wiki` }; + const file = assertWithin(projectRoot, join(projectRoot, ref.path)); + if (!existsSync(file)) return { ok: false, error: `no page "${slug}" in this wiki` }; + return { ok: true, result: readFileSync(file, "utf8") }; + } catch (e) { + return { ok: false, error: e instanceof Error ? e.message : String(e) }; + } +} + +/** One JSON object per line, in and out — the framing the recorder uses too. */ +function encode(value: unknown): string { + return `${JSON.stringify(value)}\n`; +} + +/** + * Listen for queries about this project. Returns something the window closes + * when it does — a server left behind answers about a project nobody has open. + */ +export function serveQueries(projectRoot: string): Server { + const server = createServer((socket) => { + let buffered = ""; + socket.on("data", (chunk: Buffer) => { + buffered += chunk.toString("utf8"); + let newline = buffered.indexOf("\n"); + while (newline >= 0) { + const line = buffered.slice(0, newline); + buffered = buffered.slice(newline + 1); + if (line.trim()) { + let response: SocketResponse; + try { + response = handleRequest(projectRoot, JSON.parse(line) as SocketRequest); + } catch { + response = { ok: false, error: "that is not a request" }; + } + socket.write(encode(response)); + } + newline = buffered.indexOf("\n"); + } + }); + // A client that vanished mid-request is ordinary, not an error worth + // taking the main process down for. + socket.on("error", () => socket.destroy()); + }); + server.on("error", () => {}); + const path = socketPath(projectRoot); + // A stale socket file from a process that was killed would otherwise make + // every later listen fail with EADDRINUSE. + if (process.platform !== "win32" && existsSync(path)) { + try { + unlinkSync(path); + } catch { + /* nothing listening and nothing to remove */ + } + } + server.listen(path); + return server; +} + +/** + * Ask the running application, or answer `null` when there is none. + * + * Every failure — nothing listening, a stale pipe, a timeout, a response that + * is not JSON — reads as "no application", because the caller's fallback is + * correct in all of them and slower in none that matter. + */ +export function askRunningApp( + projectRoot: string, + request: SocketRequest, + timeoutMs = 300, +): Promise { + return new Promise((resolve) => { + let settled = false; + const done = (value: SocketResponse | null): void => { + if (settled) return; + settled = true; + socket.destroy(); + resolve(value); + }; + const socket = createConnection(socketPath(projectRoot)); + // Short: the whole point is to be faster than starting a process, so a + // socket that is slow to answer has already lost its reason to exist. + socket.setTimeout(timeoutMs, () => done(null)); + socket.on("error", () => done(null)); + socket.on("connect", () => socket.write(encode(request))); + + let buffered = ""; + socket.on("data", (chunk: Buffer) => { + buffered += chunk.toString("utf8"); + const newline = buffered.indexOf("\n"); + if (newline < 0) return; + try { + done(JSON.parse(buffered.slice(0, newline)) as SocketResponse); + } catch { + done(null); + } + }); + }); +} diff --git a/packages/access/tests/socket.spec.ts b/packages/access/tests/socket.spec.ts new file mode 100644 index 0000000..d783475 --- /dev/null +++ b/packages/access/tests/socket.spec.ts @@ -0,0 +1,134 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { + askRunningApp, + handleRequest, + isQueryVerb, + serveQueries, + socketPath, + SOCKET_VERBS, +} from "../src/socket.js"; + +let root: string; + +beforeEach(() => { + root = mkdtempSync(join(tmpdir(), "ow-socket-")); + for (const part of ["raw", "wiki", ".state"]) mkdirSync(join(root, part), { recursive: true }); + writeFileSync(join(root, "wiki", "index.md"), "# Index\n", "utf8"); +}); + +afterEach(() => rmSync(root, { recursive: true, force: true })); + +function page(slug: string, body = "body\n"): void { + const front = [ + `id: topic:${slug}`, + "type: topic", + `title: ${slug}`, + "status: active", + "aliases: []", + "updated: 2026-07-01", + "sources: []", + 'superseded-by: ""', + ].join("\n"); + writeFileSync(join(root, "wiki", `${slug}.md`), `---\n${front}\n---\n\n${body}`, "utf8"); +} + +describe("socketPath", () => { + it("is per project, so two open projects do not share one", () => { + const other = mkdtempSync(join(tmpdir(), "ow-other-")); + try { + expect(socketPath(root)).not.toBe(socketPath(other)); + } finally { + rmSync(other, { recursive: true, force: true }); + } + }); + + it("is stable for one project", () => { + expect(socketPath(root)).toBe(socketPath(root)); + }); + + it("carries no part of the path, which is somebody's username", () => { + expect(socketPath(root)).not.toContain(root.split(/[\\/]/).pop()); + }); +}); + +describe("isQueryVerb (9.14)", () => { + it("accepts the two the socket carries", () => { + expect(SOCKET_VERBS).toEqual(["read", "validate"]); + for (const verb of SOCKET_VERBS) expect(isQueryVerb(verb)).toBe(true); + }); + + it("refuses every verb that writes", () => { + // The socket carries read and validate and **never** write — so the write + // verb always pays the standalone path. A write that went over the socket + // would be a second writer into a project the application has open, with + // no snapshot of its own and nothing to undo. + for (const verb of ["write", "init", "gate", "undo", "consult", "mcp", ""]) { + expect(isQueryVerb(verb)).toBe(false); + } + }); +}); + +describe("handleRequest (9.14)", () => { + it("answers a read", () => { + page("fenix"); + const response = handleRequest(root, { verb: "read", args: ["fenix"] }); + expect(response.ok).toBe(true); + expect(response.ok && String(response.result)).toContain("topic:fenix"); + }); + + it("answers a validate", () => { + page("fenix"); + const response = handleRequest(root, { verb: "validate", args: [] }); + expect(response.ok).toBe(true); + }); + + it("refuses a verb that writes, rather than doing it", () => { + const response = handleRequest(root, { verb: "write", args: ["wiki/fenix.md", "x"] }); + expect(response.ok).toBe(false); + expect(!response.ok && response.error).toMatch(/read and validate/); + }); + + it("refuses a page that is not there rather than throwing", () => { + const response = handleRequest(root, { verb: "read", args: ["ghost"] }); + expect(response.ok).toBe(false); + }); +}); + +describe("the socket end to end (9.14)", () => { + it("answers over the wire what the standalone path answers", async () => { + // "Both paths produce the same answer" is the requirement, and it is the + // one that makes the optimisation safe to have at all. + page("fenix"); + const server = serveQueries(root); + try { + const overWire = await askRunningApp(root, { verb: "read", args: ["fenix"] }); + const standalone = handleRequest(root, { verb: "read", args: ["fenix"] }); + expect(overWire).toEqual(standalone); + } finally { + server.close(); + } + }, 15_000); + + it("answers null when nothing is listening, so the caller falls back", async () => { + // The socket is an optimisation. A CLI that failed because the application + // was not running would be a CLI that needs the application. + expect(await askRunningApp(root, { verb: "read", args: ["fenix"] }, 500)).toBeNull(); + }, 15_000); + + it("refuses a write over the wire", async () => { + page("fenix"); + const server = serveQueries(root); + try { + const response = await askRunningApp(root, { + verb: "write", + args: ["wiki/fenix.md", "clobbered"], + }); + expect(response?.ok).toBe(false); + } finally { + server.close(); + } + }, 15_000); +}); diff --git a/packages/cli/package.json b/packages/cli/package.json index 54eff17..c1031f6 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,10 +1,10 @@ { "name": "open-wiki", - "version": "0.0.0", - "private": true, + "version": "0.1.0", + "private": false, "type": "module", "bin": { - "ow": "src/cli.ts" + "ow": "build/ow.mjs" }, "exports": { ".": "./src/index.ts" @@ -13,10 +13,28 @@ "test": "vitest run", "test:coverage": "vitest run --coverage", "typecheck": "tsc --noEmit -p tsconfig.json", - "lint": "eslint ." + "lint": "eslint .", + "build": "node ../../scripts/build-cli.mjs", + "prepack": "pnpm run build" }, "dependencies": { "@open-wiki/access": "workspace:*", "@open-wiki/mcp": "workspace:*" - } + }, + "description": "Build and maintain a project's wiki from its own sources — the CLI half of open-wiki.", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/protonspy/open-wiki.git", + "directory": "packages/cli" + }, + "homepage": "https://github.com/protonspy/open-wiki", + "files": [ + "build" + ], + "publishConfig": { + "access": "public", + "provenance": true + }, + "devDependencies": {} } diff --git a/packages/cli/src/main.ts b/packages/cli/src/main.ts index 0dca30f..c458c74 100644 --- a/packages/cli/src/main.ts +++ b/packages/cli/src/main.ts @@ -6,6 +6,7 @@ import { runGraph } from "./commands/graph.js"; import { runSearch } from "./commands/search.js"; import { runConsultAdd } from "./commands/consult.js"; import { parseCheckArgs, runCheck, CHECK_FAILED_TO_RUN } from "./commands/check.js"; +import { askRunningApp, handleRequest } from "@open-wiki/access/socket"; import { today } from "./date.js"; /** @@ -24,6 +25,7 @@ Usage: ow init [--language ] [--name ] scaffold a project and install the gate ow write [--content | --file ] write a page through the gate (no-hook path) ow gate pre|post the hook handlers (read JSON on stdin) + ow read print a page, through the running app when there is one ow check [--json] [--errors-only] the integrity checks; exit 2 means errors ow graph [superseded|orphans|index] structural queries, as JSON ow search lexical search over the wiki, as JSON @@ -87,6 +89,19 @@ export async function main(argv: string[], projectRoot: string = process.cwd()): return runGateCommand(kind); } + case "read": { + // 9.14 — the socket first, standalone when nothing is listening. The + // application already has this project open more often than not, and a + // hook fires this on every page write. + const slug = argv[1]; + if (!slug) return fail("ow read needs a page slug"); + const answered = await askRunningApp(projectRoot, { verb: "read", args: [slug] }); + const result = answered ?? handleRequest(projectRoot, { verb: "read", args: [slug] }); + if (!result.ok) return fail(result.error); + process.stdout.write(String(result.result)); + return 0; + } + case "check": { try { const { stdout, code } = runCheck(projectRoot, parseCheckArgs(argv.slice(1))); diff --git a/packages/cli/tests/release.spec.ts b/packages/cli/tests/release.spec.ts new file mode 100644 index 0000000..4a2f24e --- /dev/null +++ b/packages/cli/tests/release.spec.ts @@ -0,0 +1,202 @@ +import { describe, expect, it } from "vitest"; +/* + * The release scripts are plain ESM on purpose: CI runs them with `node` and + * nothing bundles them, so they carry no types. Imported dynamically and typed + * as `any` here rather than with a `.d.ts` nobody would keep in step. + */ +const { checkRelease, isPrerelease, versionOfTag } = + await import("../../../scripts/ci/release-version.mjs"); +const { installerUrl, scoopManifest, wingetManifests } = + await import("../../../scripts/ci/package-manifests.mjs"); + +/** A `read` that answers from a map rather than the filesystem. */ +function reader(versions: Record) { + return (path: string) => { + const key = path.replace(/\\/g, "/"); + const match = Object.keys(versions).find((k) => key.endsWith(k)); + if (!match) throw new Error(`no such file: ${path}`); + return { version: versions[match] }; + }; +} + +describe("versionOfTag (10.2)", () => { + it("reads a release tag", () => { + expect(versionOfTag("v0.1.0")).toBe("0.1.0"); + expect(versionOfTag("v10.20.30")).toBe("10.20.30"); + }); + + it("reads a prerelease tag", () => { + expect(versionOfTag("v0.1.0-beta.1")).toBe("0.1.0-beta.1"); + expect(isPrerelease("v0.1.0-beta.1")).toBe(true); + expect(isPrerelease("v0.1.0")).toBe(false); + }); + + it("refuses anything that is not one", () => { + for (const tag of ["0.1.0", "v0.1", "vlatest", "", "v1.2.3.4"]) { + expect(versionOfTag(tag)).toBeNull(); + } + }); +}); + +describe("checkRelease (10.3)", () => { + const both = { "apps/desktop/package.json": "0.1.0", "packages/cli/package.json": "0.1.0" }; + + it("accepts a tag both artifacts agree with", () => { + expect(checkRelease("v0.1.0", ".", reader(both))).toEqual({ ok: true, version: "0.1.0" }); + }); + + it("refuses when the installer and the npm package disagree", () => { + // `adr:0014` names the cost it accepted: a skew "fails looking like + // corrupted state rather than a bad install". A user with the application + // from one version and `npx open-wiki` resolving to another gets a CLI + // writing an older convention into their project, and nothing says so. + const result = checkRelease( + "v0.2.0", + ".", + reader({ "apps/desktop/package.json": "0.2.0", "packages/cli/package.json": "0.1.0" }), + ); + expect(result.ok).toBe(false); + expect(result.ok === false && result.problems.join(" ")).toContain("the CLI says 0.1.0"); + }); + + it("names every artifact that disagrees, not just the first", () => { + const result = checkRelease( + "v0.3.0", + ".", + reader({ "apps/desktop/package.json": "0.1.0", "packages/cli/package.json": "0.2.0" }), + ); + expect(result.ok === false && result.problems).toHaveLength(2); + }); + + it("refuses a tag that is not a release tag before reading anything", () => { + const result = checkRelease("nightly", ".", () => { + throw new Error("should not read"); + }); + expect(result.ok).toBe(false); + }); + + it("reports a manifest it could not read rather than passing", () => { + const result = checkRelease("v0.1.0", ".", reader({ "apps/desktop/package.json": "0.1.0" })); + expect(result.ok).toBe(false); + expect(result.ok === false && result.problems.join(" ")).toMatch(/could not be read/); + }); +}); + +describe("the package manifests (10.4)", () => { + const SHA = "a".repeat(64); + + it("points at the release URL for that exact version", () => { + expect(installerUrl("0.1.0")).toContain("/releases/download/v0.1.0/"); + expect(installerUrl("0.1.0")).toContain("open-wiki-Setup-0.1.0.exe"); + }); + + it("quotes the hash in the winget installer manifest", () => { + // A package manager that downloads without verifying is the + // fetch-and-execute this product refuses to ship. + const files = wingetManifests("0.1.0", SHA); + const installer = Object.entries(files).find(([p]) => p.endsWith("installer.yaml"))?.[1]; + expect(installer).toContain(`InstallerSha256: ${SHA.toUpperCase()}`); + expect(installer).toContain(installerUrl("0.1.0")); + }); + + it("writes the three files winget wants, under the version", () => { + const paths = Object.keys(wingetManifests("0.1.0", SHA)); + expect(paths).toHaveLength(3); + for (const path of paths) expect(path).toContain("/0.1.0/"); + }); + + it("quotes the hash in the Scoop manifest", () => { + const scoop = JSON.parse(scoopManifest("0.1.0", SHA.toUpperCase())); + expect(scoop.architecture["64bit"].hash).toBe(SHA); + expect(scoop.architecture["64bit"].url).toBe(installerUrl("0.1.0")); + }); + + it("keeps Scoop able to update itself from the published sums", () => { + const scoop = JSON.parse(scoopManifest("0.1.0", SHA)); + expect(scoop.autoupdate.hash.url).toContain("SHA256SUMS.txt"); + expect(scoop.version).toBe("0.1.0"); + }); + + it("declares the licence in both", () => { + expect(JSON.parse(scoopManifest("0.1.0", SHA)).license).toBe("Apache-2.0"); + const locale = Object.entries(wingetManifests("0.1.0", SHA)).find(([p]) => + p.endsWith("locale.en-US.yaml"), + )?.[1]; + expect(locale).toContain("License: Apache-2.0"); + }); +}); + +const { checkPlugin } = await import("../../../scripts/ci/check-plugin.mjs"); + +describe("checkPlugin (10.6)", () => { + const marketplace = { + plugins: [{ name: "open-wiki", source: "./plugins/open-wiki", version: "0.1.0" }], + }; + const manifest = { name: "open-wiki", version: "0.1.0" }; + const files = (over: Record = {}) => { + const map: Record = { + ".claude-plugin/marketplace.json": marketplace, + "plugins/open-wiki/.claude-plugin/plugin.json": manifest, + ...over, + }; + return (path: string) => { + const key = path.replace(/\\/g, "/"); + const hit = Object.keys(map).find((k) => key.endsWith(k)); + return hit ? map[hit] : null; + }; + }; + const exists = (present: string[]) => (path: string) => + present.some((p) => path.replace(/\\/g, "/").endsWith(p)); + + it("accepts a plugin the marketplace and the manifest agree on", () => { + expect(checkPlugin(".", files(), exists(["plugins/open-wiki"]))).toEqual({ ok: true }); + }); + + it("refuses when the two disagree on the version", () => { + // The marketplace is what a user installs from; a version that does not + // match the plugin is a install that silently gets something else. + const result = checkPlugin( + ".", + files({ + "plugins/open-wiki/.claude-plugin/plugin.json": { name: "open-wiki", version: "0.2.0" }, + }), + exists(["plugins/open-wiki"]), + ); + expect(result.ok).toBe(false); + expect(result.ok === false && result.problems.join(" ")).toContain("0.2.0"); + }); + + it("refuses a source path that is not there", () => { + const result = checkPlugin(".", files(), exists([])); + expect(result.ok).toBe(false); + expect(result.ok === false && result.problems.join(" ")).toMatch(/not there/); + }); + + it("refuses a plugin that ships the skills", () => { + // `adr:0015` gives the convention one home, and it is the project. A copy + // here would be a second, and two copies of a convention drift — which is + // the failure that record exists to prevent. + const result = checkPlugin( + ".", + files(), + exists(["plugins/open-wiki", "plugins/open-wiki/skills"]), + ); + expect(result.ok).toBe(false); + expect(result.ok === false && result.problems.join(" ")).toContain("adr:0015"); + }); + + it("refuses a plugin that ships a .mcp.json", () => { + const result = checkPlugin( + ".", + files(), + exists(["plugins/open-wiki", "plugins/open-wiki/.mcp.json"]), + ); + expect(result.ok).toBe(false); + expect(result.ok === false && result.problems.join(" ")).toMatch(/differ per user/); + }); + + it("says so when the marketplace itself will not parse", () => { + const result = checkPlugin(".", () => null, exists([])); + expect(result.ok).toBe(false); + }); +}); diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 71351fd..fd5f59a 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -1,4 +1,4 @@ { "extends": "../../tsconfig.base.json", - "include": ["src", "tests"] + "include": ["src", "tests", "types"] } diff --git a/packages/cli/types/release-scripts.d.ts b/packages/cli/types/release-scripts.d.ts new file mode 100644 index 0000000..f47bcee --- /dev/null +++ b/packages/cli/types/release-scripts.d.ts @@ -0,0 +1,33 @@ +/** + * The release scripts are plain ESM, deliberately: CI runs them with `node`, + * nothing bundles them, and giving them a build step would mean the thing that + * publishes a release depends on a build that has to happen first. + * + * So they carry no types, and these declarations exist only so the tests that + * exercise them typecheck. Deliberately loose — a hand-written `.d.ts` that + * described the real shapes would be a second declaration of them, drifting + * from the one that runs. + */ +declare module "*/scripts/ci/release-version.mjs" { + export function versionOfTag(tag: string): string | null; + export function isPrerelease(tag: string): boolean; + export function checkRelease( + tag: string, + repoRoot?: string, + read?: (path: string) => { version?: string }, + ): { ok: true; version: string } | { ok: false; problems: string[] }; +} + +declare module "*/scripts/ci/package-manifests.mjs" { + export function installerUrl(version: string): string; + export function wingetManifests(version: string, sha256: string): Record; + export function scoopManifest(version: string, sha256: string): string; +} + +declare module "*/scripts/ci/check-plugin.mjs" { + export function checkPlugin( + repoRoot?: string, + read?: (path: string) => unknown, + exists?: (path: string) => boolean, + ): { ok: true } | { ok: false; problems: string[] }; +} diff --git a/plans/open-wiki.md b/plans/open-wiki.md index 8f68bbc..fffbb77 100644 --- a/plans/open-wiki.md +++ b/plans/open-wiki.md @@ -313,19 +313,19 @@ of record. - [x] 9.11 (Unit) Announce the project in the server's name and description, so an agent with several configured says which base it answered from - [x] 9.12 (Unit) `ow graph` first and `ow search` after — the structural queries have no other owner and the supersession walk depends on the fields 5.2 records, where lexical search is what a scan over a few megabytes already does. Both are the local queries `adr:0013-the-project-directory-is-the-unit` sends to the CLI rather than to MCP, printing JSON - [x] 9.13 (Unit) A validation error readable enough for the agent to fix it on its own and try again — the same text whether it came from the CLI, a hook or the editor -- [ ] 9.14 (TDD) Pay down cold start: bundle the CLI to a single file, and talk to the running application over a local socket when there is one — the socket carries read and validate and never write, so the write verb always pays the standalone path, and both paths produce the same answer +- [x] 9.14 (TDD) Pay down cold start: bundle the CLI to a single file, and talk to the running application over a local socket when there is one — the socket carries read and validate and never write, so the write verb always pays the standalone path, and both paths produce the same answer - Deferred to group 8: the socket peer is the running desktop application, which does not exist until group 8, and bundling the CLI now then re-touching it for the socket is doing the work twice. The unbundled CLI runs the whole MVP path; the bundle is a cold-start optimisation, not a functional one. - [x] 9.15 (Unit) Verify end to end that Claude Code, working inside a project and starting from a single source, builds valid pages and then answers by citing them - [x] 9.16 (Unit) Verify that a second project with no wiki of its own consults the first through a committed `.mcp.json` naming it, and answers citing its pages ## 10 — Distribution -- [ ] 10.1 (Unit) A single NSIS installer with ffmpeg and `recorder.exe` embedded, written to `apps/desktop/release/`, with no external dependency to install, and the `ow` shim on `PATH` — `adr:0009-distribution-through-github-releases` -- [ ] 10.2 (Unit) Release from a `v*` tag: CI builds the installer, refuses a tag that disagrees with the app version or that already has a release, and publishes it to GitHub Releases with its `SHA256SUMS.txt` -- [ ] 10.3 (Unit) Publish the CLI to npm from the same tag, so `npx open-wiki init` works with nothing installed, and fail the release when the two artifacts disagree on version — claiming the package name before anyone else does, and publishing with provenance, because a product that verifies a hash on its own ffmpeg cannot ship a fetch-and-execute that verifies nothing — `adr:0014-typescript-everywhere-except-audio-capture` -- [ ] 10.4 (Unit) Publish to winget and Scoop, with the manifests pointing at the release URL and quoting its hash -- [ ] 10.5 (Unit) A README with the recording notice, the responsibility to inform participants, what the SmartScreen warning on an unsigned installer means, and what committing a wiki puts in front of everyone with repository access -- [ ] 10.6 (Unit) Package the hooks and the scaffolding command as an installable Claude Code plugin — never the skills themselves, which would be a second copy of the convention (`adr:0015-the-convention-ships-as-skills`), and never a `.mcp.json`, whose contents differ per user — with `.claude-plugin/marketplace.json` at the repository root so `/plugin marketplace add` reaches it, and `claude plugin validate --strict` in CI — see [[claude-code-plugins]] +- [x] 10.1 (Unit) A single NSIS installer with ffmpeg and `recorder.exe` embedded, written to `apps/desktop/release/`, with no external dependency to install, and the `ow` shim on `PATH` — `adr:0009-distribution-through-github-releases` +- [x] 10.2 (Unit) Release from a `v*` tag: CI builds the installer, refuses a tag that disagrees with the app version or that already has a release, and publishes it to GitHub Releases with its `SHA256SUMS.txt` +- [x] 10.3 (Unit) Publish the CLI to npm from the same tag, so `npx open-wiki init` works with nothing installed, and fail the release when the two artifacts disagree on version — claiming the package name before anyone else does, and publishing with provenance, because a product that verifies a hash on its own ffmpeg cannot ship a fetch-and-execute that verifies nothing — `adr:0014-typescript-everywhere-except-audio-capture` +- [x] 10.4 (Unit) Publish to winget and Scoop, with the manifests pointing at the release URL and quoting its hash +- [x] 10.5 (Unit) A README with the recording notice, the responsibility to inform participants, what the SmartScreen warning on an unsigned installer means, and what committing a wiki puts in front of everyone with repository access +- [x] 10.6 (Unit) Package the hooks and the scaffolding command as an installable Claude Code plugin — never the skills themselves, which would be a second copy of the convention (`adr:0015-the-convention-ships-as-skills`), and never a `.mcp.json`, whose contents differ per user — with `.claude-plugin/marketplace.json` at the repository root so `/plugin marketplace add` reaches it, and `claude plugin validate --strict` in CI — see [[claude-code-plugins]] --- diff --git a/plugins/open-wiki/.claude-plugin/plugin.json b/plugins/open-wiki/.claude-plugin/plugin.json new file mode 100644 index 0000000..65be37a --- /dev/null +++ b/plugins/open-wiki/.claude-plugin/plugin.json @@ -0,0 +1,9 @@ +{ + "name": "open-wiki", + "description": "The write gate and the scaffolding command for an open-wiki project.", + "version": "0.1.0", + "author": { "name": "protonspy" }, + "homepage": "https://github.com/protonspy/open-wiki", + "license": "Apache-2.0", + "keywords": ["wiki", "documentation", "provenance"] +} diff --git a/plugins/open-wiki/README.md b/plugins/open-wiki/README.md new file mode 100644 index 0000000..a5f87d3 --- /dev/null +++ b/plugins/open-wiki/README.md @@ -0,0 +1,36 @@ +# open-wiki, as a Claude Code plugin + +The write gate and the scaffolding command. Install it with: + +``` +/plugin marketplace add protonspy/open-wiki +/plugin install open-wiki@protonspy +``` + +## What is in here, and what deliberately is not + +**In:** the `PreToolUse` and `PostToolUse` hooks that make an agent's writes go +through the store, and an `/ow-init` command that scaffolds a project. + +The pair is what plan 9.5 settles. `PreToolUse` is handed a page's content +_before the file exists_, so it snapshots, validates, completes the frontmatter +through `updatedInput`, and denies with a reason when a page cannot be fixed by +filling a field in. `PostToolUse` appends the log, the changelog and the index +entry, because those describe a write that has actually happened. + +**Not in: the skills.** `adr:0015-the-convention-ships-as-skills` gives the +convention one home, and it is `.claude/skills/` inside each project, written by +`ow init`. Shipping them here would be a second copy, and two copies of a +convention drift — which is the exact failure that record exists to prevent. + +**Not in: a `.mcp.json`.** Its contents differ per user and per project: it names +_other_ projects this one consults, by name, and those names are on that +person's machine. `ow consult add ` writes it. + +## What the hooks cannot do + +A hook matches a tool. A page written through `Bash` arrives as a command +string with no page content to inspect, and denying `Edit(wiki/**)` does not +constrain `Bash` either, because permission rules are per tool. Where neither a +hook nor `ow write` is in the path, `ow check` is the only thing between a wrong +page and a permanent one — run it in CI. diff --git a/plugins/open-wiki/commands/ow-init.md b/plugins/open-wiki/commands/ow-init.md new file mode 100644 index 0000000..cd8132a --- /dev/null +++ b/plugins/open-wiki/commands/ow-init.md @@ -0,0 +1,19 @@ +--- +description: Scaffold an open-wiki project here — raw/, wiki/, .state/, the skills and a short CLAUDE.md +--- + +Run `npx -y open-wiki init` in the current project directory, then report what it +created and what it left alone. + +`ow init` is idempotent and overwrites nothing already there: the skills are +written only where none exist, and `CLAUDE.md` is generated. If the directory is +occupied by something that is not an open-wiki project it refuses rather than +scaffolding into it. + +Afterwards, tell the user the two things that are not obvious from the output: + +- The wiki is **theirs to write** — this application calls no LLM and never + writes a page. Building the wiki from what lands in `raw/` is your job, under + the convention in `.claude/skills/`. +- `raw/` is immutable once a source is sealed. `raw/_inbox/` is the one doorway: + material dropped there is ingested and the doorway emptied. diff --git a/plugins/open-wiki/hooks/hooks.json b/plugins/open-wiki/hooks/hooks.json new file mode 100644 index 0000000..c807398 --- /dev/null +++ b/plugins/open-wiki/hooks/hooks.json @@ -0,0 +1,16 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Write|Edit|MultiEdit", + "hooks": [{ "type": "command", "command": "npx -y open-wiki gate pre" }] + } + ], + "PostToolUse": [ + { + "matcher": "Write|Edit|MultiEdit", + "hooks": [{ "type": "command", "command": "npx -y open-wiki gate post" }] + } + ] + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9ee54e4..00eb8f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,7 +16,10 @@ importers: version: 22.20.1 '@vitest/coverage-v8': specifier: ^3.2.4 - version: 3.2.7(vitest@3.2.7(@types/node@22.20.1)(yaml@2.9.0)) + version: 3.2.7(vitest@3.2.7(@types/debug@4.1.13)(@types/node@22.20.1)(yaml@2.9.0)) + esbuild: + specifier: ^0.24.0 + version: 0.24.2 eslint: specifier: ^9.20.0 version: 9.39.5 @@ -37,7 +40,7 @@ importers: version: 8.65.0(eslint@9.39.5)(typescript@5.9.3) vitest: specifier: ^3.2.4 - version: 3.2.7(@types/node@22.20.1)(yaml@2.9.0) + version: 3.2.7(@types/debug@4.1.13)(@types/node@22.20.1)(yaml@2.9.0) apps/desktop: dependencies: @@ -69,6 +72,9 @@ importers: electron: specifier: ^38.0.0 version: 38.8.6 + electron-builder: + specifier: ^25.1.8 + version: 25.1.8(electron-builder-squirrel-windows@25.1.8) react: specifier: ^19.0.0 version: 19.2.8 @@ -122,6 +128,9 @@ importers: packages: + 7zip-bin@5.2.0: + resolution: {integrity: sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==} + '@ampproject/remapping@2.3.0': resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} @@ -213,10 +222,43 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@develar/schema-utils@2.6.5': + resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==} + engines: {node: '>= 8.9.0'} + + '@electron/asar@3.4.1': + resolution: {integrity: sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==} + engines: {node: '>=10.12.0'} + hasBin: true + '@electron/get@2.0.3': resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==} engines: {node: '>=12'} + '@electron/notarize@2.5.0': + resolution: {integrity: sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==} + engines: {node: '>= 10.0.0'} + + '@electron/osx-sign@1.3.1': + resolution: {integrity: sha512-BAfviURMHpmb1Yb50YbCxnOY0wfwaLXH5KJ4+80zS0gUkzDX3ec23naTlEqKsN+PwYn+a1cCzM7BJ4Wcd3sGzw==} + engines: {node: '>=12.0.0'} + hasBin: true + + '@electron/rebuild@3.6.1': + resolution: {integrity: sha512-f6596ZHpEq/YskUd8emYvOUne89ij8mQgjYFA5ru25QwbrRO+t1SImofdDv7kKOuWCmVOuU5tvfkbgGxIl3E/w==} + engines: {node: '>=12.13.0'} + hasBin: true + + '@electron/universal@2.0.1': + resolution: {integrity: sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA==} + engines: {node: '>=16.4'} + + '@esbuild/aix-ppc64@0.24.2': + resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} @@ -229,6 +271,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/android-arm64@0.24.2': + resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.25.12': resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} @@ -241,6 +289,12 @@ packages: cpu: [arm64] 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-arm@0.25.12': resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} @@ -253,6 +307,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-x64@0.24.2': + resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.25.12': resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} @@ -265,6 +325,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/darwin-arm64@0.24.2': + resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.25.12': resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} @@ -277,6 +343,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-x64@0.24.2': + resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.25.12': resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} @@ -289,6 +361,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/freebsd-arm64@0.24.2': + resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.25.12': resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} @@ -301,6 +379,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-x64@0.24.2': + resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.12': resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} @@ -313,6 +397,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/linux-arm64@0.24.2': + resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.25.12': resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} @@ -325,6 +415,12 @@ packages: cpu: [arm64] 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-arm@0.25.12': resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} @@ -337,6 +433,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-ia32@0.24.2': + resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.25.12': resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} @@ -349,6 +451,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-loong64@0.24.2': + resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.25.12': resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} @@ -361,6 +469,12 @@ packages: cpu: [loong64] 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-mips64el@0.25.12': resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} @@ -373,6 +487,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-ppc64@0.24.2': + resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.25.12': resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} @@ -385,6 +505,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-riscv64@0.24.2': + resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.25.12': resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} @@ -397,6 +523,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-s390x@0.24.2': + resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.25.12': resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} @@ -409,6 +541,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-x64@0.24.2': + resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.25.12': resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} @@ -421,6 +559,12 @@ packages: cpu: [x64] os: [linux] + '@esbuild/netbsd-arm64@0.24.2': + resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-arm64@0.25.12': resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} @@ -433,6 +577,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-x64@0.24.2': + resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.12': resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} @@ -445,6 +595,12 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/openbsd-arm64@0.24.2': + resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-arm64@0.25.12': resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} @@ -457,6 +613,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-x64@0.24.2': + resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.12': resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} @@ -481,6 +643,12 @@ packages: cpu: [arm64] os: [openharmony] + '@esbuild/sunos-x64@0.24.2': + resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.25.12': resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} @@ -493,6 +661,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/win32-arm64@0.24.2': + resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.25.12': resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} @@ -505,6 +679,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-ia32@0.24.2': + resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.25.12': resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} @@ -517,6 +697,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-x64@0.24.2': + resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.25.12': resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} @@ -567,6 +753,9 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@gar/promisify@1.1.3': + resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} + '@hono/node-server@2.0.12': resolution: {integrity: sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==} engines: {node: '>=20'} @@ -617,6 +806,14 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@malept/cross-spawn-promise@2.0.0': + resolution: {integrity: sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==} + engines: {node: '>= 12.13.0'} + + '@malept/flatpak-bundler@0.4.0': + resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==} + engines: {node: '>= 10.0.0'} + '@modelcontextprotocol/sdk@1.30.0': resolution: {integrity: sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==} engines: {node: '>=18'} @@ -697,6 +894,15 @@ packages: resolution: {integrity: sha512-OlI657a5XXvKGFX7kNeIzJ8rO7IXt87Mqu2H8rXE46viAuOfum/JA7ysX7+eBhxNKznT+RCZh418mndlcFX3+w==} engines: {node: '>= 10'} + '@npmcli/fs@2.1.2': + resolution: {integrity: sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + '@npmcli/move-file@2.0.1': + resolution: {integrity: sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This functionality has been moved to @npmcli/fs + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -837,6 +1043,10 @@ packages: resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} engines: {node: '>=10'} + '@tootallnate/once@2.0.1': + resolution: {integrity: sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==} + engines: {node: '>= 10'} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -855,12 +1065,18 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/fs-extra@9.0.13': + resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} + '@types/http-cache-semantics@4.2.0': resolution: {integrity: sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==} @@ -879,9 +1095,15 @@ packages: '@types/mdurl@2.0.0': resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@22.20.1': resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==} + '@types/plist@3.0.5': + resolution: {integrity: sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==} + '@types/react-dom@19.2.4': resolution: {integrity: sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==} peerDependencies: @@ -893,6 +1115,9 @@ packages: '@types/responselike@1.0.3': resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + '@types/verror@1.10.11': + resolution: {integrity: sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==} + '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} @@ -1003,6 +1228,13 @@ packages: resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} engines: {node: '>=10.0.0'} + '@xmldom/xmldom@0.9.10': + resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==} + engines: {node: '>=14.6'} + + abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -1017,6 +1249,22 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + ajv-formats@3.0.1: resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} peerDependencies: @@ -1025,6 +1273,11 @@ packages: ajv: optional: true + ajv-keywords@3.5.2: + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} @@ -1047,12 +1300,46 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} + app-builder-bin@5.0.0-alpha.10: + resolution: {integrity: sha512-Ev4jj3D7Bo+O0GPD2NMvJl+PGiBAfS7pUGawntBNpCbxtpncfUixqFj9z9Jme7V7s3LBGqsWZZP54fxBX3JKJw==} + + app-builder-lib@25.1.8: + resolution: {integrity: sha512-pCqe7dfsQFBABC1jeKZXQWhGcCPF3rPCXDdfqVKjIeWBcXzyC1iOWZdfFhGl+S9MyE/k//DFmC6FzuGAUudNDg==} + engines: {node: '>=14.0.0'} + peerDependencies: + dmg-builder: 25.1.8 + electron-builder-squirrel-windows: 25.1.8 + + aproba@2.1.0: + resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==} + + archiver-utils@2.1.0: + resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} + engines: {node: '>= 6'} + + archiver-utils@3.0.4: + resolution: {integrity: sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==} + engines: {node: '>= 10'} + + archiver@5.3.2: + resolution: {integrity: sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==} + engines: {node: '>= 10'} + + are-we-there-yet@3.0.1: + resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + assert-plus@1.0.0: + resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + engines: {node: '>=0.8'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -1060,6 +1347,24 @@ packages: ast-v8-to-istanbul@0.3.12: resolution: {integrity: sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==} + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + async-exit-hook@2.0.1: + resolution: {integrity: sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==} + engines: {node: '>=0.12.0'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1075,9 +1380,18 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + bluebird-lst@1.0.9: + resolution: {integrity: sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==} + bluebird@3.4.7: resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==} + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + body-parser@2.3.0: resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} engines: {node: '>=18'} @@ -1104,6 +1418,19 @@ packages: buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + builder-util-runtime@9.2.10: + resolution: {integrity: sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==} + engines: {node: '>=12.0.0'} + + builder-util@25.1.7: + resolution: {integrity: sha512-7jPjzBwEGRbwNcep0gGNpLXG9P94VA3CPAZQCzxkFXiV2GMQKlziMbY//rXPI7WKfhsvGgFXjTcXdBEwgXw9ww==} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -1112,6 +1439,10 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + cacache@16.1.3: + resolution: {integrity: sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + cacheable-lookup@5.0.4: resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} engines: {node: '>=10.6.0'} @@ -1151,9 +1482,44 @@ packages: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + chromium-pickle-js@0.2.0: + resolution: {integrity: sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-truncate@2.1.0: + resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} + engines: {node: '>=8'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + clone-response@1.0.3: resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1161,9 +1527,35 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@5.1.0: + resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} + engines: {node: '>= 6'} + + compare-version@0.1.2: + resolution: {integrity: sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==} + engines: {node: '>=0.10.0'} + + compress-commons@4.1.2: + resolution: {integrity: sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==} + engines: {node: '>= 10'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + config-file-ts@0.2.8-rc1: + resolution: {integrity: sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg==} + + console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + content-disposition@1.1.0: resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} engines: {node: '>=18'} @@ -1187,6 +1579,9 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + core-util-is@1.0.2: + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -1194,6 +1589,18 @@ packages: resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} engines: {node: '>= 0.10'} + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@4.0.3: + resolution: {integrity: sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==} + engines: {node: '>= 10'} + + crc@3.8.0: + resolution: {integrity: sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1221,6 +1628,9 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + defer-to-connect@2.0.1: resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} engines: {node: '>=10'} @@ -1233,16 +1643,47 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + detect-node@2.1.0: resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} dingbat-to-unicode@1.0.1: resolution: {integrity: sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==} + dir-compare@4.2.0: + resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==} + + dmg-builder@25.1.8: + resolution: {integrity: sha512-NoXo6Liy2heSklTI5OIZbCgXC1RzrDQsZkeEwXhdOro3FT1VBOvbubvscdPnjVuQ4AMwwv61oaH96AbiYg9EnQ==} + + dmg-license@1.0.11: + resolution: {integrity: sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==} + engines: {node: '>=8'} + os: [darwin] + hasBin: true + + dotenv-expand@11.0.7: + resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} + engines: {node: '>=12'} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + duck@0.1.12: resolution: {integrity: sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==} @@ -1256,6 +1697,22 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + + electron-builder-squirrel-windows@25.1.8: + resolution: {integrity: sha512-2ntkJ+9+0GFP6nAISiMabKt6eqBB0kX1QqHNWFWAXgi0VULKGisM46luRFpIBiU3u/TDmhZMM8tzvo2Abn3ayg==} + + electron-builder@25.1.8: + resolution: {integrity: sha512-poRgAtUHHOnlzZnc9PK4nzG53xh74wj2Jy7jkTrqZ0MWPoHGh1M2+C//hGeYdA+4K8w4yiVCNYoLXF7ySj2Wig==} + engines: {node: '>=14.0.0'} + hasBin: true + + electron-publish@25.1.7: + resolution: {integrity: sha512-+jbTkR9m39eDBMP4gfbqglDd6UvBC7RLh5Y0MhFSsc6UkGHj9Vj9TWobxevHYMMqmoujL11ZLjfPpMX+Pt6YEg==} + electron-to-chromium@1.5.399: resolution: {integrity: sha512-lEcqhErbHjXRvd41rnWLpzbyU/IXfIYo7QwaFWmxGeLiLyY2TBCdHnWY88vB+p3ubnihRypDm66panXl7TylLA==} @@ -1274,6 +1731,9 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} @@ -1285,6 +1745,9 @@ packages: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} + err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -1300,9 +1763,18 @@ packages: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + es6-error@4.1.1: resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + esbuild@0.24.2: + resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} + engines: {node: '>=18'} + hasBin: true + esbuild@0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} @@ -1395,6 +1867,9 @@ packages: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} + exponential-backoff@3.1.3: + resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} + express-rate-limit@8.6.1: resolution: {integrity: sha512-0D493aP61w0TJ2A0wy27riRsO7FMQ7FK+KUHOKCSfPvYo0R55aiC6emCVgFUeShH0fq0ICPVzNcgoS+BsbXQCA==} engines: {node: '>= 16'} @@ -1410,6 +1885,10 @@ packages: engines: {node: '>= 10.17.0'} hasBin: true + extsprintf@1.4.1: + resolution: {integrity: sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==} + engines: {'0': node >=0.6.0} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1438,6 +1917,9 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + filelist@1.0.6: + resolution: {integrity: sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==} + finalhandler@2.1.1: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} @@ -1457,6 +1939,10 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -1465,10 +1951,32 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-extra@11.4.0: + resolution: {integrity: sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==} + engines: {node: '>=14.14'} + fs-extra@8.1.0: resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} engines: {node: '>=6 <7 || >=8'} + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1477,10 +1985,19 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gauge@4.0.4: + resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -1502,6 +2019,15 @@ packages: deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + global-agent@3.0.0: resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} engines: {node: '>=10.0'} @@ -1540,6 +2066,13 @@ packages: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + hasown@2.0.4: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} @@ -1548,6 +2081,10 @@ packages: resolution: {integrity: sha512-+SwvkaiJtxsiPjhy9LivY/1m7UsNqCJetM1BrZl9A5DkQhlbHQDU730mMiDPWjnoCYOM8Chf3WrCJw27kNTPFQ==} engines: {node: '>=16.9.0'} + hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -1558,16 +2095,47 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} + http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + http2-wrapper@1.0.3: resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} engines: {node: '>=10.19.0'} - iconv-lite@0.7.3: - resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} - engines: {node: '>=0.10.0'} + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} - ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + + iconv-corefoundation@1.1.7: + resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==} + engines: {node: ^8.11.2 || >=10} + os: [darwin] + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} ignore@7.0.6: @@ -1585,6 +2153,17 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + infer-owner@1.0.4: + resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -1596,6 +2175,10 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + is-ci@3.0.1: + resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} + hasBin: true + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1608,12 +2191,31 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-lambda@1.0.1: + resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} + is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isbinaryfile@4.0.10: + resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==} + engines: {node: '>= 8.0.0'} + + isbinaryfile@5.0.7: + resolution: {integrity: sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==} + engines: {node: '>= 18.0.0'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1636,6 +2238,11 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jake@10.9.4: + resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==} + engines: {node: '>=10'} + hasBin: true + jose@6.2.6: resolution: {integrity: sha512-HwMtbJjMw8rC8dUTwCNilHJD+fxTeKM3JV1eprSmTjS41qwXSSt6exJXgyPK1QOu0jB9eDYLESRDkB3qaT3jnw==} @@ -1683,12 +2290,22 @@ packages: jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + jsonfile@6.2.1: + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + lazy-val@1.0.5: + resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==} + + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -1703,9 +2320,31 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.difference@4.5.0: + resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==} + + lodash.flatten@4.4.0: + resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.union@4.6.0: + resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + lop@0.4.2: resolution: {integrity: sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==} @@ -1722,6 +2361,14 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1732,6 +2379,10 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} + make-fetch-happen@10.2.1: + resolution: {integrity: sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + mammoth@1.12.0: resolution: {integrity: sha512-cwnK1RIcRdDMi2HRx2EXGYlxqIEh0Oo3bLhorgnsVJi2UkbX1+jKxuBNR9PC5+JaX7EkmJxFPmo6mjLpqShI2w==} engines: {node: '>=12.0.0'} @@ -1760,14 +2411,31 @@ packages: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + mime-db@1.54.0: resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} engines: {node: '>= 0.6'} + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + mime-types@3.0.2: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + mimic-response@1.0.1: resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} engines: {node: '>=4'} @@ -1783,14 +2451,58 @@ packages: minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + minimatch@9.0.9: resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass-collect@1.0.2: + resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} + engines: {node: '>= 8'} + + minipass-fetch@2.1.2: + resolution: {integrity: sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + minipass-flush@1.0.7: + resolution: {integrity: sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==} + engines: {node: '>= 8'} + + minipass-pipeline@1.2.4: + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} + + minipass-sized@1.0.3: + resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} + engines: {node: '>=8'} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1802,18 +2514,51 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + negotiator@1.0.0: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + node-abi@3.94.0: + resolution: {integrity: sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==} + engines: {node: '>=10'} + + node-addon-api@1.7.2: + resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==} + + node-api-version@0.2.1: + resolution: {integrity: sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==} + + node-gyp@9.4.1: + resolution: {integrity: sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==} + engines: {node: ^12.13 || ^14.13 || >=16} + hasBin: true + node-releases@2.0.51: resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} engines: {node: '>=18'} + nopt@6.0.0: + resolution: {integrity: sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + hasBin: true + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + normalize-url@6.1.0: resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} engines: {node: '>=10'} + npmlog@6.0.2: + resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -1833,6 +2578,10 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + option@0.2.4: resolution: {integrity: sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==} @@ -1840,6 +2589,10 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + p-cancelable@2.1.1: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} engines: {node: '>=8'} @@ -1852,6 +2605,10 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -1896,6 +2653,10 @@ packages: resolution: {integrity: sha512-YxFb+SQcodN2rnX9Tn3dHYlqfb7NjlzzfONPpJd+AKoKtUjEdevTfbC07d5TcczzOK6261auRkP/M8OBHs9vFQ==} engines: {node: '>=22.13.0 || >=24'} + pe-library@0.4.1: + resolution: {integrity: sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==} + engines: {node: '>=12', npm: '>=6'} + pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} @@ -1910,6 +2671,10 @@ packages: resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} engines: {node: '>=16.20.0'} + plist@3.1.1: + resolution: {integrity: sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==} + engines: {node: '>=10.4.0'} + postcss@8.5.25: resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} engines: {node: ^10 || ^12 || >=14} @@ -1930,6 +2695,18 @@ packages: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} + promise-inflight@1.0.1: + resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} + peerDependencies: + bluebird: '*' + peerDependenciesMeta: + bluebird: + optional: true + + promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -1974,17 +2751,36 @@ packages: resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} engines: {node: '>=0.10.0'} + read-binary-file-arch@1.0.6: + resolution: {integrity: sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==} + hasBin: true + readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + readdirp@5.0.0: resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} engines: {node: '>= 20.19.0'} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + resedit@1.7.2: + resolution: {integrity: sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==} + engines: {node: '>=12', npm: '>=6'} + resolve-alpn@1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} @@ -1995,6 +2791,19 @@ packages: responselike@2.0.1: resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + roarr@2.15.4: resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} engines: {node: '>=8.0'} @@ -2014,6 +2823,13 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sanitize-filename@1.6.4: + resolution: {integrity: sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==} + + sax@1.6.1: + resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==} + engines: {node: '>=11.0.0'} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -2041,6 +2857,9 @@ packages: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + setimmediate@1.0.5: resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} @@ -2074,23 +2893,61 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + + slice-ansi@3.0.0: + resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} + engines: {node: '>=8'} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks-proxy-agent@7.0.0: + resolution: {integrity: sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==} + engines: {node: '>= 10'} + + socks@2.8.9: + resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} sprintf-js@1.1.3: resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + ssri@9.0.1: + resolution: {integrity: sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + stat-mode@1.0.0: + resolution: {integrity: sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==} + engines: {node: '>= 6'} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -2132,6 +2989,18 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + temp-file@3.4.0: + resolution: {integrity: sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==} + test-exclude@7.0.2: resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==} engines: {node: '>=18'} @@ -2158,10 +3027,20 @@ packages: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} + tmp-promise@3.0.3: + resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} + + tmp@0.2.7: + resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} + engines: {node: '>=14.14'} + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + truncate-utf8-bytes@1.0.2: + resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} + ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} @@ -2201,10 +3080,22 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + unique-filename@2.0.1: + resolution: {integrity: sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + unique-slug@3.0.0: + resolution: {integrity: sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -2218,6 +3109,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + utf8-byte-length@1.0.5: + resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -2225,6 +3119,10 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + verror@1.10.1: + resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==} + engines: {node: '>=0.6.0'} + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -2338,6 +3236,9 @@ packages: jsdom: optional: true + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -2348,6 +3249,9 @@ packages: engines: {node: '>=8'} hasBin: true + wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -2367,14 +3271,33 @@ packages: resolution: {integrity: sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==} engines: {node: '>=4.0'} + xmlbuilder@15.1.1: + resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} + engines: {node: '>=8.0'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yaml@2.9.0: resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} hasBin: true + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} + engines: {node: '>=12'} + yauzl@2.10.0: resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} @@ -2382,6 +3305,10 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zip-stream@4.1.1: + resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} + engines: {node: '>= 10'} + zod-to-json-schema@3.25.2: resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} peerDependencies: @@ -2392,6 +3319,8 @@ packages: snapshots: + 7zip-bin@5.2.0: {} + '@ampproject/remapping@2.3.0': dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -2511,6 +3440,17 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} + '@develar/schema-utils@2.6.5': + dependencies: + ajv: 6.15.0 + ajv-keywords: 3.5.2(ajv@6.15.0) + + '@electron/asar@3.4.1': + dependencies: + commander: 5.1.0 + glob: 7.2.3 + minimatch: 3.1.5 + '@electron/get@2.0.3': dependencies: debug: 4.4.3 @@ -2525,126 +3465,240 @@ snapshots: transitivePeerDependencies: - supports-color + '@electron/notarize@2.5.0': + dependencies: + debug: 4.4.3 + fs-extra: 9.1.0 + promise-retry: 2.0.1 + transitivePeerDependencies: + - supports-color + + '@electron/osx-sign@1.3.1': + dependencies: + compare-version: 0.1.2 + debug: 4.4.3 + fs-extra: 10.1.0 + isbinaryfile: 4.0.10 + minimist: 1.2.8 + plist: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@electron/rebuild@3.6.1': + dependencies: + '@malept/cross-spawn-promise': 2.0.0 + chalk: 4.1.2 + debug: 4.4.3 + detect-libc: 2.1.2 + fs-extra: 10.1.0 + got: 11.8.6 + node-abi: 3.94.0 + node-api-version: 0.2.1 + node-gyp: 9.4.1 + ora: 5.4.1 + read-binary-file-arch: 1.0.6 + semver: 7.8.5 + tar: 6.2.1 + yargs: 17.7.3 + transitivePeerDependencies: + - bluebird + - supports-color + + '@electron/universal@2.0.1': + dependencies: + '@electron/asar': 3.4.1 + '@malept/cross-spawn-promise': 2.0.0 + debug: 4.4.3 + dir-compare: 4.2.0 + fs-extra: 11.4.0 + minimatch: 9.0.9 + plist: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@esbuild/aix-ppc64@0.24.2': + optional: true + '@esbuild/aix-ppc64@0.25.12': optional: true '@esbuild/aix-ppc64@0.28.1': optional: true + '@esbuild/android-arm64@0.24.2': + optional: true + '@esbuild/android-arm64@0.25.12': optional: true '@esbuild/android-arm64@0.28.1': optional: true + '@esbuild/android-arm@0.24.2': + optional: true + '@esbuild/android-arm@0.25.12': optional: true '@esbuild/android-arm@0.28.1': optional: true + '@esbuild/android-x64@0.24.2': + optional: true + '@esbuild/android-x64@0.25.12': optional: true '@esbuild/android-x64@0.28.1': optional: true + '@esbuild/darwin-arm64@0.24.2': + optional: true + '@esbuild/darwin-arm64@0.25.12': optional: true '@esbuild/darwin-arm64@0.28.1': optional: true + '@esbuild/darwin-x64@0.24.2': + optional: true + '@esbuild/darwin-x64@0.25.12': optional: true '@esbuild/darwin-x64@0.28.1': optional: true + '@esbuild/freebsd-arm64@0.24.2': + optional: true + '@esbuild/freebsd-arm64@0.25.12': optional: true '@esbuild/freebsd-arm64@0.28.1': optional: true + '@esbuild/freebsd-x64@0.24.2': + optional: true + '@esbuild/freebsd-x64@0.25.12': optional: true '@esbuild/freebsd-x64@0.28.1': optional: true + '@esbuild/linux-arm64@0.24.2': + optional: true + '@esbuild/linux-arm64@0.25.12': optional: true '@esbuild/linux-arm64@0.28.1': optional: true + '@esbuild/linux-arm@0.24.2': + optional: true + '@esbuild/linux-arm@0.25.12': optional: true '@esbuild/linux-arm@0.28.1': optional: true + '@esbuild/linux-ia32@0.24.2': + optional: true + '@esbuild/linux-ia32@0.25.12': optional: true '@esbuild/linux-ia32@0.28.1': optional: true + '@esbuild/linux-loong64@0.24.2': + optional: true + '@esbuild/linux-loong64@0.25.12': optional: true '@esbuild/linux-loong64@0.28.1': optional: true + '@esbuild/linux-mips64el@0.24.2': + optional: true + '@esbuild/linux-mips64el@0.25.12': optional: true '@esbuild/linux-mips64el@0.28.1': optional: true + '@esbuild/linux-ppc64@0.24.2': + optional: true + '@esbuild/linux-ppc64@0.25.12': optional: true '@esbuild/linux-ppc64@0.28.1': optional: true + '@esbuild/linux-riscv64@0.24.2': + optional: true + '@esbuild/linux-riscv64@0.25.12': optional: true '@esbuild/linux-riscv64@0.28.1': optional: true + '@esbuild/linux-s390x@0.24.2': + optional: true + '@esbuild/linux-s390x@0.25.12': optional: true '@esbuild/linux-s390x@0.28.1': optional: true + '@esbuild/linux-x64@0.24.2': + optional: true + '@esbuild/linux-x64@0.25.12': optional: true '@esbuild/linux-x64@0.28.1': optional: true + '@esbuild/netbsd-arm64@0.24.2': + optional: true + '@esbuild/netbsd-arm64@0.25.12': optional: true '@esbuild/netbsd-arm64@0.28.1': optional: true + '@esbuild/netbsd-x64@0.24.2': + optional: true + '@esbuild/netbsd-x64@0.25.12': optional: true '@esbuild/netbsd-x64@0.28.1': optional: true + '@esbuild/openbsd-arm64@0.24.2': + optional: true + '@esbuild/openbsd-arm64@0.25.12': optional: true '@esbuild/openbsd-arm64@0.28.1': optional: true + '@esbuild/openbsd-x64@0.24.2': + optional: true + '@esbuild/openbsd-x64@0.25.12': optional: true @@ -2657,24 +3711,36 @@ snapshots: '@esbuild/openharmony-arm64@0.28.1': optional: true + '@esbuild/sunos-x64@0.24.2': + optional: true + '@esbuild/sunos-x64@0.25.12': optional: true '@esbuild/sunos-x64@0.28.1': optional: true + '@esbuild/win32-arm64@0.24.2': + optional: true + '@esbuild/win32-arm64@0.25.12': optional: true '@esbuild/win32-arm64@0.28.1': optional: true + '@esbuild/win32-ia32@0.24.2': + optional: true + '@esbuild/win32-ia32@0.25.12': optional: true '@esbuild/win32-ia32@0.28.1': optional: true + '@esbuild/win32-x64@0.24.2': + optional: true + '@esbuild/win32-x64@0.25.12': optional: true @@ -2727,6 +3793,8 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@gar/promisify@1.1.3': {} + '@hono/node-server@2.0.12(hono@4.12.33)': dependencies: hono: 4.12.33 @@ -2777,6 +3845,19 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@malept/cross-spawn-promise@2.0.0': + dependencies: + cross-spawn: 7.0.6 + + '@malept/flatpak-bundler@0.4.0': + dependencies: + debug: 4.4.3 + fs-extra: 9.1.0 + lodash: 4.18.1 + tmp-promise: 3.0.3 + transitivePeerDependencies: + - supports-color + '@modelcontextprotocol/sdk@1.30.0(zod@4.4.3)': dependencies: '@hono/node-server': 2.0.12(hono@4.12.33) @@ -2847,6 +3928,16 @@ snapshots: '@napi-rs/canvas-win32-x64-msvc': 1.0.3 optional: true + '@npmcli/fs@2.1.2': + dependencies: + '@gar/promisify': 1.1.3 + semver: 7.8.5 + + '@npmcli/move-file@2.0.1': + dependencies: + mkdirp: 1.0.4 + rimraf: 3.0.2 + '@pkgjs/parseargs@0.11.0': optional: true @@ -2933,6 +4024,8 @@ snapshots: dependencies: defer-to-connect: 2.0.1 + '@tootallnate/once@2.0.1': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.29.8 @@ -2966,10 +4059,18 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} '@types/estree@1.0.9': {} + '@types/fs-extra@9.0.13': + dependencies: + '@types/node': 22.20.1 + '@types/http-cache-semantics@4.2.0': {} '@types/json-schema@7.0.15': {} @@ -2987,10 +4088,18 @@ snapshots: '@types/mdurl@2.0.0': {} + '@types/ms@2.1.0': {} + '@types/node@22.20.1': dependencies: undici-types: 6.21.0 + '@types/plist@3.0.5': + dependencies: + '@types/node': 22.20.1 + xmlbuilder: 15.1.1 + optional: true + '@types/react-dom@19.2.4(@types/react@19.2.18)': dependencies: '@types/react': 19.2.18 @@ -3003,6 +4112,9 @@ snapshots: dependencies: '@types/node': 22.20.1 + '@types/verror@1.10.11': + optional: true + '@types/yauzl@2.10.3': dependencies: '@types/node': 22.20.1 @@ -3111,7 +4223,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@3.2.7(vitest@3.2.7(@types/node@22.20.1)(yaml@2.9.0))': + '@vitest/coverage-v8@3.2.7(vitest@3.2.7(@types/debug@4.1.13)(@types/node@22.20.1)(yaml@2.9.0))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -3126,7 +4238,7 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.2 tinyrainbow: 2.0.0 - vitest: 3.2.7(@types/node@22.20.1)(yaml@2.9.0) + vitest: 3.2.7(@types/debug@4.1.13)(@types/node@22.20.1)(yaml@2.9.0) transitivePeerDependencies: - supports-color @@ -3174,6 +4286,10 @@ snapshots: '@xmldom/xmldom@0.8.13': {} + '@xmldom/xmldom@0.9.10': {} + + abbrev@1.1.1: {} + accepts@2.0.0: dependencies: mime-types: 3.0.2 @@ -3185,10 +4301,31 @@ snapshots: acorn@8.18.0: {} + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + agent-base@7.1.4: {} + + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + ajv-formats@3.0.1(ajv@8.20.0): optionalDependencies: ajv: 8.20.0 + ajv-keywords@3.5.2(ajv@6.15.0): + dependencies: + ajv: 6.15.0 + ajv@6.15.0: dependencies: fast-deep-equal: 3.1.3 @@ -3213,12 +4350,100 @@ snapshots: ansi-styles@6.2.3: {} + app-builder-bin@5.0.0-alpha.10: {} + + app-builder-lib@25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8): + dependencies: + '@develar/schema-utils': 2.6.5 + '@electron/notarize': 2.5.0 + '@electron/osx-sign': 1.3.1 + '@electron/rebuild': 3.6.1 + '@electron/universal': 2.0.1 + '@malept/flatpak-bundler': 0.4.0 + '@types/fs-extra': 9.0.13 + async-exit-hook: 2.0.1 + bluebird-lst: 1.0.9 + builder-util: 25.1.7 + builder-util-runtime: 9.2.10 + chromium-pickle-js: 0.2.0 + config-file-ts: 0.2.8-rc1 + debug: 4.4.3 + dmg-builder: 25.1.8(electron-builder-squirrel-windows@25.1.8) + dotenv: 16.6.1 + dotenv-expand: 11.0.7 + ejs: 3.1.10 + electron-builder-squirrel-windows: 25.1.8(dmg-builder@25.1.8) + electron-publish: 25.1.7 + form-data: 4.0.6 + fs-extra: 10.1.0 + hosted-git-info: 4.1.0 + is-ci: 3.0.1 + isbinaryfile: 5.0.7 + js-yaml: 4.3.1 + json5: 2.2.3 + lazy-val: 1.0.5 + minimatch: 10.2.6 + resedit: 1.7.2 + sanitize-filename: 1.6.4 + semver: 7.8.5 + tar: 6.2.1 + temp-file: 3.4.0 + transitivePeerDependencies: + - bluebird + - supports-color + + aproba@2.1.0: {} + + archiver-utils@2.1.0: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 2.3.8 + + archiver-utils@3.0.4: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + + archiver@5.3.2: + dependencies: + archiver-utils: 2.1.0 + async: 3.2.6 + buffer-crc32: 0.2.13 + readable-stream: 3.6.2 + readdir-glob: 1.1.3 + tar-stream: 2.2.0 + zip-stream: 4.1.1 + + are-we-there-yet@3.0.1: + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 argparse@2.0.1: {} + assert-plus@1.0.0: + optional: true + assertion-error@2.0.1: {} ast-v8-to-istanbul@0.3.12: @@ -3227,6 +4452,17 @@ snapshots: estree-walker: 3.0.3 js-tokens: 10.0.0 + astral-regex@2.0.0: + optional: true + + async-exit-hook@2.0.1: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + + at-least-node@1.0.0: {} + balanced-match@1.0.2: {} balanced-match@4.0.4: {} @@ -3235,8 +4471,20 @@ snapshots: baseline-browser-mapping@2.11.10: {} + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + bluebird-lst@1.0.9: + dependencies: + bluebird: 3.7.2 + bluebird@3.4.7: {} + bluebird@3.7.2: {} + body-parser@2.3.0: dependencies: bytes: 3.1.2 @@ -3277,10 +4525,68 @@ snapshots: buffer-crc32@0.2.13: {} + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + builder-util-runtime@9.2.10: + dependencies: + debug: 4.4.3 + sax: 1.6.1 + transitivePeerDependencies: + - supports-color + + builder-util@25.1.7: + dependencies: + 7zip-bin: 5.2.0 + '@types/debug': 4.1.13 + app-builder-bin: 5.0.0-alpha.10 + bluebird-lst: 1.0.9 + builder-util-runtime: 9.2.10 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + fs-extra: 10.1.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-ci: 3.0.1 + js-yaml: 4.3.1 + source-map-support: 0.5.21 + stat-mode: 1.0.0 + temp-file: 3.4.0 + transitivePeerDependencies: + - supports-color + bytes@3.1.2: {} cac@6.7.14: {} + cacache@16.1.3: + dependencies: + '@npmcli/fs': 2.1.2 + '@npmcli/move-file': 2.0.1 + chownr: 2.0.0 + fs-minipass: 2.1.0 + glob: 8.1.0 + infer-owner: 1.0.4 + lru-cache: 7.18.3 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-flush: 1.0.7 + minipass-pipeline: 1.2.4 + mkdirp: 1.0.4 + p-map: 4.0.0 + promise-inflight: 1.0.1 + rimraf: 3.0.2 + ssri: 9.0.1 + tar: 6.2.1 + unique-filename: 2.0.1 + transitivePeerDependencies: + - bluebird + cacheable-lookup@5.0.4: {} cacheable-request@7.0.4: @@ -3326,18 +4632,70 @@ snapshots: dependencies: readdirp: 5.0.0 + chownr@2.0.0: {} + + chromium-pickle-js@0.2.0: {} + + ci-info@3.9.0: {} + + clean-stack@2.2.0: {} + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-spinners@2.9.2: {} + + cli-truncate@2.1.0: + dependencies: + slice-ansi: 3.0.0 + string-width: 4.2.3 + optional: true + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + clone-response@1.0.3: dependencies: mimic-response: 1.0.1 + clone@1.0.4: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 color-name@1.1.4: {} + color-support@1.1.3: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@5.1.0: {} + + compare-version@0.1.2: {} + + compress-commons@4.1.2: + dependencies: + buffer-crc32: 0.2.13 + crc32-stream: 4.0.3 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + concat-map@0.0.1: {} + config-file-ts@0.2.8-rc1: + dependencies: + glob: 10.5.0 + typescript: 5.9.3 + + console-control-strings@1.1.0: {} + content-disposition@1.1.0: {} content-type@1.0.5: {} @@ -3350,6 +4708,9 @@ snapshots: cookie@0.7.2: {} + core-util-is@1.0.2: + optional: true + core-util-is@1.0.3: {} cors@2.8.6: @@ -3357,6 +4718,18 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 + crc-32@1.2.2: {} + + crc32-stream@4.0.3: + dependencies: + crc-32: 1.2.2 + readable-stream: 3.6.2 + + crc@3.8.0: + dependencies: + buffer: 5.7.1 + optional: true + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -3377,6 +4750,10 @@ snapshots: deep-is@0.1.4: {} + defaults@1.0.4: + dependencies: + clone: 1.0.4 + defer-to-connect@2.0.1: {} define-data-property@1.1.4: @@ -3393,13 +4770,57 @@ snapshots: object-keys: 1.1.1 optional: true + delayed-stream@1.0.0: {} + + delegates@1.0.0: {} + depd@2.0.0: {} + detect-libc@2.1.2: {} + detect-node@2.1.0: optional: true dingbat-to-unicode@1.0.1: {} + dir-compare@4.2.0: + dependencies: + minimatch: 3.1.5 + p-limit: 3.1.0 + + dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8): + dependencies: + app-builder-lib: 25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8) + builder-util: 25.1.7 + builder-util-runtime: 9.2.10 + fs-extra: 10.1.0 + iconv-lite: 0.6.3 + js-yaml: 4.3.1 + optionalDependencies: + dmg-license: 1.0.11 + transitivePeerDependencies: + - bluebird + - electron-builder-squirrel-windows + - supports-color + + dmg-license@1.0.11: + dependencies: + '@types/plist': 3.0.5 + '@types/verror': 1.10.11 + ajv: 6.15.0 + crc: 3.8.0 + iconv-corefoundation: 1.1.7 + plist: 3.1.1 + smart-buffer: 4.2.0 + verror: 1.10.1 + optional: true + + dotenv-expand@11.0.7: + dependencies: + dotenv: 16.6.1 + + dotenv@16.6.1: {} + duck@0.1.12: dependencies: underscore: 1.13.8 @@ -3414,6 +4835,50 @@ snapshots: ee-first@1.1.1: {} + ejs@3.1.10: + dependencies: + jake: 10.9.4 + + electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8): + dependencies: + app-builder-lib: 25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8) + archiver: 5.3.2 + builder-util: 25.1.7 + fs-extra: 10.1.0 + transitivePeerDependencies: + - bluebird + - dmg-builder + - supports-color + + electron-builder@25.1.8(electron-builder-squirrel-windows@25.1.8): + dependencies: + app-builder-lib: 25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8) + builder-util: 25.1.7 + builder-util-runtime: 9.2.10 + chalk: 4.1.2 + dmg-builder: 25.1.8(electron-builder-squirrel-windows@25.1.8) + fs-extra: 10.1.0 + is-ci: 3.0.1 + lazy-val: 1.0.5 + simple-update-notifier: 2.0.0 + yargs: 17.7.3 + transitivePeerDependencies: + - bluebird + - electron-builder-squirrel-windows + - supports-color + + electron-publish@25.1.7: + dependencies: + '@types/fs-extra': 9.0.13 + builder-util: 25.1.7 + builder-util-runtime: 9.2.10 + chalk: 4.1.2 + fs-extra: 10.1.0 + lazy-val: 1.0.5 + mime: 2.6.0 + transitivePeerDependencies: + - supports-color + electron-to-chromium@1.5.399: {} electron@38.8.6: @@ -3430,6 +4895,11 @@ snapshots: encodeurl@2.0.0: {} + encoding@0.1.13: + dependencies: + iconv-lite: 0.6.3 + optional: true + end-of-stream@1.4.5: dependencies: once: 1.4.0 @@ -3438,6 +4908,8 @@ snapshots: env-paths@2.2.1: {} + err-code@2.0.3: {} + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -3448,9 +4920,44 @@ snapshots: dependencies: es-errors: 1.3.0 + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + es6-error@4.1.1: optional: true + esbuild@0.24.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.24.2 + '@esbuild/android-arm': 0.24.2 + '@esbuild/android-arm64': 0.24.2 + '@esbuild/android-x64': 0.24.2 + '@esbuild/darwin-arm64': 0.24.2 + '@esbuild/darwin-x64': 0.24.2 + '@esbuild/freebsd-arm64': 0.24.2 + '@esbuild/freebsd-x64': 0.24.2 + '@esbuild/linux-arm': 0.24.2 + '@esbuild/linux-arm64': 0.24.2 + '@esbuild/linux-ia32': 0.24.2 + '@esbuild/linux-loong64': 0.24.2 + '@esbuild/linux-mips64el': 0.24.2 + '@esbuild/linux-ppc64': 0.24.2 + '@esbuild/linux-riscv64': 0.24.2 + '@esbuild/linux-s390x': 0.24.2 + '@esbuild/linux-x64': 0.24.2 + '@esbuild/netbsd-arm64': 0.24.2 + '@esbuild/netbsd-x64': 0.24.2 + '@esbuild/openbsd-arm64': 0.24.2 + '@esbuild/openbsd-x64': 0.24.2 + '@esbuild/sunos-x64': 0.24.2 + '@esbuild/win32-arm64': 0.24.2 + '@esbuild/win32-ia32': 0.24.2 + '@esbuild/win32-x64': 0.24.2 + esbuild@0.25.12: optionalDependencies: '@esbuild/aix-ppc64': 0.25.12 @@ -3601,6 +5108,8 @@ snapshots: expect-type@1.4.0: {} + exponential-backoff@3.1.3: {} + express-rate-limit@8.6.1(express@5.2.1): dependencies: debug: 4.4.3 @@ -3652,6 +5161,9 @@ snapshots: transitivePeerDependencies: - supports-color + extsprintf@1.4.1: + optional: true + fast-deep-equal@3.1.3: {} fast-json-stable-stringify@2.1.0: {} @@ -3672,6 +5184,10 @@ snapshots: dependencies: flat-cache: 4.0.1 + filelist@1.0.6: + dependencies: + minimatch: 5.1.9 + finalhandler@2.1.1: dependencies: debug: 4.4.3 @@ -3700,23 +5216,71 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + forwarded@0.2.0: {} fresh@2.0.0: {} + fs-constants@1.0.0: {} + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + + fs-extra@11.4.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + fs-extra@8.1.0: dependencies: graceful-fs: 4.2.11 jsonfile: 4.0.0 universalify: 0.1.2 + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + + fs.realpath@1.0.0: {} + fsevents@2.3.3: optional: true function-bind@1.1.2: {} + gauge@4.0.4: + dependencies: + aproba: 2.1.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + gensync@1.0.0-beta.2: {} + get-caller-file@2.0.5: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -3752,6 +5316,23 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + + glob@8.1.0: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.9 + once: 1.4.0 + global-agent@3.0.0: dependencies: boolean: 3.2.0 @@ -3799,12 +5380,22 @@ snapshots: has-symbols@1.1.0: {} + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + has-unicode@2.0.1: {} + hasown@2.0.4: dependencies: function-bind: 1.1.2 hono@4.12.33: {} + hosted-git-info@4.1.0: + dependencies: + lru-cache: 6.0.0 + html-escaper@2.0.2: {} http-cache-semantics@4.2.0: {} @@ -3817,15 +5408,60 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 + http-proxy-agent@5.0.0: + dependencies: + '@tootallnate/once': 2.0.1 + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + http2-wrapper@1.0.3: dependencies: quick-lru: 5.1.1 resolve-alpn: 1.2.1 + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + + iconv-corefoundation@1.1.7: + dependencies: + cli-truncate: 2.1.0 + node-addon-api: 1.7.2 + optional: true + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + iconv-lite@0.7.3: dependencies: safer-buffer: 2.1.2 + ieee754@1.2.1: {} + ignore@5.3.2: {} ignore@7.0.6: {} @@ -3839,12 +5475,25 @@ snapshots: imurmurhash@0.1.4: {} + indent-string@4.0.0: {} + + infer-owner@1.0.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + inherits@2.0.4: {} ip-address@10.4.0: {} ipaddr.js@1.9.1: {} + is-ci@3.0.1: + dependencies: + ci-info: 3.9.0 + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -3853,10 +5502,20 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-interactive@1.0.0: {} + + is-lambda@1.0.1: {} + is-promise@4.0.0: {} + is-unicode-supported@0.1.0: {} + isarray@1.0.0: {} + isbinaryfile@4.0.10: {} + + isbinaryfile@5.0.7: {} + isexe@2.0.0: {} istanbul-lib-coverage@3.2.2: {} @@ -3886,6 +5545,12 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jake@10.9.4: + dependencies: + async: 3.2.6 + filelist: 1.0.6 + picocolors: 1.1.1 + jose@6.2.6: {} js-tokens@10.0.0: {} @@ -3919,6 +5584,12 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonfile@6.2.1: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + jszip@3.10.1: dependencies: lie: 3.3.0 @@ -3930,6 +5601,12 @@ snapshots: dependencies: json-buffer: 3.0.1 + lazy-val@1.0.5: {} + + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -3947,8 +5624,25 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.defaults@4.2.0: {} + + lodash.difference@4.5.0: {} + + lodash.flatten@4.4.0: {} + + lodash.isplainobject@4.0.6: {} + lodash.merge@4.6.2: {} + lodash.union@4.6.0: {} + + lodash@4.18.1: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + lop@0.4.2: dependencies: duck: 0.1.12 @@ -3965,6 +5659,12 @@ snapshots: dependencies: yallist: 3.1.1 + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + + lru-cache@7.18.3: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -3979,6 +5679,28 @@ snapshots: dependencies: semver: 7.8.5 + make-fetch-happen@10.2.1: + dependencies: + agentkeepalive: 4.6.0 + cacache: 16.1.3 + http-cache-semantics: 4.2.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-lambda: 1.0.1 + lru-cache: 7.18.3 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-fetch: 2.1.2 + minipass-flush: 1.0.7 + minipass-pipeline: 1.2.4 + negotiator: 0.6.4 + promise-retry: 2.0.1 + socks-proxy-agent: 7.0.0 + ssri: 9.0.1 + transitivePeerDependencies: + - bluebird + - supports-color + mammoth@1.12.0: dependencies: '@xmldom/xmldom': 0.8.13 @@ -4014,12 +5736,22 @@ snapshots: merge-descriptors@2.0.0: {} + mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + mime-types@3.0.2: dependencies: mime-db: 1.54.0 + mime@2.6.0: {} + + mimic-fn@2.1.0: {} + mimic-response@1.0.1: {} mimic-response@3.1.0: {} @@ -4032,24 +5764,110 @@ snapshots: dependencies: brace-expansion: 1.1.18 + minimatch@5.1.9: + dependencies: + brace-expansion: 2.1.4 + minimatch@9.0.9: dependencies: brace-expansion: 2.1.4 + minimist@1.2.8: {} + + minipass-collect@1.0.2: + dependencies: + minipass: 3.3.6 + + minipass-fetch@2.1.2: + dependencies: + minipass: 3.3.6 + minipass-sized: 1.0.3 + minizlib: 2.1.2 + optionalDependencies: + encoding: 0.1.13 + + minipass-flush@1.0.7: + dependencies: + minipass: 3.3.6 + + minipass-pipeline@1.2.4: + dependencies: + minipass: 3.3.6 + + minipass-sized@1.0.3: + dependencies: + minipass: 3.3.6 + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + minipass@7.1.3: {} + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + + mkdirp@1.0.4: {} + ms@2.1.3: {} nanoid@3.3.16: {} natural-compare@1.4.0: {} + negotiator@0.6.4: {} + negotiator@1.0.0: {} + node-abi@3.94.0: + dependencies: + semver: 7.8.5 + + node-addon-api@1.7.2: + optional: true + + node-api-version@0.2.1: + dependencies: + semver: 7.8.5 + + node-gyp@9.4.1: + dependencies: + env-paths: 2.2.1 + exponential-backoff: 3.1.3 + glob: 7.2.3 + graceful-fs: 4.2.11 + make-fetch-happen: 10.2.1 + nopt: 6.0.0 + npmlog: 6.0.2 + rimraf: 3.0.2 + semver: 7.8.5 + tar: 6.2.1 + which: 2.0.2 + transitivePeerDependencies: + - bluebird + - supports-color + node-releases@2.0.51: {} + nopt@6.0.0: + dependencies: + abbrev: 1.1.1 + + normalize-path@3.0.0: {} + normalize-url@6.1.0: {} + npmlog@6.0.2: + dependencies: + are-we-there-yet: 3.0.1 + console-control-strings: 1.1.0 + gauge: 4.0.4 + set-blocking: 2.0.0 + object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -4065,6 +5883,10 @@ snapshots: dependencies: wrappy: 1.0.2 + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + option@0.2.4: {} optionator@0.9.4: @@ -4076,6 +5898,18 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + p-cancelable@2.1.1: {} p-limit@3.1.0: @@ -4086,6 +5920,10 @@ snapshots: dependencies: p-limit: 3.1.0 + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + package-json-from-dist@1.0.1: {} pako@1.0.11: {} @@ -4117,6 +5955,8 @@ snapshots: optionalDependencies: '@napi-rs/canvas': 1.0.3 + pe-library@0.4.1: {} + pend@1.2.0: {} picocolors@1.1.1: {} @@ -4125,6 +5965,12 @@ snapshots: pkce-challenge@5.0.1: {} + plist@3.1.1: + dependencies: + '@xmldom/xmldom': 0.9.10 + base64-js: 1.5.1 + xmlbuilder: 15.1.1 + postcss@8.5.25: dependencies: nanoid: 3.3.16 @@ -4139,6 +5985,13 @@ snapshots: progress@2.0.3: {} + promise-inflight@1.0.1: {} + + promise-retry@2.0.1: + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -4178,6 +6031,12 @@ snapshots: react@19.2.8: {} + read-binary-file-arch@1.0.6: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + readable-stream@2.3.8: dependencies: core-util-is: 1.0.3 @@ -4188,10 +6047,26 @@ snapshots: string_decoder: 1.1.1 util-deprecate: 1.0.2 + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.9 + readdirp@5.0.0: {} + require-directory@2.1.1: {} + require-from-string@2.0.2: {} + resedit@1.7.2: + dependencies: + pe-library: 0.4.1 + resolve-alpn@1.2.1: {} resolve-from@4.0.0: {} @@ -4200,6 +6075,17 @@ snapshots: dependencies: lowercase-keys: 2.0.0 + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + retry@0.12.0: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + roarr@2.15.4: dependencies: boolean: 3.2.0 @@ -4255,6 +6141,12 @@ snapshots: safer-buffer@2.1.2: {} + sanitize-filename@1.6.4: + dependencies: + truncate-utf8-bytes: 1.0.2 + + sax@1.6.1: {} + scheduler@0.27.0: {} semver-compare@1.0.0: @@ -4294,6 +6186,8 @@ snapshots: transitivePeerDependencies: - supports-color + set-blocking@2.0.0: {} + setimmediate@1.0.5: {} setprototypeof@1.2.0: {} @@ -4334,17 +6228,58 @@ snapshots: siginfo@2.0.0: {} + signal-exit@3.0.7: {} + signal-exit@4.1.0: {} + simple-update-notifier@2.0.0: + dependencies: + semver: 7.8.5 + + slice-ansi@3.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + optional: true + + smart-buffer@4.2.0: {} + + socks-proxy-agent@7.0.0: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + socks: 2.8.9 + transitivePeerDependencies: + - supports-color + + socks@2.8.9: + dependencies: + ip-address: 10.4.0 + smart-buffer: 4.2.0 + source-map-js@1.2.1: {} + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + sprintf-js@1.0.3: {} sprintf-js@1.1.3: optional: true + ssri@9.0.1: + dependencies: + minipass: 3.3.6 + stackback@0.0.2: {} + stat-mode@1.0.0: {} + statuses@2.0.2: {} std-env@3.10.0: {} @@ -4389,6 +6324,28 @@ snapshots: dependencies: has-flag: 4.0.0 + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + + temp-file@3.4.0: + dependencies: + async-exit-hook: 2.0.1 + fs-extra: 10.1.0 + test-exclude@7.0.2: dependencies: '@istanbuljs/schema': 0.1.6 @@ -4410,8 +6367,18 @@ snapshots: tinyspy@4.0.4: {} + tmp-promise@3.0.3: + dependencies: + tmp: 0.2.7 + + tmp@0.2.7: {} + toidentifier@1.0.1: {} + truncate-utf8-bytes@1.0.2: + dependencies: + utf8-byte-length: 1.0.5 + ts-api-utils@2.5.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -4448,8 +6415,18 @@ snapshots: undici-types@6.21.0: {} + unique-filename@2.0.1: + dependencies: + unique-slug: 3.0.0 + + unique-slug@3.0.0: + dependencies: + imurmurhash: 0.1.4 + universalify@0.1.2: {} + universalify@2.0.1: {} + unpipe@1.0.0: {} update-browserslist-db@1.2.3(browserslist@4.28.7): @@ -4462,10 +6439,19 @@ snapshots: dependencies: punycode: 2.3.1 + utf8-byte-length@1.0.5: {} + util-deprecate@1.0.2: {} vary@1.1.2: {} + verror@1.10.1: + dependencies: + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.4.1 + optional: true + vite-node@3.2.4(@types/node@22.20.1)(yaml@2.9.0): dependencies: cac: 6.7.14 @@ -4513,7 +6499,7 @@ snapshots: fsevents: 2.3.3 yaml: 2.9.0 - vitest@3.2.7(@types/node@22.20.1)(yaml@2.9.0): + vitest@3.2.7(@types/debug@4.1.13)(@types/node@22.20.1)(yaml@2.9.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.7 @@ -4539,6 +6525,7 @@ snapshots: vite-node: 3.2.4(@types/node@22.20.1)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: + '@types/debug': 4.1.13 '@types/node': 22.20.1 transitivePeerDependencies: - jiti @@ -4554,6 +6541,10 @@ snapshots: - tsx - yaml + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -4563,6 +6554,10 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + wide-align@1.1.5: + dependencies: + string-width: 4.2.3 + word-wrap@1.2.5: {} wrap-ansi@7.0.0: @@ -4581,10 +6576,28 @@ snapshots: xmlbuilder@10.1.1: {} + xmlbuilder@15.1.1: {} + + y18n@5.0.8: {} + yallist@3.1.1: {} + yallist@4.0.0: {} + yaml@2.9.0: {} + yargs-parser@21.1.1: {} + + yargs@17.7.3: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yauzl@2.10.0: dependencies: buffer-crc32: 0.2.13 @@ -4592,6 +6605,12 @@ snapshots: yocto-queue@0.1.0: {} + zip-stream@4.1.1: + dependencies: + archiver-utils: 3.0.4 + compress-commons: 4.1.2 + readable-stream: 3.6.2 + zod-to-json-schema@3.25.2(zod@4.4.3): dependencies: zod: 4.4.3 diff --git a/scripts/build-cli.mjs b/scripts/build-cli.mjs new file mode 100644 index 0000000..1e6ebcd --- /dev/null +++ b/scripts/build-cli.mjs @@ -0,0 +1,42 @@ +/** + * Bundle the CLI to a single file (plan 9.14, first half). + * + * node scripts/build-cli.mjs + * + * **Not a preference.** An unbundled CLI pays Node's module resolution on + * every invocation, and a `PreToolUse` hook fires it on *every page write* — + * so the cost is not paid once when a person types `ow check`, it is paid + * every time an agent touches a page. `adr:0014-typescript-everywhere-except-audio-capture` + * names this as the reason esbuild is in the stack at all. + * + * The result is what npm publishes: `bin.ow` points at `build/ow.mjs`, and + * `files` carries nothing else — so `npx open-wiki init` downloads one file + * plus its manifest. + */ +import { build } from "esbuild"; +import { fileURLToPath } from "node:url"; +import { dirname, join, resolve } from "node:path"; + +const here = dirname(fileURLToPath(import.meta.url)); +const cli = resolve(here, "..", "packages", "cli"); + +await build({ + entryPoints: [join(cli, "src", "cli.ts")], + outfile: join(cli, "build", "ow.mjs"), + bundle: true, + platform: "node", + target: "node22", + format: "esm", + // The shebang `cli.ts` carries is a comment to esbuild; put it back. + banner: { js: "#!/usr/bin/env node" }, + // Bundled, including the workspace packages — that is the point. What stays + // external is what cannot be bundled: the native and lazily-loaded parts of + // the source adapters, which the CLI reaches only through a dynamic import + // and which npm installs alongside. + external: ["pdfjs-dist", "mammoth", "chokidar", "electron"], + minify: false, + sourcemap: false, + logLevel: "info", +}); + +console.log("build-cli: packages/cli/build/ow.mjs"); diff --git a/scripts/build-main.mjs b/scripts/build-main.mjs new file mode 100644 index 0000000..b91f737 --- /dev/null +++ b/scripts/build-main.mjs @@ -0,0 +1,44 @@ +/** + * Bundle the Electron main process and its preload (plan 10.1). + * + * node scripts/build-main.mjs + * + * The renderer is Vite's job; this is the other half. Electron's `main` points + * at `build/main/index.js`, and the preload at `build/main/preload.js` — a + * packaged application has no TypeScript and no `node_modules` for the + * workspace packages, so both have to be bundles rather than entry points. + * + * `electron` itself stays external: it is provided by the runtime, and + * bundling it produces a file that cannot start. + */ +import { build } from "esbuild"; +import { fileURLToPath } from "node:url"; +import { dirname, join, resolve } from "node:path"; + +const here = dirname(fileURLToPath(import.meta.url)); +const desktop = resolve(here, "..", "apps", "desktop"); + +const shared = { + bundle: true, + platform: "node", + target: "node22", + format: "cjs", + // Provided by Electron, and by the OS in the case of the optional native + // pieces the source adapters load lazily. + external: ["electron", "pdfjs-dist", "mammoth", "chokidar", "fsevents"], + logLevel: "info", +}; + +await build({ + ...shared, + entryPoints: [join(desktop, "src", "main", "index.ts")], + outfile: join(desktop, "build", "main", "index.js"), +}); + +await build({ + ...shared, + entryPoints: [join(desktop, "src", "main", "preload.ts")], + outfile: join(desktop, "build", "main", "preload.js"), +}); + +console.log("build-main: apps/desktop/build/main/{index,preload}.js"); diff --git a/scripts/ci/check-plugin.mjs b/scripts/ci/check-plugin.mjs new file mode 100644 index 0000000..312c422 --- /dev/null +++ b/scripts/ci/check-plugin.mjs @@ -0,0 +1,92 @@ +/** + * The plugin's own shape (plan 10.6). + * + * node scripts/ci/check-plugin.mjs + * + * `claude plugin validate --strict` is the authority and CI runs it — but it + * needs the CLI installed, and a check that silently passes when its tool is + * missing is not a check. This is the floor underneath it: the two manifests + * parse, they agree on the plugin's name and version, the source path the + * marketplace names actually exists, and the two things `adr:0015` says must + * *not* be in here are not in here. + */ +import { existsSync, readFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), "..", ".."); + +export function checkPlugin(repoRoot = root, read = defaultRead, exists = existsSync) { + const problems = []; + + const marketplace = read(join(repoRoot, ".claude-plugin", "marketplace.json")); + if (!marketplace) + return { + ok: false, + problems: [".claude-plugin/marketplace.json is missing or will not parse"], + }; + if (!Array.isArray(marketplace.plugins) || marketplace.plugins.length === 0) { + problems.push("the marketplace lists no plugins"); + return { ok: false, problems }; + } + + for (const entry of marketplace.plugins) { + const source = String(entry.source ?? ""); + if (!source.startsWith("./")) { + problems.push(`plugin "${entry.name}" has a source that is not a path in this repository`); + continue; + } + const dir = join(repoRoot, source); + if (!exists(dir)) { + problems.push(`plugin "${entry.name}" names ${source}, which is not there`); + continue; + } + const manifest = read(join(dir, ".claude-plugin", "plugin.json")); + if (!manifest) { + problems.push(`${source}/.claude-plugin/plugin.json is missing or will not parse`); + continue; + } + if (manifest.name !== entry.name) { + problems.push( + `${source} calls itself "${manifest.name}" and the marketplace calls it "${entry.name}"`, + ); + } + if (manifest.version !== entry.version) { + problems.push( + `${source} is version ${manifest.version} and the marketplace says ${entry.version}`, + ); + } + // `adr:0015-the-convention-ships-as-skills`: the skills have one home, and + // it is `.claude/skills/` inside each project. A copy here would be a + // second one, and two copies of a convention drift. + if (exists(join(dir, "skills"))) { + problems.push( + `${source} ships skills — the convention has one home (adr:0015), and it is the project`, + ); + } + // A `.mcp.json`'s contents differ per user: it names *other* projects, by + // name, on that person's machine. + if (exists(join(dir, ".mcp.json"))) { + problems.push(`${source} ships a .mcp.json — its contents differ per user`); + } + } + + return problems.length > 0 ? { ok: false, problems } : { ok: true }; +} + +function defaultRead(path) { + try { + return JSON.parse(readFileSync(path, "utf8")); + } catch { + return null; + } +} + +if (process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\/g, "/"))) { + const result = checkPlugin(); + if (!result.ok) { + for (const problem of result.problems) console.error(`plugin: ${problem}`); + process.exit(1); + } + console.log("plugin: ok"); +} diff --git a/scripts/ci/package-manifests.mjs b/scripts/ci/package-manifests.mjs new file mode 100644 index 0000000..478acdf --- /dev/null +++ b/scripts/ci/package-manifests.mjs @@ -0,0 +1,156 @@ +/** + * The winget and Scoop manifests (plan 10.4). + * + * node scripts/ci/package-manifests.mjs v0.1.0 [outDir] + * + * Both point at the GitHub release URL and quote its hash, which is the whole + * requirement: a package manager that downloads without verifying is the + * fetch-and-execute this product refuses to ship — the same argument + * `scripts/fetch-ffmpeg.mjs` makes about its own download, applied to the + * installer. + * + * Generated rather than hand-maintained because the hash changes every + * release, and a manifest with last release's hash fails as "the download is + * corrupt" rather than "somebody forgot to update a file". + */ +import { mkdirSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { versionOfTag } from "./release-version.mjs"; + +export const OWNER = "protonspy"; +export const REPO = "open-wiki"; +export const PUBLISHER = "protonspy"; +export const PACKAGE_ID = "protonspy.open-wiki"; + +/** Where a release's installer lives. `adr:0009` — GitHub Releases, no host of ours. */ +export function installerUrl(version) { + return `https://github.com/${OWNER}/${REPO}/releases/download/v${version}/open-wiki-Setup-${version}.exe`; +} + +/** + * winget wants three files under `manifests/////`. + * Returned as a map of relative path to content so a test can read them + * without a filesystem. + */ +export function wingetManifests(version, sha256) { + const dir = `manifests/${PUBLISHER[0].toLowerCase()}/${PUBLISHER}/open-wiki/${version}`; + const header = (type) => + [ + `PackageIdentifier: ${PACKAGE_ID}`, + `PackageVersion: ${version}`, + `ManifestType: ${type}`, + "ManifestVersion: 1.6.0", + ].join("\n"); + + return { + [`${dir}/${PACKAGE_ID}.yaml`]: [ + `PackageIdentifier: ${PACKAGE_ID}`, + `PackageVersion: ${version}`, + "DefaultLocale: en-US", + "ManifestType: version", + "ManifestVersion: 1.6.0", + "", + ].join("\n"), + + [`${dir}/${PACKAGE_ID}.locale.en-US.yaml`]: [ + `PackageIdentifier: ${PACKAGE_ID}`, + `PackageVersion: ${version}`, + "PackageLocale: en-US", + `Publisher: ${PUBLISHER}`, + `PublisherUrl: https://github.com/${OWNER}`, + "PackageName: open-wiki", + `PackageUrl: https://github.com/${OWNER}/${REPO}`, + "License: Apache-2.0", + `LicenseUrl: https://github.com/${OWNER}/${REPO}/blob/main/LICENSE`, + "ShortDescription: A project's documentation as a wiki the AI agent already has open.", + "Description: >-", + " Takes in sources — a file or a recording — reduces them to text with", + " provenance anchors, and stores a validated markdown wiki inside the", + " project directory. The application calls no LLM.", + "Tags:", + " - documentation", + " - wiki", + " - transcription", + "ManifestType: defaultLocale", + "ManifestVersion: 1.6.0", + "", + ].join("\n"), + + [`${dir}/${PACKAGE_ID}.installer.yaml`]: [ + header("installer").split("\nManifestType")[0], + "InstallerType: nullsoft", + "Scope: user", + "InstallModes:", + " - interactive", + " - silent", + "Installers:", + " - Architecture: x64", + ` InstallerUrl: ${installerUrl(version)}`, + ` InstallerSha256: ${sha256.toUpperCase()}`, + "ManifestType: installer", + "ManifestVersion: 1.6.0", + "", + ].join("\n"), + }; +} + +/** Scoop is one JSON manifest, and `autoupdate` is what keeps it current. */ +export function scoopManifest(version, sha256) { + return `${JSON.stringify( + { + version, + description: "A project's documentation as a wiki the AI agent already has open.", + homepage: `https://github.com/${OWNER}/${REPO}`, + license: "Apache-2.0", + architecture: { + "64bit": { + url: installerUrl(version), + hash: sha256.toLowerCase(), + }, + }, + innosetup: false, + // The NSIS installer puts `ow` on PATH itself; Scoop shims the exe. + bin: ["ow.cmd"], + checkver: { + github: `https://github.com/${OWNER}/${REPO}`, + }, + autoupdate: { + architecture: { + "64bit": { + url: installerUrl("$version"), + }, + }, + hash: { + url: `https://github.com/${OWNER}/${REPO}/releases/download/v$version/SHA256SUMS.txt`, + }, + }, + }, + null, + 2, + )}\n`; +} + +export function writeManifests(version, sha256, outDir) { + const files = { + ...wingetManifests(version, sha256), + "scoop/open-wiki.json": scoopManifest(version, sha256), + }; + for (const [rel, content] of Object.entries(files)) { + const target = join(outDir, rel); + mkdirSync(join(target, ".."), { recursive: true }); + writeFileSync(target, content, "utf8"); + } + return Object.keys(files); +} + +if (process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\/g, "/"))) { + const version = versionOfTag(process.argv[2] ?? ""); + const sha256 = process.argv[3] ?? ""; + if (!version || !/^[0-9a-f]{64}$/i.test(sha256)) { + console.error("usage: package-manifests.mjs v1.2.3 [outDir]"); + process.exit(1); + } + for (const file of writeManifests(version, sha256, process.argv[4] ?? "dist/manifests")) { + console.log(file); + } +} diff --git a/scripts/ci/release-version.mjs b/scripts/ci/release-version.mjs new file mode 100644 index 0000000..882d54d --- /dev/null +++ b/scripts/ci/release-version.mjs @@ -0,0 +1,84 @@ +/** + * The version every artifact of a release has to agree on (plan 10.2, 10.3). + * + * node scripts/ci/release-version.mjs v0.1.0 + * + * Two artifacts ship from one tag — the installer and the npm package — and + * `adr:0014-typescript-everywhere-except-audio-capture` names the cost it + * accepted: "a skew between them fails looking like corrupted state rather + * than a bad install". A user with the desktop application from `v0.2.0` and + * `npx open-wiki` resolving to `0.1.0` gets a CLI that writes an older + * convention into their project, and nothing on either side says so. + * + * So the tag, the desktop app and the CLI are checked to be the same string + * *before* anything is published, and the check is a module rather than a + * shell snippet so it can be tested. + */ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +/** The packages whose `version` a release publishes. */ +export const RELEASED = [ + { name: "the desktop application", path: "apps/desktop/package.json" }, + { name: "the CLI", path: "packages/cli/package.json" }, +]; + +/** `v0.1.0` → `0.1.0`. A tag that is not a version is not a release tag. */ +export function versionOfTag(tag) { + const match = /^v(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/.exec(tag ?? ""); + return match ? match[1] : null; +} + +/** True for a tag carrying a suffix — `v0.1.0-beta.1` is not a stable release. */ +export function isPrerelease(tag) { + return (versionOfTag(tag) ?? "").includes("-"); +} + +/** + * Check that everything agrees, and say precisely what does not. + * + * Returns `{ ok: true, version }` or `{ ok: false, problems }`. It never + * throws for a disagreement: a release that must not happen is an answer, and + * the caller decides the exit code. + */ +export function checkRelease(tag, repoRoot = ".", read = defaultRead) { + const version = versionOfTag(tag); + if (!version) { + return { + ok: false, + problems: [`"${tag}" is not a release tag — it has to look like v1.2.3`], + }; + } + const problems = []; + for (const pkg of RELEASED) { + let declared; + try { + declared = read(join(repoRoot, pkg.path)).version; + } catch { + problems.push(`${pkg.name} (${pkg.path}) could not be read`); + continue; + } + if (declared !== version) { + problems.push(`${pkg.name} says ${String(declared)}, the tag says ${version}`); + } + } + return problems.length > 0 ? { ok: false, problems } : { ok: true, version }; +} + +function defaultRead(path) { + return JSON.parse(readFileSync(path, "utf8")); +} + +// Run as a script: exit 1 on disagreement, and print what to fix. +if (process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\/g, "/"))) { + const result = checkRelease(process.argv[2] ?? ""); + if (!result.ok) { + for (const problem of result.problems) console.error(`release: ${problem}`); + console.error( + "release: the installer and the npm package ship from one tag and must " + + "agree on the version — a skew fails looking like corrupted state.", + ); + process.exit(1); + } + console.log(result.version); +} From ec85f78d4f0817ffa53c761b1d7fbc056bdf1df7 Mon Sep 17 00:00:00 2001 From: prode Date: Sat, 1 Aug 2026 17:08:24 -0300 Subject: [PATCH 2/3] fix(dist): close the code and security review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reviews found the packaging did not work and the socket trusted whoever answered it. Both are fixed here, with what they cost written down. **The packaged application could not start, and would not have recorded.** The main bundle was CommonJS, so esbuild erased `import.meta.url` into an empty object — three call sites, `fileURLToPath(undefined)` at module load, and a *warning* rather than an error, so the packaging run stayed green. It is ESM now, the sandboxed preload is `.cjs` because a sandboxed preload cannot be an ES module, and both carry a `createRequire` shim: the ESM output's `require` stub throws, and `yaml` calls `require("process")` on the first page parsed. `resources.ts` states where ffmpeg and `recorder.exe` sit in the packaged app instead of counting directories up from a source file, which is three different depths flattened into one bundle. The preload took `CHANNELS` from `ipc.ts` and dragged the whole main graph in behind it — 273 KB down to 5.4 KB through `channels.ts`. **The published npm package could not be installed.** It declared two `workspace:*` dependencies that are never published, so every `npx open-wiki` would 404, and left three libraries external that npm installs for nobody. The bundle carries the graph; the manifest declares nothing. Running the built `ow.mjs` — which nothing had done — found two more: a duplicated shebang on line 2, and the `require` stub above. **The `ow` shim pointed at a file that is never packaged**, and `.mjs` has no Windows file association to be invoked through anyway. It runs the CLI bundle through `open-wiki.exe` with `ELECTRON_RUN_AS_NODE=1`, which is the only Node an installer promising no external dependency can count on. It also passed `--project "%CD%"`, overriding the one test that makes `ow` in a download folder open the launcher rather than treating that folder as a wiki. The shim now lives in its own `bin/` directory and only that goes on PATH, so nothing else in the installation can be shadowed by a file dropped beside it. `EnVar` is gone — a plugin that may not be in whichever NSIS electron-builder downloads fails at `makensis` on a machine that is not this one. **The socket trusted the endpoint name.** It is a hash of a directory path: obscure, not secret, and off Windows it sat in world-writable `/tmp` where no legitimate listener ever exists. A failed bind was swallowed, so a squatter won silently and `ow read` printed its bytes as trusted wiki content into an agent's context. Both directions are authenticated now — a random token in the application's data directory, 0600 in a 0700 directory, and an HMAC over the client's nonce coming back — both buffers are capped, connections are bounded, the listen error is reported, and the project key is normalised so two spellings of one directory reach one pipe. The read is confined to `wiki/` rather than to the project, matching `mcp/src/tools.ts`. **The release could publish half of itself.** npm ran before the GitHub release: a failure after it left a permanent version on the registry with no installer and no way to re-run the tag. Reversed, and the republish guard now checks both registries. `id-token: write` and `registry-url` were both missing, so provenance and authentication would each have failed; the tag is no longer interpolated into a `run:` body in the job holding the npm token and the signing certificate; a prerelease goes to the `next` dist-tag rather than becoming what `npx open-wiki` resolves to; and the manifests 10.4 generates are attached to the release instead of being zipped and dropped. **The plugin.** `continue-on-error` made `claude plugin validate --strict` report success whatever it said. Its hooks dropped `Bash` — the matcher whose whole purpose is shell writes — and ran `npx -y open-wiki` unpinned on every page write, which is the cost the CLI bundle exists to remove and a skew 10.3 exists to prevent. `HOOK_MATCHERS` is now one declaration the plugin is tested against, and `check-plugin.mjs` refuses an unpinned hook. README: winget and Scoop were offered as if published. Scoop is, from the manifest each release attaches; winget is not, and says so. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01D3VYWWTZtEE2NxPiksKsAK --- .github/workflows/ci.yml | 13 +- .github/workflows/release.yml | 107 ++++++++--- README.md | 15 +- apps/desktop/build-resources/installer.nsh | 63 +++++-- apps/desktop/package.json | 19 +- apps/desktop/src/main/channels.ts | 52 +++++ apps/desktop/src/main/index.ts | 12 +- apps/desktop/src/main/ipc.ts | 45 +---- apps/desktop/src/main/preload.ts | 5 +- apps/desktop/src/main/resources.ts | 39 ++++ apps/desktop/tests/resources.spec.ts | 55 ++++++ packages/access/src/config/app-dir.ts | 41 ++++ packages/access/src/config/secrets.ts | 31 +-- packages/access/src/socket.ts | 210 ++++++++++++++++++--- packages/access/tests/socket.spec.ts | 164 ++++++++++++++-- packages/cli/package.json | 8 +- packages/cli/src/cli.ts | 8 +- packages/cli/src/install.ts | 25 ++- packages/cli/tests/install.spec.ts | 16 +- packages/cli/tests/main.spec.ts | 34 ++++ packages/cli/tests/release.spec.ts | 116 ++++++++++-- plans/open-wiki.md | 22 +++ plugins/open-wiki/hooks/hooks.json | 6 +- pnpm-lock.yaml | 17 +- scripts/build-cli.mjs | 36 +++- scripts/build-main.mjs | 43 ++++- scripts/ci/check-plugin.mjs | 39 ++++ scripts/ci/package-manifests.mjs | 50 +++-- scripts/ci/release-version.mjs | 20 +- 29 files changed, 1068 insertions(+), 243 deletions(-) create mode 100644 apps/desktop/src/main/channels.ts create mode 100644 apps/desktop/src/main/resources.ts create mode 100644 apps/desktop/tests/resources.spec.ts create mode 100644 packages/access/src/config/app-dir.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7180537..ef6fb49 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -142,11 +142,18 @@ jobs: - name: The marketplace and the plugin manifest are well formed run: node scripts/ci/check-plugin.mjs + # A failed *install* is tolerated — a registry outage is not a finding + # about this repository. A failed *validation* is not: `continue-on-error` + # made the `|| exit 1` inert, so the step reported success whatever the + # tool said — which is exactly what check-plugin.mjs's own header calls + # "a check that silently passes when its tool is missing". - name: claude plugin validate --strict - continue-on-error: true run: | - npm i -g @anthropic-ai/claude-code >/dev/null 2>&1 || exit 0 - claude plugin validate --strict . || exit 1 + if ! npm i -g @anthropic-ai/claude-code >/dev/null 2>&1; then + echo "could not install the Claude Code CLI — skipped (check-plugin.mjs still ran)" + exit 0 + fi + claude plugin validate --strict . # The single check to require on the branch. Skipped jobs are fine — an empty # workspace has nothing to test — but a failed or cancelled one is not. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b59a200..2aec4ae 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,13 +10,23 @@ concurrency: group: release-${{ github.ref }} cancel-in-progress: false -permissions: - contents: write +permissions: {} jobs: release: name: Build and publish the installer runs-on: windows-latest + permissions: + # Least privilege, and granted per job rather than per workflow: the + # release needs to create one, and `id-token` is what npm provenance + # signs with. Nothing else in this repository should inherit either. + contents: write + id-token: write + env: + # Never interpolated into a `run:` body. A tag name may contain `"`, `$`, + # `;` and a backtick — `git check-ref-format` permits all four — and this + # job holds the npm token and the signing certificate. + TAG: ${{ github.ref_name }} steps: - uses: actions/checkout@v4 @@ -36,21 +46,32 @@ jobs: - name: Check the tag and both artifacts agree if: startsWith(github.ref, 'refs/tags/v') shell: pwsh - run: node scripts/ci/release-version.mjs "${{ github.ref_name }}" - - - name: Refuse to republish an existing release + run: node scripts/ci/release-version.mjs "$env:TAG" + + # Both registries, before anything is built. A GitHub release can be + # deleted; an npm version cannot, so finding out afterwards that one of + # the two is already taken leaves a half-published release with no way + # back except bumping the version — which is what the agreement check + # above exists to make impossible. + - name: Refuse to republish if: startsWith(github.ref, 'refs/tags/v') shell: pwsh env: GH_TOKEN: ${{ github.token }} run: | # A published release has been downloaded; deleting it does not undo that. - gh release view "${{ github.ref_name }}" 2>$null + gh release view "$env:TAG" 2>$null + if ($LASTEXITCODE -eq 0) { + Write-Error "release $env:TAG already exists — bump the version and tag again" + exit 1 + } + $version = $env:TAG.TrimStart("v") + npm view "open-wiki@$version" version 2>$null if ($LASTEXITCODE -eq 0) { - Write-Error "release ${{ github.ref_name }} already exists — bump the version and tag again" + Write-Error "open-wiki@$version is already on npm — a version there is permanent; bump and tag again" exit 1 } - Write-Host "no release for ${{ github.ref_name }} yet" + Write-Host "neither registry has $env:TAG yet" - uses: pnpm/action-setup@v4 @@ -58,6 +79,9 @@ jobs: with: node-version: 22 cache: pnpm + # Without this, `setup-node` writes no .npmrc and NODE_AUTH_TOKEN is + # read by nothing — the publish below would be unauthenticated. + registry-url: https://registry.npmjs.org - run: pnpm install --frozen-lockfile @@ -70,8 +94,18 @@ jobs: - name: Fetch and verify ffmpeg shell: pwsh env: + FFMPEG_URL: ${{ vars.FFMPEG_URL }} FFMPEG_SHA256: ${{ vars.FFMPEG_SHA256 }} - run: node scripts/fetch-ffmpeg.mjs + run: | + # The hash is a repository variable rather than a committed constant + # because the upstream URL is a moving one. An empty value here fails + # inside the fetch script as "no expected hash", which reads like a + # bug in the script rather than a setting nobody set. + if (-not $env:FFMPEG_SHA256) { + Write-Error "vars.FFMPEG_SHA256 is not set — the installer bundles ffmpeg and the download is verified against it. Set it (with vars.FFMPEG_URL) in the repository settings." + exit 1 + } + node scripts/fetch-ffmpeg.mjs - name: Build the installer shell: pwsh @@ -100,29 +134,35 @@ jobs: Out-File -FilePath "$dir/SHA256SUMS.txt" -Encoding utf8 Get-Content "$dir/SHA256SUMS.txt" - # 10.3 — the second artifact, from the same tag. Provenance is on, and - # it is not decoration: a product that verifies the SHA256 of its own - # ffmpeg cannot ship a `npx` entry point that verifies nothing. - - name: Publish the CLI to npm - if: startsWith(github.ref, 'refs/tags/v') - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - run: | - pnpm --filter open-wiki run build - pnpm --filter open-wiki publish --access public --no-git-checks - - # 10.4 — the manifests, generated from the tag and the hash that was just - # published. A manifest carrying last release's hash fails as "the - # download is corrupt" rather than "somebody forgot to update a file". + # 10.4 — the manifests, generated from the tag and the hash of the + # installer that is about to be released. A manifest carrying last + # release's hash fails as "the download is corrupt" rather than + # "somebody forgot to update a file". - name: Generate the winget and Scoop manifests if: startsWith(github.ref, 'refs/tags/v') shell: pwsh run: | - $line = (Get-Content "apps/desktop/release/SHA256SUMS.txt" | Select-Object -First 1) + # By name, not by position: the collect step allows more than one + # `.exe`, and taking the first line would silently publish some other + # file's hash. + $version = $env:TAG.TrimStart("v") + $wanted = "open-wiki-Setup-$version.exe" + $line = Get-Content "apps/desktop/release/SHA256SUMS.txt" | + Where-Object { $_ -match [regex]::Escape($wanted) + '$' } | + Select-Object -First 1 + if (-not $line) { + Write-Error "no checksum line for $wanted — the installer is not named what the manifests will point at" + exit 1 + } $sha = $line.Split(" ")[0] - node scripts/ci/package-manifests.mjs "${{ github.ref_name }}" $sha "dist/manifests" + node scripts/ci/package-manifests.mjs "$env:TAG" $sha "dist/manifests" + Copy-Item "dist/manifests/scoop/open-wiki.json" "apps/desktop/release/open-wiki.json" Compress-Archive -Path "dist/manifests/*" -DestinationPath "apps/desktop/release/manifests.zip" + # The reversible step first. A GitHub release can be deleted and made + # again; an npm version is permanent, so publishing to npm before the + # release existed meant any later failure left `open-wiki@X` on the + # registry with no installer to go with it and no way to re-run the tag. - name: Publish the release if: startsWith(github.ref, 'refs/tags/v') uses: softprops/action-gh-release@v2 @@ -130,11 +170,28 @@ jobs: files: | apps/desktop/release/*.exe apps/desktop/release/SHA256SUMS.txt + apps/desktop/release/open-wiki.json + apps/desktop/release/manifests.zip generate_release_notes: true # A tag carrying a suffix — v0.1.0-beta.1 — is not a stable release. prerelease: ${{ contains(github.ref_name, '-') }} fail_on_unmatched_files: true + # 10.3 — the second artifact, from the same tag. Provenance is on, and + # it is not decoration: a product that verifies the SHA256 of its own + # ffmpeg cannot ship a `npx` entry point that verifies nothing. + - name: Publish the CLI to npm + if: startsWith(github.ref, 'refs/tags/v') + shell: pwsh + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + # A prerelease must not become what `npx open-wiki` resolves to. + $pre = node scripts/ci/release-version.mjs --prerelease "$env:TAG" + $tag = if ($pre) { "next" } else { "latest" } + Write-Host "publishing to the '$tag' dist-tag" + pnpm --filter open-wiki publish --access public --no-git-checks --tag $tag + # workflow_dispatch builds without a tag: useful for checking the packaging # still works without publishing anything. - name: Upload the installer as a build artifact diff --git a/README.md b/README.md index a548109..3f790c4 100644 --- a/README.md +++ b/README.md @@ -22,13 +22,20 @@ Windows 10/11. Apache-2.0. ## Install +The installer from [Releases](https://github.com/protonspy/open-wiki/releases). +Every release publishes a `SHA256SUMS.txt` beside it — check it. + +Or through Scoop, from the manifest that release attaches: + ```powershell -winget install protonspy.open-wiki +scoop install https://github.com/protonspy/open-wiki/releases/latest/download/open-wiki.json ``` -or `scoop install open-wiki`, or the installer from -[Releases](https://github.com/protonspy/open-wiki/releases). Every release -publishes a `SHA256SUMS.txt` beside the installer — check it. +A winget manifest is generated and attached to every release as well +(`manifests.zip`), quoting the same hash. **It is not in the winget community +repository yet** — submitting it is a pull request to `microsoft/winget-pkgs` +that nothing here opens for you, so `winget install protonspy.open-wiki` does +not work today. For the CLI alone, with nothing installed: diff --git a/apps/desktop/build-resources/installer.nsh b/apps/desktop/build-resources/installer.nsh index cb263af..b73b87b 100644 --- a/apps/desktop/build-resources/installer.nsh +++ b/apps/desktop/build-resources/installer.nsh @@ -9,30 +9,61 @@ ; The shim is a `.cmd` rather than a copy of the exe because it has to do two ; different things: with no arguments it opens the application scoped to the ; current directory, and with a subcommand it runs headless (9.2). +; +; **The headless branch runs the CLI bundle through Electron, not through +; node.** `ELECTRON_RUN_AS_NODE=1` makes `open-wiki.exe` a plain Node process, +; which is the only Node the installer can count on: 10.1 is "no external +; dependency to install", and Windows ships neither node nor a `.mjs` file +; association for one to be invoked through. +; +; **The no-argument branch passes no `--project`.** `start` hands the new +; process the shell's working directory, and the application already decides +; from that — counting it "only if it already looks like a project", which is +; what makes `ow` in a download folder open the launcher of 8.4 rather than +; treating that folder as a wiki. Passing `--project "%CD%"` overrode exactly +; that test, and quoting `%CD%` at a drive root broke besides. +; +; **Only headers NSIS itself ships are used.** An installer that depends on a +; plugin being present in whichever NSIS electron-builder downloads fails at +; `makensis` on a machine that is not this one, and the packaging run is the +; only place that would ever find out. + +!include "WordFunc.nsh" +!include "WinMessages.nsh" !macro customInstall - ; A one-line launcher beside the application. - FileOpen $0 "$INSTDIR\ow.cmd" w + ; The shim goes in its own directory, and only the shim goes there. That + ; directory is what lands on PATH, so nothing else in the installation can be + ; shadowed by a file dropped beside it — $INSTDIR is under %LOCALAPPDATA%, + ; is writable by any process running as this user, and the user chose it. + CreateDirectory "$INSTDIR\bin" + FileOpen $0 "$INSTDIR\bin\ow.cmd" w FileWrite $0 "@echo off$\r$\n" FileWrite $0 "rem open-wiki launcher. Generated by the installer; do not edit.$\r$\n" - FileWrite $0 'if "%~1"=="" ($\r$\n' - FileWrite $0 ' start "" "%~dp0open-wiki.exe" --project "%CD%"$\r$\n' - FileWrite $0 ") else ($\r$\n" - FileWrite $0 ' "%~dp0resources\app.asar.unpacked\ow.mjs" %*$\r$\n' - FileWrite $0 ")$\r$\n" + FileWrite $0 "setlocal$\r$\n" + FileWrite $0 'if not "%~1"=="" goto :headless$\r$\n' + FileWrite $0 'start "" "%~dp0..\open-wiki.exe"$\r$\n' + FileWrite $0 "goto :eof$\r$\n" + FileWrite $0 ":headless$\r$\n" + FileWrite $0 'set "ELECTRON_RUN_AS_NODE=1"$\r$\n' + FileWrite $0 '"%~dp0..\open-wiki.exe" "%~dp0..\resources\ow.mjs" %*$\r$\n' FileClose $0 ; Per-user PATH, because the installer is per-user (`nsis.perMachine: false`). - ; `EnVar` writes the registry value and broadcasts the change, so a shell - ; opened after the install sees it without a reboot. - EnVar::SetHKCU - EnVar::AddValue "PATH" "$INSTDIR" - Pop $0 + ; `WordAdd` with `+` appends only when it is not already there, so reinstalling + ; does not grow the variable; the broadcast is what lets a shell opened after + ; the install see it without a reboot. + ReadRegStr $0 HKCU "Environment" "PATH" + ${WordAdd} "$0" ";" "+$INSTDIR\bin" $1 + WriteRegExpandStr HKCU "Environment" "PATH" "$1" + SendMessage ${HWND_BROADCAST} ${WM_SETTINGCHANGE} 0 "STR:Environment" /TIMEOUT=5000 !macroend !macro customUnInstall - EnVar::SetHKCU - EnVar::DeleteValue "PATH" "$INSTDIR" - Pop $0 - Delete "$INSTDIR\ow.cmd" + ReadRegStr $0 HKCU "Environment" "PATH" + ${WordAdd} "$0" ";" "-$INSTDIR\bin" $1 + WriteRegExpandStr HKCU "Environment" "PATH" "$1" + SendMessage ${HWND_BROADCAST} ${WM_SETTINGCHANGE} 0 "STR:Environment" /TIMEOUT=5000 + Delete "$INSTDIR\bin\ow.cmd" + RMDir "$INSTDIR\bin" !macroend diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 498dd98..2e9efce 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -12,25 +12,24 @@ "typecheck": "tsc --noEmit -p tsconfig.json", "lint": "eslint .", "build:main": "node ../../scripts/build-main.mjs", - "build": "pnpm run build:main && pnpm run build:renderer", + "build:cli": "pnpm --filter open-wiki run build", + "build": "pnpm run build:main && pnpm run build:renderer && pnpm run build:cli", "package": "pnpm run build && electron-builder --win --publish never" }, - "dependencies": { + "devDependencies": { "@open-wiki/access": "workspace:*", "@open-wiki/audio": "workspace:*", - "chokidar": "^5.0.0", - "markdown-it": "^14.1.0" - }, - "devDependencies": { "@types/markdown-it": "^14.1.2", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.3.4", + "chokidar": "^5.0.0", "electron": "^38.0.0", + "electron-builder": "^25.1.8", + "markdown-it": "^14.1.0", "react": "^19.0.0", "react-dom": "^19.0.0", - "vite": "^6.0.0", - "electron-builder": "^25.1.8" + "vite": "^6.0.0" }, "build": { "appId": "dev.protonspy.openwiki", @@ -52,6 +51,10 @@ { "from": "../../target/release/recorder.exe", "to": "recorder.exe" + }, + { + "from": "../../packages/cli/build/ow.mjs", + "to": "ow.mjs" } ], "win": { diff --git a/apps/desktop/src/main/channels.ts b/apps/desktop/src/main/channels.ts new file mode 100644 index 0000000..56dd940 --- /dev/null +++ b/apps/desktop/src/main/channels.ts @@ -0,0 +1,52 @@ +/** + * The IPC channel names, and nothing else (plan 8.2). + * + * Its own module because **the preload imports it**. A sandboxed preload that + * reached this through `ipc.ts` pulled the whole main-process graph in behind + * it — the store, the audio package, ffmpeg's resolver — which made the + * preload bundle 273 KB of code that cannot run in a preload and produced an + * `import.meta` warning from the bundler that would hide a real one. + */ +export const CHANNELS = { + project: "project:info", + index: "wiki:index", + page: "wiki:page", + sources: "sources:list", + recordStart: "record:start", + recordPause: "record:pause", + recordResume: "record:resume", + recordStop: "record:stop", + recordStatus: "record:status", + + // Editing (8.7, 8.8, 8.9) and the history behind it (8.11). + save: "wiki:save", + create: "wiki:create", + rename: "wiki:rename", + remove: "wiki:delete", + history: "history:list", + undo: "history:undo", + + // Sources (6.2 to 6.7), the checks (7.6), and what a citation opens (8.6). + sourceDetail: "sources:detail", + sourcesOfPage: "sources:of-page", + retitle: "sources:retitle", + findings: "check:findings", + locate: "sources:locate", + drop: "sources:drop", + + // The credential (8.3), the launcher (8.4), the content language (8.12) and + // the run 6.3 starts. + credential: "settings:credential", + saveCredential: "settings:save-credential", + language: "settings:language", + setLanguage: "settings:set-language", + knownProjects: "launcher:projects", + createProject: "launcher:create", + forgetProject: "launcher:forget", + transcribe: "sources:transcribe", + + /** Main → renderer, for 8.10. */ + changed: "project:changed", +} as const; + +export type Channel = (typeof CHANNELS)[keyof typeof CHANNELS]; diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 76a1ea5..71bc6d4 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -4,6 +4,7 @@ import { fileURLToPath, pathToFileURL } from "node:url"; import { CHANNELS, createApi, dispatch } from "./ipc.js"; import { resolveProject } from "./project.js"; import { RecorderSession, resolveRecorder, spawnTransport } from "./recorder.js"; +import { applyPackagedBinaries } from "./resources.js"; import { serveQueries } from "@open-wiki/access/socket"; import { isOpenableExternally } from "../renderer/navigation.js"; import { watchProject } from "./watcher.js"; @@ -28,7 +29,11 @@ function createWindow(projectRoot: string | null): BrowserWindow { minHeight: 480, backgroundColor: "#101216", webPreferences: { - preload: join(here, "..", "preload.js"), + // `.cjs`, and that is not a detail: a sandboxed preload cannot be an ES + // module, and this package is `"type": "module"`, so a `.js` bundle here + // would be parsed as ESM and fail to load — leaving a window with no + // `window.ow` at all. + preload: join(here, "..", "preload.cjs"), // The three that matter. This window renders markdown an agent wrote, // and a renderer with Node in it is one prompt injection away from being // the agent's hands. @@ -126,6 +131,11 @@ function createWindow(projectRoot: string | null): BrowserWindow { } void app.whenReady().then(() => { + // 10.1 — ffmpeg and `recorder.exe` ship beside the asar, and the resolvers + // that look for them count directories up from their own source file. The + // bundle collapses those depths, so the packaged location is stated here. + if (app.isPackaged) applyPackagedBinaries(process.resourcesPath); + // 8.4 — `ow` outside a project opens the launcher rather than guessing at // one. A window with no project answers `null` to `project()`, and the // renderer shows the list of known projects instead of a wiki. diff --git a/apps/desktop/src/main/ipc.ts b/apps/desktop/src/main/ipc.ts index 91a8874..ff4b8ae 100644 --- a/apps/desktop/src/main/ipc.ts +++ b/apps/desktop/src/main/ipc.ts @@ -53,49 +53,10 @@ import { * dozen lines that bolt them to a window. Nothing here imports `electron`. */ -export const CHANNELS = { - project: "project:info", - index: "wiki:index", - page: "wiki:page", - sources: "sources:list", - recordStart: "record:start", - recordPause: "record:pause", - recordResume: "record:resume", - recordStop: "record:stop", - recordStatus: "record:status", +import { CHANNELS } from "./channels.js"; - // Editing (8.7, 8.8, 8.9) and the history behind it (8.11). - save: "wiki:save", - create: "wiki:create", - rename: "wiki:rename", - remove: "wiki:delete", - history: "history:list", - undo: "history:undo", - - // Sources (6.2 to 6.7), the checks (7.6), and what a citation opens (8.6). - sourceDetail: "sources:detail", - sourcesOfPage: "sources:of-page", - retitle: "sources:retitle", - findings: "check:findings", - locate: "sources:locate", - drop: "sources:drop", - - // The credential (8.3), the launcher (8.4), the content language (8.12) and - // the run 6.3 starts. - credential: "settings:credential", - saveCredential: "settings:save-credential", - language: "settings:language", - setLanguage: "settings:set-language", - knownProjects: "launcher:projects", - createProject: "launcher:create", - forgetProject: "launcher:forget", - transcribe: "sources:transcribe", - - /** Main → renderer, for 8.10. */ - changed: "project:changed", -} as const; - -export type Channel = (typeof CHANNELS)[keyof typeof CHANNELS]; +export { CHANNELS }; +export type { Channel } from "./channels.js"; /** * How a window gets at its recorder. diff --git a/apps/desktop/src/main/preload.ts b/apps/desktop/src/main/preload.ts index 092f18f..daf7e90 100644 --- a/apps/desktop/src/main/preload.ts +++ b/apps/desktop/src/main/preload.ts @@ -1,5 +1,8 @@ import { contextBridge, ipcRenderer, webUtils } from "electron"; -import { CHANNELS } from "./ipc.js"; +// From `channels.js`, never from `ipc.js`. A preload that imported the latter +// dragged the whole main-process graph — the store, the audio package — into a +// sandboxed bundle that cannot run any of it. +import { CHANNELS } from "./channels.js"; import type { OwBridge } from "../renderer/bridge.js"; /** diff --git a/apps/desktop/src/main/resources.ts b/apps/desktop/src/main/resources.ts new file mode 100644 index 0000000..3f3656a --- /dev/null +++ b/apps/desktop/src/main/resources.ts @@ -0,0 +1,39 @@ +import { join } from "node:path"; + +/** + * Where the bundled binaries are once the application is packaged (plan 10.1). + * + * `resolveFfmpeg` and `resolveRecorder` find their binary by counting + * directories up from the module that defines them — four levels for one, five + * for the other, each correct for where its source file sits in the checkout. + * **Bundling collapses those three depths into one file**, so both counts are + * wrong in the packaged application, and wrong quietly: recording fails on + * every installed copy while every test passes. + * + * So the packaged location is stated rather than derived. `extraResources` + * puts both beside the asar, which is `process.resourcesPath`, and both + * resolvers already honour an explicit override — which is also what lets a + * developer point at a build they already have. + */ +export const BINARY_VARIABLES = ["OPEN_WIKI_FFMPEG", "OPEN_WIKI_RECORDER"] as const; + +export function packagedBinaries(resourcesPath: string): Record { + return { + OPEN_WIKI_FFMPEG: join(resourcesPath, "ffmpeg.exe"), + OPEN_WIKI_RECORDER: join(resourcesPath, "recorder.exe"), + }; +} + +/** + * Point the resolvers at the packaged binaries, leaving an existing override + * alone — a developer running the packaged build against their own ffmpeg + * meant that, and this is not the place to overrule it. + */ +export function applyPackagedBinaries( + resourcesPath: string, + env: Record = process.env, +): void { + for (const [variable, file] of Object.entries(packagedBinaries(resourcesPath))) { + env[variable] ??= file; + } +} diff --git a/apps/desktop/tests/resources.spec.ts b/apps/desktop/tests/resources.spec.ts new file mode 100644 index 0000000..70f5ce3 --- /dev/null +++ b/apps/desktop/tests/resources.spec.ts @@ -0,0 +1,55 @@ +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { + applyPackagedBinaries, + BINARY_VARIABLES, + packagedBinaries, +} from "../src/main/resources.js"; + +/** + * Plan 10.1 — the packaged application has to find the two binaries the + * installer bundles. `resolveFfmpeg` and `resolveRecorder` count directories up + * from their own source file, four levels for one and five for the other, and + * the bundle collapses those depths into one file: both counts are wrong once + * packaged, and wrong silently, because every test passes and only an installed + * copy fails to record. + */ +describe("packagedBinaries (10.1)", () => { + it("names both binaries beside the asar, where extraResources puts them", () => { + const resources = join( + "C:", + "Users", + "x", + "AppData", + "Local", + "Programs", + "open-wiki", + "resources", + ); + expect(packagedBinaries(resources)).toEqual({ + OPEN_WIKI_FFMPEG: join(resources, "ffmpeg.exe"), + OPEN_WIKI_RECORDER: join(resources, "recorder.exe"), + }); + }); + + it("covers every variable the resolvers read", () => { + expect(Object.keys(packagedBinaries("r")).sort()).toEqual([...BINARY_VARIABLES].sort()); + }); +}); + +describe("applyPackagedBinaries (10.1)", () => { + it("points the resolvers at the packaged binaries", () => { + const env: Record = {}; + applyPackagedBinaries(join("app", "resources"), env); + expect(env["OPEN_WIKI_RECORDER"]).toBe(join("app", "resources", "recorder.exe")); + }); + + it("leaves an override alone", () => { + // A developer running the packaged build against a build of ffmpeg they + // already have meant that, and this is not the place to overrule it. + const env: Record = { OPEN_WIKI_FFMPEG: "D:\\ffmpeg.exe" }; + applyPackagedBinaries(join("app", "resources"), env); + expect(env["OPEN_WIKI_FFMPEG"]).toBe("D:\\ffmpeg.exe"); + expect(env["OPEN_WIKI_RECORDER"]).toBe(join("app", "resources", "recorder.exe")); + }); +}); diff --git a/packages/access/src/config/app-dir.ts b/packages/access/src/config/app-dir.ts new file mode 100644 index 0000000..806b251 --- /dev/null +++ b/packages/access/src/config/app-dir.ts @@ -0,0 +1,41 @@ +import { join } from "node:path"; + +/** + * Where the application keeps what must not live in the project directory. + * + * This is split out of `secrets.ts` deliberately. That module carries the + * transcription credential and says, at its top, that the CLI/hook/MCP + * entrypoints must not import it — their stderr is consumed by an agent and + * travels to a model provider. But *the directory* is not the secret, and the + * socket (9.14) needs it from the CLI side to find the token the running + * application left there. + * + * So the location is here, and the credential stays there. Importing this from + * the CLI reaches a path and nothing else. + */ + +export class NoAppDataDirError extends Error { + constructor() { + super( + "no application data directory: neither APPDATA nor HOME is set. The " + + "credential is written nowhere else — falling back to the working " + + "directory would put it inside the project, which is usually a git repository.", + ); + this.name = "NoAppDataDirError"; + } +} + +/** + * The application data directory. Overridable for tests. + * + * **It never falls back to the working directory.** That fallback existed, + * and it was the exact leak `secrets.ts` is written to prevent: the desktop + * process runs with the project as its cwd, and the managed gitignore covers + * `.state/` and the audio — not an `open-wiki/` directory appearing beside + * them. + */ +export function defaultAppDataDir(): string { + const base = process.env["APPDATA"] ?? process.env["HOME"]; + if (!base) throw new NoAppDataDirError(); + return join(base, "open-wiki"); +} diff --git a/packages/access/src/config/secrets.ts b/packages/access/src/config/secrets.ts index 00d2872..6e1f2ee 100644 --- a/packages/access/src/config/secrets.ts +++ b/packages/access/src/config/secrets.ts @@ -1,6 +1,7 @@ import { createHash } from "node:crypto"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; +import { defaultAppDataDir, NoAppDataDirError } from "./app-dir.js"; /** * The transcription credential is the application's **only** secret @@ -22,32 +23,10 @@ export interface ProjectSecrets { stt: SttSecret; } -/** The application data directory. Overridable for tests. */ -export class NoAppDataDirError extends Error { - constructor() { - super( - "no application data directory: neither APPDATA nor HOME is set. The " + - "credential is written nowhere else — falling back to the working " + - "directory would put it inside the project, which is usually a git repository.", - ); - this.name = "NoAppDataDirError"; - } -} - -/** - * The application data directory. Overridable for tests. - * - * **It never falls back to the working directory.** That fallback existed, - * and it was the exact leak this module is written to prevent: the desktop - * process runs with the project as its cwd, and the managed gitignore covers - * `.state/` and the audio — not an `open-wiki/` directory appearing beside - * them. - */ -export function defaultAppDataDir(): string { - const base = process.env["APPDATA"] ?? process.env["HOME"]; - if (!base) throw new NoAppDataDirError(); - return join(base, "open-wiki"); -} +// The directory itself moved to `app-dir.ts` so the socket can find it from +// the CLI without importing this module. Re-exported because everything that +// reads the credential already imports them from here. +export { defaultAppDataDir, NoAppDataDirError }; function hashPath(projectRoot: string): string { return createHash("sha256").update(projectRoot).digest("hex"); diff --git a/packages/access/src/socket.ts b/packages/access/src/socket.ts index 4ee4ffe..be1db6f 100644 --- a/packages/access/src/socket.ts +++ b/packages/access/src/socket.ts @@ -1,8 +1,9 @@ -import { createHash } from "node:crypto"; +import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto"; import { createConnection, createServer, type Server } from "node:net"; -import { existsSync, readFileSync, unlinkSync } from "node:fs"; -import { join } from "node:path"; +import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs"; +import { join, resolve } from "node:path"; import { checkProject } from "./check/checks.js"; +import { defaultAppDataDir } from "./config/app-dir.js"; import { assertWithin } from "./paths.js"; import { listPages } from "./store/index.js"; @@ -29,6 +30,18 @@ import { listPages } from "./store/index.js"; * needs the application, and `adr:0013-the-project-directory-is-the-unit` says * the opposite. * + * **Both directions are authenticated, because the endpoint name is not a + * secret.** It is a hash of a directory path — obscure, and a local process + * guesses or enumerates it. Whoever is listening could otherwise feed the CLI + * text that is printed as trusted wiki content into an agent's context, which + * is the whole prize. So the server writes a random token into the + * application's own data directory (0600, in a 0700 directory) and requires it + * on every request, and answers with an HMAC over the client's nonce so the + * client can tell the real application from something that squatted the name + * first. Both reduce the reachable set to "a process already running as this + * user with read access to their profile" — which is the boundary the project + * directory itself sits behind. + * * Reached as `@open-wiki/access/socket`, not from the barrel: this module * opens a listening socket, and the MCP process's read surface has no business * being able to. @@ -42,8 +55,61 @@ export interface SocketRequest { args: string[]; } +/** What actually goes over the wire — the request plus what proves the peer. */ +interface WireRequest extends SocketRequest { + token: string; + nonce: string; +} + export type SocketResponse = { ok: true; result: unknown } | { ok: false; error: string }; +/** A response carries the proof the client checks; `askRunningApp` strips it. */ +type WireResponse = SocketResponse & { mac?: string }; + +export interface SocketOptions { + /** Overridable so a test does not write into the real profile. */ + appDataDir?: string; +} + +/** + * A line longer than this is not a request. + * + * Without it either side buffers whatever the peer sends until the process + * dies — and the server side is the Electron main process, which is the window + * the user is looking at. + */ +export const MAX_LINE_BYTES = 64 * 1024; + +/** More than this many at once is not a CLI asking a question. */ +const MAX_CONNECTIONS = 8; + +/** + * The identity of a project, for naming its endpoint. + * + * Normalised first, and that is not cosmetic: the desktop passes whatever + * `resolveProject` produced and the CLI passes `process.cwd()`, and on Windows + * those routinely differ in drive-letter case or by a junction for the same + * directory. Every such difference used to hash to a different pipe, so the + * optimisation silently did not fire and nothing said so. + */ +export function projectKey(projectRoot: string): string { + let normalised = resolve(projectRoot); + try { + normalised = realpathSync(normalised); + } catch { + /* not there yet, or not readable — the resolved form is the best we have */ + } + if (process.platform === "win32") normalised = normalised.toLowerCase(); + return createHash("sha256").update(normalised).digest("hex").slice(0, 16); +} + +/** Where the token and (off Windows) the socket file live. 0700, always. */ +function endpointDir(options: SocketOptions = {}): string { + const dir = join(options.appDataDir ?? defaultAppDataDir(), "sockets"); + mkdirSync(dir, { recursive: true, mode: 0o700 }); + return dir; +} + /** * The pipe a project's application listens on. * @@ -51,14 +117,21 @@ export type SocketResponse = { ok: true; result: unknown } | { ok: false; error: * hashes it too: a pipe name is visible to every process on the machine, and * the path carries somebody's username and the name of what they are working * on. Windows named pipes are the only form that works on the platform this - * product supports; the same name is a filesystem socket elsewhere, which is - * what makes this testable off Windows. + * product supports; elsewhere it is a filesystem socket, which is what makes + * this testable off Windows — and it goes in the application's own 0700 + * directory rather than `/tmp`, which is world-writable and therefore + * squattable by any local account. */ -export function socketPath(projectRoot: string): string { - const id = createHash("sha256").update(projectRoot).digest("hex").slice(0, 16); +export function socketPath(projectRoot: string, options: SocketOptions = {}): string { + const id = projectKey(projectRoot); return process.platform === "win32" ? `\\\\.\\pipe\\open-wiki-${id}` - : join(process.env["TMPDIR"] ?? "/tmp", `open-wiki-${id}.sock`); + : join(endpointDir(options), `${id}.sock`); +} + +/** The file the server leaves its token in, for a client running as the same user. */ +export function tokenFile(projectRoot: string, options: SocketOptions = {}): string { + return join(endpointDir(options), `${projectKey(projectRoot)}.token`); } /** Whether a verb is one the socket may answer. Everything else writes. */ @@ -91,7 +164,11 @@ export function handleRequest(projectRoot: string, request: SocketRequest): Sock // sits, so the correct implementation is also the confined one). const ref = listPages(projectRoot).find((p) => p.slug === slug); if (!ref) return { ok: false, error: `no page "${slug}" in this wiki` }; - const file = assertWithin(projectRoot, join(projectRoot, ref.path)); + // Confined to `wiki/`, not to the project — the same check and the same + // reason as `packages/mcp/src/tools.ts`: a path that is inside the project + // but outside the wiki is not a page, and the weaker of two checks in one + // codebase is the one that is eventually wrong. + const file = assertWithin(join(projectRoot, "wiki"), join(projectRoot, ref.path)); if (!existsSync(file)) return { ok: false, error: `no page "${slug}" in this wiki` }; return { ok: true, result: readFileSync(file, "utf8") }; } catch (e) { @@ -104,25 +181,62 @@ function encode(value: unknown): string { return `${JSON.stringify(value)}\n`; } +/** Constant time, and false rather than throwing when the lengths differ. */ +function sameSecret(a: string, b: string): boolean { + const left = Buffer.from(a, "utf8"); + const right = Buffer.from(b, "utf8"); + if (left.length !== right.length || left.length === 0) return false; + return timingSafeEqual(left, right); +} + +function macOf(token: string, nonce: string): string { + return createHmac("sha256", token).update(nonce).digest("hex"); +} + /** * Listen for queries about this project. Returns something the window closes * when it does — a server left behind answers about a project nobody has open. + * + * `onError` is called rather than swallowed. A failed bind means either a + * second window on the same project or something that took the name first, and + * both are worth saying out loud: the CLI would otherwise be talking to + * whatever that is, with nothing anywhere indicating this window is not the + * one answering. */ -export function serveQueries(projectRoot: string): Server { +export function serveQueries( + projectRoot: string, + options: SocketOptions & { onError?: (error: Error) => void } = {}, +): Server { + const token = randomBytes(32).toString("hex"); const server = createServer((socket) => { let buffered = ""; socket.on("data", (chunk: Buffer) => { buffered += chunk.toString("utf8"); + if (buffered.length > MAX_LINE_BYTES) { + socket.destroy(); + return; + } let newline = buffered.indexOf("\n"); while (newline >= 0) { const line = buffered.slice(0, newline); buffered = buffered.slice(newline + 1); if (line.trim()) { - let response: SocketResponse; + let response: WireResponse; try { - response = handleRequest(projectRoot, JSON.parse(line) as SocketRequest); + const wire = JSON.parse(line) as WireRequest; + // Unauthenticated peers get nothing at all — not an error message, + // which would confirm the endpoint is the real one. + if (typeof wire.token !== "string" || !sameSecret(wire.token, token)) { + socket.destroy(); + return; + } + response = { + ...handleRequest(projectRoot, { verb: wire.verb, args: wire.args ?? [] }), + mac: macOf(token, String(wire.nonce ?? "")), + }; } catch { - response = { ok: false, error: "that is not a request" }; + socket.destroy(); + return; } socket.write(encode(response)); } @@ -133,33 +247,54 @@ export function serveQueries(projectRoot: string): Server { // taking the main process down for. socket.on("error", () => socket.destroy()); }); - server.on("error", () => {}); - const path = socketPath(projectRoot); - // A stale socket file from a process that was killed would otherwise make - // every later listen fail with EADDRINUSE. - if (process.platform !== "win32" && existsSync(path)) { - try { - unlinkSync(path); - } catch { - /* nothing listening and nothing to remove */ - } - } - server.listen(path); + server.maxConnections = MAX_CONNECTIONS; + + const onError = + options.onError ?? ((error: Error) => console.error(`open-wiki: ${error.message}`)); + const file = tokenFile(projectRoot, options); + server.on("error", (error: Error) => { + // Whatever the reason, this window is not the one answering. Take the + // token back so a client reads nothing rather than the wrong thing. + rmSync(file, { force: true }); + onError(error); + }); + server.on("close", () => rmSync(file, { force: true })); + + // A live socket at the path is another window's, and removing it would steal + // its clients; a leftover from a process that was killed makes `listen` fail + // with EADDRINUSE. Both are reported rather than guessed at — the product is + // Windows-only, where a named pipe has neither problem, and a silent unlink + // of a path in a shared directory is how a socket becomes a delete. + writeFileSync(file, token, { encoding: "utf8", mode: 0o600 }); + server.listen(socketPath(projectRoot, options)); return server; } /** * Ask the running application, or answer `null` when there is none. * - * Every failure — nothing listening, a stale pipe, a timeout, a response that - * is not JSON — reads as "no application", because the caller's fallback is - * correct in all of them and slower in none that matter. + * Every failure — no token, nothing listening, a stale pipe, a timeout, a + * response that is not JSON or does not prove it came from the application — + * reads as "no application", because the caller's fallback is correct in all of + * them and slower in none that matter. */ export function askRunningApp( projectRoot: string, request: SocketRequest, timeoutMs = 300, + options: SocketOptions = {}, ): Promise { + let token: string; + let path: string; + try { + token = readFileSync(tokenFile(projectRoot, options), "utf8").trim(); + path = socketPath(projectRoot, options); + } catch { + return Promise.resolve(null); // no application has this project open + } + if (!token) return Promise.resolve(null); + const nonce = randomBytes(16).toString("hex"); + return new Promise((resolve) => { let settled = false; const done = (value: SocketResponse | null): void => { @@ -168,20 +303,33 @@ export function askRunningApp( socket.destroy(); resolve(value); }; - const socket = createConnection(socketPath(projectRoot)); + const socket = createConnection(path); // Short: the whole point is to be faster than starting a process, so a // socket that is slow to answer has already lost its reason to exist. socket.setTimeout(timeoutMs, () => done(null)); socket.on("error", () => done(null)); - socket.on("connect", () => socket.write(encode(request))); + socket.on("connect", () => socket.write(encode({ ...request, token, nonce }))); let buffered = ""; socket.on("data", (chunk: Buffer) => { buffered += chunk.toString("utf8"); + // A peer that answers with megabytes and no newline is not the + // application, and holding it all first is how it wins anyway. + if (buffered.length > MAX_LINE_BYTES) { + done(null); + return; + } const newline = buffered.indexOf("\n"); if (newline < 0) return; try { - done(JSON.parse(buffered.slice(0, newline)) as SocketResponse); + const { mac, ...response } = JSON.parse(buffered.slice(0, newline)) as WireResponse; + // Whoever answered has to hold the token too. Without this the CLI + // prints whatever squatted the endpoint name as wiki content. + if (typeof mac !== "string" || !sameSecret(mac, macOf(token, nonce))) { + done(null); + return; + } + done(response as SocketResponse); } catch { done(null); } diff --git a/packages/access/tests/socket.spec.ts b/packages/access/tests/socket.spec.ts index d783475..a82f169 100644 --- a/packages/access/tests/socket.spec.ts +++ b/packages/access/tests/socket.spec.ts @@ -1,4 +1,5 @@ -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { createConnection, createServer, type Server } from "node:net"; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; @@ -6,20 +7,30 @@ import { askRunningApp, handleRequest, isQueryVerb, + MAX_LINE_BYTES, + projectKey, serveQueries, socketPath, SOCKET_VERBS, + tokenFile, } from "../src/socket.js"; let root: string; +let appDataDir: string; beforeEach(() => { root = mkdtempSync(join(tmpdir(), "ow-socket-")); + appDataDir = mkdtempSync(join(tmpdir(), "ow-appdata-")); for (const part of ["raw", "wiki", ".state"]) mkdirSync(join(root, part), { recursive: true }); writeFileSync(join(root, "wiki", "index.md"), "# Index\n", "utf8"); }); -afterEach(() => rmSync(root, { recursive: true, force: true })); +afterEach(() => { + rmSync(root, { recursive: true, force: true }); + rmSync(appDataDir, { recursive: true, force: true }); +}); + +const opts = (): { appDataDir: string } => ({ appDataDir }); function page(slug: string, body = "body\n"): void { const front = [ @@ -39,18 +50,34 @@ describe("socketPath", () => { it("is per project, so two open projects do not share one", () => { const other = mkdtempSync(join(tmpdir(), "ow-other-")); try { - expect(socketPath(root)).not.toBe(socketPath(other)); + expect(socketPath(root, opts())).not.toBe(socketPath(other, opts())); } finally { rmSync(other, { recursive: true, force: true }); } }); it("is stable for one project", () => { - expect(socketPath(root)).toBe(socketPath(root)); + expect(socketPath(root, opts())).toBe(socketPath(root, opts())); }); it("carries no part of the path, which is somebody's username", () => { - expect(socketPath(root)).not.toContain(root.split(/[\\/]/).pop()); + expect(socketPath(root, opts())).not.toContain(root.split(/[\\/]/).pop()); + }); + + it("agrees for two spellings of one directory", () => { + // The desktop passes what `resolveProject` produced and the CLI passes + // `process.cwd()`. When those differ only in spelling — a trailing + // separator, `.`, or drive-letter case on Windows — a socket named from + // the unnormalised string silently never fires, and nothing says so. + const spellings = [ + root, + `${root}${process.platform === "win32" ? "\\" : "/"}`, + join(root, "."), + ]; + if (process.platform === "win32") spellings.push(root.toUpperCase()); + for (const spelling of spellings) { + expect(projectKey(spelling)).toBe(projectKey(root)); + } }); }); @@ -95,6 +122,16 @@ describe("handleRequest (9.14)", () => { const response = handleRequest(root, { verb: "read", args: ["ghost"] }); expect(response.ok).toBe(false); }); + + it("refuses to read a file that is in the project but not in the wiki", () => { + // The slug arrives over a socket. Nothing outside `wiki/` is a page, and + // confining to the project alone would make `../README` reachable. + writeFileSync(join(root, "README.md"), "not a page\n", "utf8"); + for (const slug of ["../README", "..\\README", "../.state/log.jsonl"]) { + const response = handleRequest(root, { verb: "read", args: [slug] }); + expect(response.ok).toBe(false); + } + }); }); describe("the socket end to end (9.14)", () => { @@ -102,9 +139,9 @@ describe("the socket end to end (9.14)", () => { // "Both paths produce the same answer" is the requirement, and it is the // one that makes the optimisation safe to have at all. page("fenix"); - const server = serveQueries(root); + const server = serveQueries(root, opts()); try { - const overWire = await askRunningApp(root, { verb: "read", args: ["fenix"] }); + const overWire = await askRunningApp(root, { verb: "read", args: ["fenix"] }, 2000, opts()); const standalone = handleRequest(root, { verb: "read", args: ["fenix"] }); expect(overWire).toEqual(standalone); } finally { @@ -115,20 +152,121 @@ describe("the socket end to end (9.14)", () => { it("answers null when nothing is listening, so the caller falls back", async () => { // The socket is an optimisation. A CLI that failed because the application // was not running would be a CLI that needs the application. - expect(await askRunningApp(root, { verb: "read", args: ["fenix"] }, 500)).toBeNull(); + expect(await askRunningApp(root, { verb: "read", args: ["fenix"] }, 500, opts())).toBeNull(); }, 15_000); it("refuses a write over the wire", async () => { page("fenix"); - const server = serveQueries(root); + const server = serveQueries(root, opts()); try { - const response = await askRunningApp(root, { - verb: "write", - args: ["wiki/fenix.md", "clobbered"], - }); + const response = await askRunningApp( + root, + { verb: "write", args: ["wiki/fenix.md", "clobbered"] }, + 2000, + opts(), + ); expect(response?.ok).toBe(false); } finally { server.close(); } }, 15_000); + + it("takes the token back when the window closes", async () => { + page("fenix"); + const server = serveQueries(root, opts()); + expect(readFileSync(tokenFile(root, opts()), "utf8")).not.toBe(""); + await new Promise((done) => server.close(() => done())); + expect(await askRunningApp(root, { verb: "read", args: ["fenix"] }, 500, opts())).toBeNull(); + }, 15_000); }); + +describe("the socket authenticates both directions (9.14)", () => { + it("says nothing to a peer without the token", async () => { + // The endpoint name is a hash of a directory — obscure, not secret. A + // local process that guesses it must not be able to read the wiki, and an + // error message back would confirm it found the right name. + page("fenix"); + const server = serveQueries(root, opts()); + try { + const answered = await rawRoundTrip(socketPath(root, opts()), { + verb: "read", + args: ["fenix"], + token: "0".repeat(64), + nonce: "n", + }); + expect(answered).toBeNull(); + } finally { + server.close(); + } + }, 15_000); + + it("ignores an answer that cannot prove it came from the application", async () => { + // The other direction, and the one that matters more: whatever holds the + // endpoint decides what `ow read` prints into an agent's context. + page("fenix"); + writeFileSync(tokenFile(root, opts()), "a-token-the-impostor-does-not-have", { + encoding: "utf8", + mode: 0o600, + }); + const impostor = await listenOnce(socketPath(root, opts()), (socket) => { + socket.write(`${JSON.stringify({ ok: true, result: "forged", mac: "beef" })}\n`); + }); + try { + const answered = await askRunningApp(root, { verb: "read", args: ["fenix"] }, 2000, opts()); + expect(answered).toBeNull(); + } finally { + impostor.close(); + } + }, 15_000); + + it("hangs up on a peer that sends a line it will never finish", async () => { + page("fenix"); + const server = serveQueries(root, opts()); + try { + const closed = await new Promise((done) => { + const socket = createConnection(socketPath(root, opts())); + socket.on("connect", () => socket.write("x".repeat(MAX_LINE_BYTES + 1))); + socket.on("close", () => done(true)); + socket.on("error", () => done(true)); + setTimeout(() => done(false), 3000); + }); + expect(closed).toBe(true); + } finally { + server.close(); + } + }, 15_000); +}); + +/** A client that skips `askRunningApp`, so a test can send something it never would. */ +function rawRoundTrip(path: string, payload: unknown): Promise { + return new Promise((done) => { + let settled = false; + const finish = (value: unknown | null): void => { + if (settled) return; + settled = true; + socket.destroy(); + done(value); + }; + const socket = createConnection(path); + socket.on("connect", () => socket.write(`${JSON.stringify(payload)}\n`)); + socket.on("data", (chunk: Buffer) => finish(JSON.parse(chunk.toString("utf8")) as unknown)); + socket.on("close", () => finish(null)); + socket.on("error", () => finish(null)); + setTimeout(() => finish(null), 2000); + }); +} + +/** A server that is not this application, holding the endpoint name. */ +function listenOnce( + path: string, + answer: (socket: import("node:net").Socket) => void, +): Promise { + return new Promise((done, fail) => { + const server = createServer((socket) => { + socket.on("data", () => answer(socket)); + socket.on("error", () => socket.destroy()); + }); + server.on("error", fail); + server.listen(path, () => done(server)); + }); +} diff --git a/packages/cli/package.json b/packages/cli/package.json index c1031f6..f9f3aca 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -6,9 +6,6 @@ "bin": { "ow": "build/ow.mjs" }, - "exports": { - ".": "./src/index.ts" - }, "scripts": { "test": "vitest run", "test:coverage": "vitest run --coverage", @@ -17,7 +14,7 @@ "build": "node ../../scripts/build-cli.mjs", "prepack": "pnpm run build" }, - "dependencies": { + "devDependencies": { "@open-wiki/access": "workspace:*", "@open-wiki/mcp": "workspace:*" }, @@ -35,6 +32,5 @@ "publishConfig": { "access": "public", "provenance": true - }, - "devDependencies": {} + } } diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index a91b966..8a5bd4d 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -1,9 +1,15 @@ -#!/usr/bin/env node import { main } from "./main.js"; /** * The process entrypoint, and nothing else. Everything a test would want to * drive lives in `main.ts`; what is left here is the pair of things a test * cannot have — the real `process.argv` and a real `process.exit`. + * + * **No shebang here; `scripts/build-cli.mjs` writes it.** What ships is the + * bundle, and that bundle needs a `createRequire` shim above the first line of + * code — esbuild emits the banner before the shebang it hoists out of this + * file, so two shebangs would land in the output and the second one is a + * syntax error. One place writes it, and it is the place that also writes the + * shim. */ main(process.argv.slice(2)).then((code) => process.exit(code)); diff --git a/packages/cli/src/install.ts b/packages/cli/src/install.ts index 7ea572a..9e481da 100644 --- a/packages/cli/src/install.ts +++ b/packages/cli/src/install.ts @@ -9,6 +9,27 @@ const OW_HOOKS = { post: "ow gate post", }; +/** + * Which tools each hook matches. + * + * Exported because `plugins/open-wiki/hooks/hooks.json` has to say the same + * thing, and there is no mechanism that makes two JSON files agree. A user who + * installs the plugin instead of running `ow init` gets whatever that file + * says, and the two drifted the first time they were written: the plugin + * dropped `Bash` — the matcher whose whole purpose is shell writes — and added + * `MultiEdit`, which this one did not have. `packages/cli/tests/release.spec.ts` + * asserts they match. + */ +export const HOOK_MATCHERS = { + // `Bash` is here because a page written through a shell command arrives as a + // command string with no page content to inspect, and denying `Edit(wiki/**)` + // does not constrain `Bash` — permission rules are per tool. + pre: "Write|Edit|MultiEdit|Bash", + // Post records a write that has actually happened, and a `Bash` write is one + // this gate never saw the content of. + post: "Write|Edit|MultiEdit", +} as const; + interface HookEntry { matcher: string; hooks: Array<{ type: string; command: string }>; @@ -39,8 +60,8 @@ export function writeHooks(projectRoot: string): { written: string } { } doc.hooks ??= {}; - doc.hooks.PreToolUse = upsertEntry(doc.hooks.PreToolUse, "Write|Edit|Bash", OW_HOOKS.pre); - doc.hooks.PostToolUse = upsertEntry(doc.hooks.PostToolUse, "Write|Edit", OW_HOOKS.post); + doc.hooks.PreToolUse = upsertEntry(doc.hooks.PreToolUse, HOOK_MATCHERS.pre, OW_HOOKS.pre); + doc.hooks.PostToolUse = upsertEntry(doc.hooks.PostToolUse, HOOK_MATCHERS.post, OW_HOOKS.post); mkdirSync(join(file, ".."), { recursive: true }); writeFileSync(file, JSON.stringify(doc, null, 2) + "\n", "utf8"); diff --git a/packages/cli/tests/install.spec.ts b/packages/cli/tests/install.spec.ts index 1b0cdde..a442f1a 100644 --- a/packages/cli/tests/install.spec.ts +++ b/packages/cli/tests/install.spec.ts @@ -3,7 +3,7 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "nod import { tmpdir } from "node:os"; import { join } from "node:path"; import { LANGUAGES } from "@open-wiki/access"; -import { writeClaudeMd, writeHooks } from "../src/install.js"; +import { HOOK_MATCHERS, writeClaudeMd, writeHooks } from "../src/install.js"; import { generateClaudeMd } from "@open-wiki/access"; /** @@ -45,8 +45,8 @@ describe("writeHooks (9.5)", () => { expect(written).toBe(join(root, ".claude", "hooks", "hooks.json")); const doc = readHooks(root); - expect(commandsFor(doc.hooks.PreToolUse, "Write|Edit|Bash")).toEqual(["ow gate pre"]); - expect(commandsFor(doc.hooks.PostToolUse, "Write|Edit")).toEqual(["ow gate post"]); + expect(commandsFor(doc.hooks.PreToolUse, HOOK_MATCHERS.pre)).toEqual(["ow gate pre"]); + expect(commandsFor(doc.hooks.PostToolUse, HOOK_MATCHERS.post)).toEqual(["ow gate post"]); }); it("keeps the project's own hooks — installing the gate is not destructive", () => { @@ -56,7 +56,7 @@ describe("writeHooks (9.5)", () => { JSON.stringify({ hooks: { PreToolUse: [ - { matcher: "Write|Edit|Bash", hooks: [{ type: "command", command: "their-linter" }] }, + { matcher: HOOK_MATCHERS.pre, hooks: [{ type: "command", command: "their-linter" }] }, { matcher: "Read", hooks: [{ type: "command", command: "their-auditor" }] }, ], }, @@ -65,7 +65,7 @@ describe("writeHooks (9.5)", () => { ); const doc = readHooks(writeHooksAt(root)); - expect(commandsFor(doc.hooks.PreToolUse, "Write|Edit|Bash")).toEqual([ + expect(commandsFor(doc.hooks.PreToolUse, HOOK_MATCHERS.pre)).toEqual([ "their-linter", "ow gate pre", ]); @@ -77,15 +77,15 @@ describe("writeHooks (9.5)", () => { writeHooks(root); writeHooks(root); const doc = readHooks(root); - expect(commandsFor(doc.hooks.PreToolUse, "Write|Edit|Bash")).toEqual(["ow gate pre"]); - expect(commandsFor(doc.hooks.PostToolUse, "Write|Edit")).toEqual(["ow gate post"]); + expect(commandsFor(doc.hooks.PreToolUse, HOOK_MATCHERS.pre)).toEqual(["ow gate pre"]); + expect(commandsFor(doc.hooks.PostToolUse, HOOK_MATCHERS.post)).toEqual(["ow gate post"]); }); it("starts over from a hooks.json that will not parse rather than refusing to install", () => { mkdirSync(join(root, ".claude", "hooks"), { recursive: true }); writeFileSync(join(root, ".claude", "hooks", "hooks.json"), "{ not json", "utf8"); const doc = readHooks(writeHooksAt(root)); - expect(commandsFor(doc.hooks.PreToolUse, "Write|Edit|Bash")).toEqual(["ow gate pre"]); + expect(commandsFor(doc.hooks.PreToolUse, HOOK_MATCHERS.pre)).toEqual(["ow gate pre"]); }); }); diff --git a/packages/cli/tests/main.spec.ts b/packages/cli/tests/main.spec.ts index af4f6ce..52734a1 100644 --- a/packages/cli/tests/main.spec.ts +++ b/packages/cli/tests/main.spec.ts @@ -172,6 +172,40 @@ describe("ow write", () => { }); }); +describe("ow read (9.14)", () => { + // The socket module is tested where it lives. What is tested here is the + // wiring that makes 9.14 user-visible at all — the verb a hook actually + // runs. It is also the level at which "both paths produce the same answer" + // is a thing a user would notice: with no application listening, `ow read` + // has to print the page rather than fail because nothing answered. + let root: string; + beforeEach(() => { + root = tempProject(); + writeFileSync( + join(root, "wiki", "fenix.md"), + page("fenix", "Fenix", "Fenix is a rebuild.\n"), + "utf8", + ); + }); + afterEach(() => rmSync(root, { recursive: true, force: true })); + + it("prints the page when no application is listening", async () => { + expect(await main(["read", "fenix"], root)).toBe(0); + expect(stdout()).toContain("Fenix is a rebuild."); + }); + + it("needs a slug", async () => { + expect(await main(["read"], root)).toBe(2); + expect(stderr()).toContain("slug"); + }); + + it("reports a page that is not there as a sentence, not a stack", async () => { + expect(await main(["read", "ghost"], root)).toBe(2); + expect(stderr()).toContain("ghost"); + expect(stderr()).not.toContain(" at "); + }); +}); + describe("ow graph and ow search", () => { let root: string; beforeEach(() => { diff --git a/packages/cli/tests/release.spec.ts b/packages/cli/tests/release.spec.ts index 4a2f24e..9846c82 100644 --- a/packages/cli/tests/release.spec.ts +++ b/packages/cli/tests/release.spec.ts @@ -1,4 +1,11 @@ +import { readFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; +import { HOOK_MATCHERS } from "../src/install.js"; + +/** This file, `packages/cli/tests/`, is three levels down from the root. */ +const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", ".."); /* * The release scripts are plain ESM on purpose: CI runs them with `node` and * nothing bundles them, so they carry no types. Imported dynamically and typed @@ -36,6 +43,15 @@ describe("versionOfTag (10.2)", () => { expect(versionOfTag(tag)).toBeNull(); } }); + + it("refuses a version that walks up a directory", () => { + // The version becomes a path segment — of the manifest directory and of + // the URL those manifests point at. The prerelease suffix permits `.` and + // `-`, which is enough to spell `..`. + for (const tag of ["v1.0.0-..", "v1.0.0-..-..", "v1.0.0-a..b"]) { + expect(versionOfTag(tag)).toBeNull(); + } + }); }); describe("checkRelease (10.3)", () => { @@ -108,7 +124,19 @@ describe("the package manifests (10.4)", () => { it("quotes the hash in the Scoop manifest", () => { const scoop = JSON.parse(scoopManifest("0.1.0", SHA.toUpperCase())); expect(scoop.architecture["64bit"].hash).toBe(SHA); - expect(scoop.architecture["64bit"].url).toBe(installerUrl("0.1.0")); + expect(scoop.architecture["64bit"].url).toContain(installerUrl("0.1.0")); + }); + + it("actually installs something through Scoop", () => { + // What the release publishes is an NSIS setup, and Scoop understands Inno + // but not NSIS. A manifest with a bare `.exe` url and a `bin` entry + // downloads the setup, never runs it, and then fails to shim a file that + // was never extracted — passing every assertion about its strings. + const scoop = JSON.parse(scoopManifest("0.1.0", SHA)); + expect(scoop.bin).toBeUndefined(); + expect(scoop.installer.script.join("\n")).toContain("open-wiki-Setup-0.1.0.exe"); + expect(scoop.installer.script.join("\n")).toContain("/S"); + expect(scoop.uninstaller.script.join("\n")).toContain("Uninstall open-wiki.exe"); }); it("keeps Scoop able to update itself from the published sums", () => { @@ -133,10 +161,22 @@ describe("checkPlugin (10.6)", () => { plugins: [{ name: "open-wiki", source: "./plugins/open-wiki", version: "0.1.0" }], }; const manifest = { name: "open-wiki", version: "0.1.0" }; + const hooks = { + hooks: { + PreToolUse: [ + { + matcher: "Write|Edit|MultiEdit|Bash", + hooks: [{ type: "command", command: "npx -y open-wiki@0.1.0 gate pre" }], + }, + ], + }, + }; const files = (over: Record = {}) => { const map: Record = { ".claude-plugin/marketplace.json": marketplace, "plugins/open-wiki/.claude-plugin/plugin.json": manifest, + "plugins/open-wiki/hooks/hooks.json": hooks, + "packages/cli/package.json": { version: "0.1.0" }, ...over, }; return (path: string) => { @@ -145,11 +185,47 @@ describe("checkPlugin (10.6)", () => { return hit ? map[hit] : null; }; }; + const PRESENT = ["plugins/open-wiki", "plugins/open-wiki/hooks/hooks.json"]; const exists = (present: string[]) => (path: string) => present.some((p) => path.replace(/\\/g, "/").endsWith(p)); it("accepts a plugin the marketplace and the manifest agree on", () => { - expect(checkPlugin(".", files(), exists(["plugins/open-wiki"]))).toEqual({ ok: true }); + expect(checkPlugin(".", files(), exists(PRESENT))).toEqual({ ok: true }); + }); + + it("refuses a hook that resolves whatever is latest on the registry", () => { + // `npx -y open-wiki gate pre` fetches from the registry on *every page + // write* — the exact cost the CLI bundle exists to remove — and it defeats + // 10.3: the installer and the npm package ship from one tag so they cannot + // skew, and a hook picking up `latest` skews by design. + const result = checkPlugin( + ".", + files({ + "plugins/open-wiki/hooks/hooks.json": { + hooks: { + PreToolUse: [{ hooks: [{ type: "command", command: "npx -y open-wiki gate pre" }] }], + }, + }, + }), + exists(PRESENT), + ); + expect(result.ok).toBe(false); + expect(result.ok === false && result.problems.join(" ")).toContain("open-wiki@0.1.0"); + }); + + it("refuses a pin that is not the version this repository publishes", () => { + const result = checkPlugin( + ".", + files({ "packages/cli/package.json": { version: "0.2.0" } }), + exists(PRESENT), + ); + expect(result.ok).toBe(false); + }); + + it("refuses a plugin with no hooks — the gate is what it is for", () => { + const result = checkPlugin(".", files(), exists(["plugins/open-wiki"])); + expect(result.ok).toBe(false); + expect(result.ok === false && result.problems.join(" ")).toMatch(/ships no hooks/); }); it("refuses when the two disagree on the version", () => { @@ -176,21 +252,13 @@ describe("checkPlugin (10.6)", () => { // `adr:0015` gives the convention one home, and it is the project. A copy // here would be a second, and two copies of a convention drift — which is // the failure that record exists to prevent. - const result = checkPlugin( - ".", - files(), - exists(["plugins/open-wiki", "plugins/open-wiki/skills"]), - ); + const result = checkPlugin(".", files(), exists([...PRESENT, "plugins/open-wiki/skills"])); expect(result.ok).toBe(false); expect(result.ok === false && result.problems.join(" ")).toContain("adr:0015"); }); it("refuses a plugin that ships a .mcp.json", () => { - const result = checkPlugin( - ".", - files(), - exists(["plugins/open-wiki", "plugins/open-wiki/.mcp.json"]), - ); + const result = checkPlugin(".", files(), exists([...PRESENT, "plugins/open-wiki/.mcp.json"])); expect(result.ok).toBe(false); expect(result.ok === false && result.problems.join(" ")).toMatch(/differ per user/); }); @@ -200,3 +268,27 @@ describe("checkPlugin (10.6)", () => { expect(result.ok).toBe(false); }); }); + +describe("the plugin and `ow init` install the same gate (10.6)", () => { + // Two JSON files, one convention, and nothing that makes them agree. They + // drifted the first time they were written: the plugin dropped `Bash` — the + // matcher whose whole purpose is shell writes, which `ow init` has because a + // page written through a command carries no content for the gate to see — + // and added `MultiEdit`, which `ow init` did not have. A user who installs + // the plugin instead of running `ow init` gets whichever of the two is wrong. + const plugin = JSON.parse( + readFileSync(join(REPO_ROOT, "plugins/open-wiki/hooks/hooks.json"), "utf8"), + ) as { hooks: Record> }; + + it("matches the same tools before a write", () => { + expect(plugin.hooks["PreToolUse"]?.map((e) => e.matcher)).toEqual([HOOK_MATCHERS.pre]); + }); + + it("matches the same tools after one", () => { + expect(plugin.hooks["PostToolUse"]?.map((e) => e.matcher)).toEqual([HOOK_MATCHERS.post]); + }); + + it("passes its own shape check against the real files", () => { + expect(checkPlugin(REPO_ROOT)).toEqual({ ok: true }); + }); +}); diff --git a/plans/open-wiki.md b/plans/open-wiki.md index fffbb77..c38f301 100644 --- a/plans/open-wiki.md +++ b/plans/open-wiki.md @@ -395,6 +395,28 @@ which have to say the same thing. (10.3). A skew between them fails looking like corrupted state rather than a bad install, which is the cost `adr:0014-typescript-everywhere-except-audio-capture` accepted. +*9.14 was built RED first.* Seven assertions in `packages/access/tests/socket.spec.ts` +failed against signature-only stubs before `socket.ts` existed — the verb split, the refusal +of a write, and the two paths agreeing. The socket is a boundary, and a boundary whose test +never failed has not been shown to test anything. + +*The endpoint name is not a secret.* The pipe is named from a hash of the project path, +which is obscure and nothing more: any local process guesses or enumerates it. So both +directions are authenticated — a random token in the application's own data directory +(0600, in a 0700 directory), and an HMAC over the client's nonce coming back — because +whatever holds that endpoint decides what `ow read` prints into an agent's context. + +*Bundling is what makes the packaging real, and it breaks three things quietly.* The +packaged application has no `node_modules` and no TypeScript, so the main process, the +preload and the CLI are all bundles — and each collapsed something the source relied on. +`import.meta.url` cannot be expressed in CommonJS (esbuild warns and exits 0, so the +packaging run stays green and the application does not start); the ESM output's `require` +stub throws for the CommonJS packages in the graph; and the resolvers that find ffmpeg and +`recorder.exe` count directories up from *their own source file*, which is three different +depths flattened into one file. The last is the worst, because everything passes and only an +installed copy fails to record — so `apps/desktop/src/main/resources.ts` states the packaged +location rather than deriving it. + **Research, later, and why it still costs almost nothing.** Nothing in this plan asks the application to go and find material — sources arrive because a person uploaded a file or recorded a meeting. But 3.7 watches an inbox, which means an agent that can already read diff --git a/plugins/open-wiki/hooks/hooks.json b/plugins/open-wiki/hooks/hooks.json index c807398..b2bc4fb 100644 --- a/plugins/open-wiki/hooks/hooks.json +++ b/plugins/open-wiki/hooks/hooks.json @@ -2,14 +2,14 @@ "hooks": { "PreToolUse": [ { - "matcher": "Write|Edit|MultiEdit", - "hooks": [{ "type": "command", "command": "npx -y open-wiki gate pre" }] + "matcher": "Write|Edit|MultiEdit|Bash", + "hooks": [{ "type": "command", "command": "npx -y open-wiki@0.1.0 gate pre" }] } ], "PostToolUse": [ { "matcher": "Write|Edit|MultiEdit", - "hooks": [{ "type": "command", "command": "npx -y open-wiki gate post" }] + "hooks": [{ "type": "command", "command": "npx -y open-wiki@0.1.0 gate post" }] } ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 00eb8f9..712489b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,20 +43,13 @@ importers: version: 3.2.7(@types/debug@4.1.13)(@types/node@22.20.1)(yaml@2.9.0) apps/desktop: - dependencies: + devDependencies: '@open-wiki/access': specifier: workspace:* version: link:../../packages/access '@open-wiki/audio': specifier: workspace:* version: link:../../packages/audio - chokidar: - specifier: ^5.0.0 - version: 5.0.0 - markdown-it: - specifier: ^14.1.0 - version: 14.3.0 - devDependencies: '@types/markdown-it': specifier: ^14.1.2 version: 14.1.2 @@ -69,12 +62,18 @@ importers: '@vitejs/plugin-react': specifier: ^4.3.4 version: 4.7.0(vite@6.4.3(@types/node@22.20.1)(yaml@2.9.0)) + chokidar: + specifier: ^5.0.0 + version: 5.0.0 electron: specifier: ^38.0.0 version: 38.8.6 electron-builder: specifier: ^25.1.8 version: 25.1.8(electron-builder-squirrel-windows@25.1.8) + markdown-it: + specifier: ^14.1.0 + version: 14.3.0 react: specifier: ^19.0.0 version: 19.2.8 @@ -106,7 +105,7 @@ importers: packages/audio: {} packages/cli: - dependencies: + devDependencies: '@open-wiki/access': specifier: workspace:* version: link:../access diff --git a/scripts/build-cli.mjs b/scripts/build-cli.mjs index 1e6ebcd..aee0f64 100644 --- a/scripts/build-cli.mjs +++ b/scripts/build-cli.mjs @@ -12,6 +12,15 @@ * The result is what npm publishes: `bin.ow` points at `build/ow.mjs`, and * `files` carries nothing else — so `npx open-wiki init` downloads one file * plus its manifest. + * + * **Nothing stays external.** `open-wiki` declares no runtime dependencies: + * `@open-wiki/access` and `@open-wiki/mcp` are workspace packages that are + * never published, and declaring them would make every install 404. So the + * bundle has to carry the whole graph — including the source adapters' + * libraries, which are reached through a dynamic `import()` and would + * otherwise be an `ERR_MODULE_NOT_FOUND` the first time somebody drops a PDF + * into `raw/`. The dynamic import still buys what it was for: esbuild splits + * those out of the startup path, so a hook run does not parse them. */ import { build } from "esbuild"; import { fileURLToPath } from "node:url"; @@ -27,13 +36,26 @@ await build({ platform: "node", target: "node22", format: "esm", - // The shebang `cli.ts` carries is a comment to esbuild; put it back. - banner: { js: "#!/usr/bin/env node" }, - // Bundled, including the workspace packages — that is the point. What stays - // external is what cannot be bundled: the native and lazily-loaded parts of - // the source adapters, which the CLI reaches only through a dynamic import - // and which npm installs alongside. - external: ["pdfjs-dist", "mammoth", "chokidar", "electron"], + // ESM, and the shebang and the `require` shim both come from here. + // + // ESM because the graph uses `import.meta.url`, which esbuild cannot express + // in CommonJS. But the graph also contains CommonJS packages — `yaml` calls + // `require("process")` — and esbuild's ESM output replaces `require` with a + // stub that throws, so the CLI died on its first `parse`. `createRequire` + // is what makes those calls real. + // + // The shebang is here rather than in `cli.ts` because esbuild emits the + // banner *above* the shebang it hoists out of the entry point: with both, + // the output carried two, and the second one is a syntax error. + banner: { + js: [ + "#!/usr/bin/env node", + 'import { createRequire as __owCreateRequire } from "node:module";', + "const require = __owCreateRequire(import.meta.url);", + ].join("\n"), + }, + // macOS-only, optional inside chokidar, and this product is Windows-only. + external: ["fsevents"], minify: false, sourcemap: false, logLevel: "info", diff --git a/scripts/build-main.mjs b/scripts/build-main.mjs index b91f737..bfb9c02 100644 --- a/scripts/build-main.mjs +++ b/scripts/build-main.mjs @@ -4,12 +4,27 @@ * node scripts/build-main.mjs * * The renderer is Vite's job; this is the other half. Electron's `main` points - * at `build/main/index.js`, and the preload at `build/main/preload.js` — a + * at `build/main/index.js`, and the preload at `build/main/preload.cjs` — a * packaged application has no TypeScript and no `node_modules` for the * workspace packages, so both have to be bundles rather than entry points. * - * `electron` itself stays external: it is provided by the runtime, and - * bundling it produces a file that cannot start. + * **Two formats, and neither is a preference.** + * + * The main process is ESM, because `apps/desktop/package.json` is + * `"type": "module"` and because the graph uses `import.meta.url` in three + * places. esbuild cannot express `import.meta` in CommonJS: it substitutes an + * empty object, `fileURLToPath(undefined)` throws at module load, and it + * *warns rather than fails*, so the packaging run stays green and the + * application does not start. + * + * The preload is CommonJS with a `.cjs` extension, because a sandboxed preload + * cannot be an ES module and `"type": "module"` would make a `.js` one. + * + * **Everything is bundled except `electron` itself.** A packaged application + * has no `node_modules`: `apps/desktop/package.json` declares its libraries as + * devDependencies precisely so electron-builder collects none of them, and the + * bundle carries them instead. Leaving one external would make it a runtime + * `ERR_MODULE_NOT_FOUND` in the installed copy and nowhere else. */ import { build } from "esbuild"; import { fileURLToPath } from "node:url"; @@ -22,23 +37,33 @@ const shared = { bundle: true, platform: "node", target: "node22", - format: "cjs", - // Provided by Electron, and by the OS in the case of the optional native - // pieces the source adapters load lazily. - external: ["electron", "pdfjs-dist", "mammoth", "chokidar", "fsevents"], + // Provided by the Electron runtime; bundling it produces a file that cannot + // start. `fsevents` is macOS-only and optional inside chokidar. + external: ["electron", "fsevents"], logLevel: "info", }; await build({ ...shared, + format: "esm", + // The graph contains CommonJS packages — `yaml` calls `require("process")` — + // and esbuild's ESM output replaces `require` with a stub that throws. The + // application would start and then die on the first page it parsed. + banner: { + js: [ + 'import { createRequire as __owCreateRequire } from "node:module";', + "const require = __owCreateRequire(import.meta.url);", + ].join("\n"), + }, entryPoints: [join(desktop, "src", "main", "index.ts")], outfile: join(desktop, "build", "main", "index.js"), }); await build({ ...shared, + format: "cjs", entryPoints: [join(desktop, "src", "main", "preload.ts")], - outfile: join(desktop, "build", "main", "preload.js"), + outfile: join(desktop, "build", "main", "preload.cjs"), }); -console.log("build-main: apps/desktop/build/main/{index,preload}.js"); +console.log("build-main: apps/desktop/build/main/{index.js,preload.cjs}"); diff --git a/scripts/ci/check-plugin.mjs b/scripts/ci/check-plugin.mjs index 312c422..a88744e 100644 --- a/scripts/ci/check-plugin.mjs +++ b/scripts/ci/check-plugin.mjs @@ -69,11 +69,50 @@ export function checkPlugin(repoRoot = root, read = defaultRead, exists = exists if (exists(join(dir, ".mcp.json"))) { problems.push(`${source} ships a .mcp.json — its contents differ per user`); } + + problems.push(...checkHooks(dir, source, read, exists, read(join(repoRoot, CLI_MANIFEST)))); } return problems.length > 0 ? { ok: false, problems } : { ok: true }; } +export const CLI_MANIFEST = "packages/cli/package.json"; + +/** + * The hook commands have to name a version. + * + * `npx -y open-wiki gate pre` resolves whatever is `latest` on the registry — + * on **every page write**, which is the exact cost `scripts/build-cli.mjs` + * exists to remove. It also defeats 10.3's whole point: the installer and the + * npm package ship from one tag so they cannot skew, and a hook that picks up + * `latest` skews by design. So the pin is checked, and checked against the + * version this repository would publish. + */ +function checkHooks(dir, source, read, exists, cli) { + const file = join(dir, "hooks", "hooks.json"); + if (!exists(file)) return [`${source} ships no hooks — the gate is what the plugin is for`]; + const doc = read(file); + if (!doc) return [`${source}/hooks/hooks.json is missing or will not parse`]; + + const problems = []; + const version = cli?.version; + const entries = Object.values(doc.hooks ?? {}).flat(); + if (entries.length === 0) problems.push(`${source}/hooks/hooks.json declares no hooks`); + for (const entry of entries) { + for (const hook of entry?.hooks ?? []) { + const command = String(hook.command ?? ""); + if (!command.includes("open-wiki")) continue; + if (!command.includes(`open-wiki@${version}`)) { + problems.push( + `${source} runs "${command}" — pin it to open-wiki@${String(version)}, or a page write ` + + `resolves whatever is latest on the registry`, + ); + } + } + } + return problems; +} + function defaultRead(path) { try { return JSON.parse(readFileSync(path, "utf8")); diff --git a/scripts/ci/package-manifests.mjs b/scripts/ci/package-manifests.mjs index 478acdf..baece7b 100644 --- a/scripts/ci/package-manifests.mjs +++ b/scripts/ci/package-manifests.mjs @@ -34,13 +34,6 @@ export function installerUrl(version) { */ export function wingetManifests(version, sha256) { const dir = `manifests/${PUBLISHER[0].toLowerCase()}/${PUBLISHER}/open-wiki/${version}`; - const header = (type) => - [ - `PackageIdentifier: ${PACKAGE_ID}`, - `PackageVersion: ${version}`, - `ManifestType: ${type}`, - "ManifestVersion: 1.6.0", - ].join("\n"); return { [`${dir}/${PACKAGE_ID}.yaml`]: [ @@ -77,7 +70,8 @@ export function wingetManifests(version, sha256) { ].join("\n"), [`${dir}/${PACKAGE_ID}.installer.yaml`]: [ - header("installer").split("\nManifestType")[0], + `PackageIdentifier: ${PACKAGE_ID}`, + `PackageVersion: ${version}`, "InstallerType: nullsoft", "Scope: user", "InstallModes:", @@ -94,8 +88,21 @@ export function wingetManifests(version, sha256) { }; } -/** Scoop is one JSON manifest, and `autoupdate` is what keeps it current. */ +/** + * Scoop is one JSON manifest, and `autoupdate` is what keeps it current. + * + * **It runs the installer rather than pretending to be portable.** Scoop's + * usual shape — download an archive, `bin` a file out of it — does not apply + * here: what the release publishes is an NSIS setup, and Scoop understands + * Inno but not NSIS. A manifest with a bare `.exe` url and a `bin` entry + * downloads the setup, never runs it, and then fails to shim a file that was + * never extracted. So the setup is invoked silently by an `installer.script`, + * and the matching `uninstaller.script` calls NSIS's own uninstaller — which + * is also what takes `$INSTDIR\bin` back off PATH. + */ export function scoopManifest(version, sha256) { + const setup = `open-wiki-Setup-${version}.exe`; + const installed = "$env:LOCALAPPDATA\\Programs\\open-wiki"; return `${JSON.stringify( { version, @@ -104,20 +111,35 @@ export function scoopManifest(version, sha256) { license: "Apache-2.0", architecture: { "64bit": { - url: installerUrl(version), + // The fragment renames the download, so the installer script below + // knows what it is called without parsing the URL. + url: `${installerUrl(version)}#/${setup}`, hash: sha256.toLowerCase(), }, }, - innosetup: false, - // The NSIS installer puts `ow` on PATH itself; Scoop shims the exe. - bin: ["ow.cmd"], + installer: { + script: [ + `Start-Process -FilePath "$dir\\${setup}" -ArgumentList '/S' -Wait`, + `Remove-Item "$dir\\${setup}" -Force -ErrorAction SilentlyContinue`, + ], + }, + uninstaller: { + script: [ + `$uninstall = "${installed}\\Uninstall open-wiki.exe"`, + "if (Test-Path $uninstall) { Start-Process -FilePath $uninstall -ArgumentList '/S' -Wait }", + ], + }, + notes: [ + "open-wiki installed itself per-user and put `ow` on your PATH.", + "Open a new shell before running it.", + ], checkver: { github: `https://github.com/${OWNER}/${REPO}`, }, autoupdate: { architecture: { "64bit": { - url: installerUrl("$version"), + url: `${installerUrl("$version")}#/open-wiki-Setup-$version.exe`, }, }, hash: { diff --git a/scripts/ci/release-version.mjs b/scripts/ci/release-version.mjs index 882d54d..f8d5379 100644 --- a/scripts/ci/release-version.mjs +++ b/scripts/ci/release-version.mjs @@ -23,10 +23,19 @@ export const RELEASED = [ { name: "the CLI", path: "packages/cli/package.json" }, ]; -/** `v0.1.0` → `0.1.0`. A tag that is not a version is not a release tag. */ +/** + * `v0.1.0` → `0.1.0`. A tag that is not a version is not a release tag. + * + * `..` is rejected separately from the pattern: the prerelease suffix permits + * `.` and `-`, and the version becomes a path segment — of the manifest + * directory this writes, and of the download URL those manifests point at. + * Anyone who can push a tag can already do worse, but a version that walks out + * of `dist/manifests` is not a thing to leave available. + */ export function versionOfTag(tag) { const match = /^v(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/.exec(tag ?? ""); - return match ? match[1] : null; + if (!match || match[1].includes("..")) return null; + return match[1]; } /** True for a tag carrying a suffix — `v0.1.0-beta.1` is not a stable release. */ @@ -71,6 +80,13 @@ function defaultRead(path) { // Run as a script: exit 1 on disagreement, and print what to fix. if (process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\/g, "/"))) { + // `--prerelease ` prints "true" or nothing, so the release workflow can + // pick npm's dist-tag from it. A prerelease published to `latest` is what + // `npx open-wiki` would then hand every user. + if (process.argv[2] === "--prerelease") { + if (isPrerelease(process.argv[3] ?? "")) console.log("true"); + process.exit(0); + } const result = checkRelease(process.argv[2] ?? ""); if (!result.ok) { for (const problem of result.problems) console.error(`release: ${problem}`); From 6c1117cc7a6f0ce8bafd76bca2e20b985bd35bcc Mon Sep 17 00:00:00 2001 From: prode Date: Sat, 1 Aug 2026 17:39:33 -0300 Subject: [PATCH 3/3] fix(plugin): give the marketplace a description, and validate both manifests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `claude plugin validate --strict` failed the moment CI could actually report it — which is the point of removing `continue-on-error`. A marketplace with no description is a warning, and `--strict` makes a warning an error. Two things follow it into `check-plugin.mjs` and CI: - The floor now refuses a marketplace with no description too. That script exists because "a check that silently passes when its tool is missing is not a check", and a floor that misses what the tool catches is the same failure one level down. - The marketplace manifest and the plugin manifest are different files, and validating the first never opens the second. CI runs both. Verified locally against the real CLI: both validate clean. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01D3VYWWTZtEE2NxPiksKsAK --- .claude-plugin/marketplace.json | 1 + .github/workflows/ci.yml | 4 ++++ packages/cli/tests/release.spec.ts | 19 +++++++++++++++++++ scripts/ci/check-plugin.mjs | 6 ++++++ 4 files changed, 30 insertions(+) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 2637c92..cd11cc2 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,5 +1,6 @@ { "name": "protonspy", + "description": "open-wiki: a project's documentation as a wiki the agent already has open, with every write going through a gate that validates it.", "owner": { "name": "protonspy", "url": "https://github.com/protonspy" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef6fb49..957b24f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -153,7 +153,11 @@ jobs: echo "could not install the Claude Code CLI — skipped (check-plugin.mjs still ran)" exit 0 fi + # Both, because they are different manifests: validating the + # marketplace reads `.claude-plugin/marketplace.json` and stops — it + # never opens the plugin's own manifest. claude plugin validate --strict . + claude plugin validate --strict ./plugins/open-wiki # The single check to require on the branch. Skipped jobs are fine — an empty # workspace has nothing to test — but a failed or cancelled one is not. diff --git a/packages/cli/tests/release.spec.ts b/packages/cli/tests/release.spec.ts index 9846c82..6048114 100644 --- a/packages/cli/tests/release.spec.ts +++ b/packages/cli/tests/release.spec.ts @@ -158,6 +158,7 @@ const { checkPlugin } = await import("../../../scripts/ci/check-plugin.mjs"); describe("checkPlugin (10.6)", () => { const marketplace = { + description: "what this marketplace offers", plugins: [{ name: "open-wiki", source: "./plugins/open-wiki", version: "0.1.0" }], }; const manifest = { name: "open-wiki", version: "0.1.0" }; @@ -222,6 +223,24 @@ describe("checkPlugin (10.6)", () => { expect(result.ok).toBe(false); }); + it("refuses a marketplace with no description", () => { + // `claude plugin validate --strict` treats the warning as an error, and + // this script is the floor underneath that tool rather than a looser + // second opinion — it exists because a check that silently passes when its + // tool is missing is not a check. + const result = checkPlugin( + ".", + files({ + ".claude-plugin/marketplace.json": { + plugins: [{ name: "open-wiki", source: "./plugins/open-wiki", version: "0.1.0" }], + }, + }), + exists(PRESENT), + ); + expect(result.ok).toBe(false); + expect(result.ok === false && result.problems.join(" ")).toMatch(/--strict/); + }); + it("refuses a plugin with no hooks — the gate is what it is for", () => { const result = checkPlugin(".", files(), exists(["plugins/open-wiki"])); expect(result.ok).toBe(false); diff --git a/scripts/ci/check-plugin.mjs b/scripts/ci/check-plugin.mjs index a88744e..f399b4c 100644 --- a/scripts/ci/check-plugin.mjs +++ b/scripts/ci/check-plugin.mjs @@ -29,6 +29,12 @@ export function checkPlugin(repoRoot = root, read = defaultRead, exists = exists problems.push("the marketplace lists no plugins"); return { ok: false, problems }; } + // `claude plugin validate --strict` treats a missing description as an + // error, and this is the floor underneath it — a check that only runs when + // the CLI installs is a check that reports the tool's absence as success. + if (!marketplace.description) { + problems.push("the marketplace has no description — --strict treats that as an error"); + } for (const entry of marketplace.plugins) { const source = String(entry.source ?? "");