npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@planuze/pack-runtime

v1.0.6

Published

Runtime de packs .plnzpack: download/cache, verify/decrypt in-memory, generator engine, orchestration, registry e uninstall.

Readme

@planuze/pack-runtime

Runtime de .plnzpack para o app desktop: baixa, verifica, decifra, materializa, compõe e executa packs, além de manter o registry de packs carregados e o uninstall. Pacote público (SDK de packs, ADR-0429); ancora no contrato de @planuze/pack-format e no OS abstraído de @planuze/platform-core.

O SDK público não resolve userData: todo caminho de cache/destino é injetado pelo app (composition root).

Pipeline de consume (fim-a-fim)

O fluxo de produção — download → verify → decrypt → materialize — é orquestrado por install/download-and-install.ts e install/install-pack-to-disk.ts:

flowchart LR
  A["POST /download<br/>(fetcher injetado)"] --> B["parse + fail-closed<br/>download-contract"]
  B --> C["PackDownloader<br/>stream + sha256 + resume"]
  C --> D["PackVerifier<br/>Ed25519 cadeia"]
  D --> E["PackDecryptor<br/>XChaCha20 + untar in-memory"]
  E --> F["materialize → targetDir<br/>+ docs em claro"]
  1. downloadAndInstallPack(input) chama o fetchDownload injetado (no app é um POST /v1/marketplace/:packId/download autenticado ao api-gateway), valida a resposta contra packDownloadResponseSchema (install/download-contract.ts) e a mapeia para InstallPackInput fail-closed: sem contentKeyHex (escrow-unwrap do registry), sem fileHash/fileBytes (integridade) ou sem publisherPublicKeyPem (chave para a cadeia), aborta em vez de instalar parcialmente (ADR-0393/0439/0440).
  2. installPackToDisk(input) executa download → verify → decrypt → materialize, reporta progresso por etapa via onProgress (ADR-0489) e devolve o PackManifest instalado. Single-pack por design (o NEP é template standalone; stack.requires são capabilities de ambiente, não um grafo de download).

Download

PackDownloader (download/downloader.ts) é um DisposableBase que faz streaming HTTP com:

  • Verificação de integridade — hash SHA-256 e contagem de bytes computados no stream; divergência de expectedHash/expectedBytes descarta o .partial e falha (validationFormat).
  • Resume — .partial no cache; envia Range: bytes=N- e só retoma com 206 Partial Content cujo content-range casa (senão trunca e rebaixa a 200, escrevendo do zero — anexar corromperia hash).
  • Rename atômico — grava em <file>.partial, valida, e só então rename para o nome final <segment>-<version>.plnzpack.
  • Path-safety do nome — o packId vai no filename como segmento on-disk (acme~widget, ADR-0367): packIdToSegment colapsa o / scoped em ~; o / nunca toca o filesystem.
  • Cancelamento e leak-safety — AbortController por download in-flight (o dispose() aborta todos), timeout de headers, e try/finally que libera o reader lock + destrói o write stream em erro.
  • Progresso throttled — onBytes reporta a cada ~256 KiB (ADR-0491), com a posição retomada reportada imediatamente.
  • purgeVersions(packId, keep) remove versões antigas do cache por mtime.

Verify

PackVerifier (decrypt/verifier.ts) verifica sobre um PackArchiveReader (do pack-format) em dois modos:

  • direto (KeyObject) — o pack foi assinado por essa chave. Usado pelo pack inspect (inspeção local pré-publicação, chave ainda não root-assinada).
  • cadeia (PackChainConfig, ADR-0440) — modo do consume: pack ← pubkey do publisher E pubkey do publisher ← root (root = âncora embarcada no app).

verifyAll(reader) faz, em ordem fail-closed: pré-check de que manifest.publicKeyFingerprint bate com a chave de assinatura (defesa contra key confusion + downgrade, §4.8); no modo cadeia, manifest.publisherFingerprint bate e a pubkey está root-assinada (signedByRootSignature !== null); manifest.sig sobre manifest.json; content.sig sobre pack.lock; e verifyPackLock (hashes de cada blob).

Decrypt

PackDecryptor (decrypt/decryptor.ts) decifra os blobs in-memory: lê content/{base,generator,agent}.tar.zst, desserializa o envelope XChaCha20-Poly1305 (com AAD ${packId}@${version}), decifra com a content key e extrai o tar em memória (decrypt/tar.ts via tar-stream) para um Map<path, Buffer>. A extração tem os mesmos caps anti-DoS do reader (≤20 000 entradas, ≤32 MiB/entrada, ≤256 MiB total) e rejeita paths inseguros. agent é opcional (ausência = missing_archive_entry tratado como undefined). Um contentDecoder opcional permite injetar descompressão zstd; o default é identidade.

O resultado é um DecryptedPackHandle (decrypt/handle.ts): guarda manifest + contents (base/generator/agent?) e, em dev mode, o sourcePath. É DisposableBase — o dispose() zera os buffers de conteúdo (buffer.fill(0)) antes de limpar os Maps (§4.9).

installPackToDisk materializa o handle em targetDir (handle/materializeDecryptedPack.ts), depois copia os docs/** em claro do archive (best-effort — a doc é acessória e nenhuma falha aqui invalida um install cujo código já materializou; ADR-0575), e descarta o handle liberando a memória.

handleFromFilesystem(packPath, packId) (handle/handleFromFilesystem.ts) constrói o mesmo handle direto de um diretório de pack já decifrado (dev mode no monorepo, ou prod pós-install) — bypass do pipeline criptográfico, com path-safety (resolveInside) no walk recursivo.

Engine (generator em subprocesso)

GeneratorEngine (engine/generator-engine.ts) executa o generator do pack e expõe um Observable<GeneratorEvent> (RxJS):

  • Staging isolado — prepareGenerator cria um tmpdir, materializa generator/
    • base/ + manifest.json + um package.json mínimo (type: module, para o loader tsx), e cria um symlink node_modules → sourcePath/node_modules (dev mode) resolvendo as workspace deps. Paths reservados (node_modules, package.json) não podem vir do pack.
  • Runner — spawn via platformProcessRunner (engine/process-runner.ts, sobre runProcess do platform-core): prefere .bin/tsx (com fallback .cmd no Windows), senão node. Env allowlist (generatorBaseEnvironment) + vars de contexto injetadas: PLANUZE_PACK_DIR, PLANUZE_PROJECT_PATH, PLANUZE_PACKS_DIR (localizar packs-filhos no step module-packs), PLANUZE_MODULE_PATH/_ID/ _ACTIVE_MODULES/_PROPS (module-packs), e o par do approval workflow (PLANUZE_APPROVAL=ndjson + PLANUZE_AI_KEY/_PROVIDER).
  • Protocolo NDJSON — o init (projectPath, project, steps/models a rodar, firstRun) vai pelo stdin; o subprocess emite eventos linha-a-linha no stdout, cada um validado por generatorEventSchema (engine/events.ts). Exit code ≠ 0 vira subscriber.error. O stdin fica aberto quando o caller registra onInput (round-trip do approval), ou é fechado após o init em runners não-interativos (senão o readline do generator segura o event loop; ADR-0265).
  • Lifecycle — IDisposable: o teardown encerra o stdin, mata o subprocess e remove o tmpdir.

resolveModuleTargetPath(...) e resolveInstallTarget centralizam a regra de "onde o pack escreve" a partir do kind.

Modo de criação nas escritas do gerador

As escritas de generator-tools (writeAtomic, writeIfChanged e writeWithApproval) aceitam mode?: number. Um gerador pode solicitar, por exemplo, mode: 0o600: o temporário já nasce com esse modo, filtrado pelo umask, antes de receber conteúdo e substituir o destino. Sem a opção, o padrão anterior é mantido. Conteúdo igual e decisão de pular continuam sem escrita ou chmod; a opção não migra permissões de arquivos existentes automaticamente. Bits POSIX não representam uma garantia de ACL no Windows. Aprovação, contenção de paths e política de conteúdo permanecem responsabilidades das respectivas APIs.

Contrato de eventos

generatorEventSchema (discriminated union por type, schemaVersion: 2): step.started / step.log / step.completed / step.failed / run.finished, e os eventos do approval workflow step.prompt_requested / step.prompt_response / file.diff_proposed / file.write_decision. O tipo é exportado também via @planuze/pack-runtime/generator-tools para o generator de cada pack validar a própria saída.

Orchestrator (progresso via RxJS)

StepOrchestrator (orchestrator/step-orchestrator.ts) consome o Observable<GeneratorEvent> e reduz a um OrchestratorState observável (steps com status pending/running/completed/failed, logs, progresso current/total, currentStepId, timing). Expõe state$ como BehaviorSubject.asObservable() — caller externo não consegue empurrar .next() e quebrar o invariante (anti-pattern de segurança do CLAUDE.md); currentState é o snapshot síncrono para reidratação de UI. IDisposable completa o subject e cancela a subscription. Um OrchestratorMetrics opcional recebe recordStep/recordRun. Erros de step são normalizados para texto legível (kind: messageKey) — nunca String(appError) cru.

Registry (packs carregados)

PackRegistry (registry/pack-registry.ts) é o cache in-memory de DecryptedPackHandles (DisposableBase):

  • load(packId, version, contentKey, credentials) — resolve o .plnzpack do cache (ioNotFound se ausente), monta um PackVerifier de cadeia com as PackLoadCredentials do /download, verifica, decifra e memoiza o handle. Deduplica requisições concorrentes (mapa loading de promises in-flight) e invalida handles descartados.
  • get / unload(packId, version) / listInstalled() — o listInstalled varre o cache por parsePackFileName (segmento on-disk → packId on-wire).
  • O dispose() descarta todos os handles (zerando buffers) e o downloader próprio.

Além do registry criptográfico, discover-monorepo-packs / installed-packs-schema / read-installed-packs / write-installed-packs gerenciam o registro persistente de packs instalados no disco.

Uninstall (idempotente)

PackUninstaller (uninstall/uninstaller.ts) descarrega do registry e remove os arquivos do cache. uninstall(packId, version?) — sem version, remove todas as versões. Idempotente: pula arquivos já ausentes (existsSync) e conta só o que removeu; emite events$ (observable read-only) por remoção. DisposableBase.

Compose (template + extensions)

composeManifests(input) (compose/composer.ts) é data-only (não toca disco nem processo): combina um template com suas extensions ativas em um manifest efetivo. Valida kinds e compatibilidade (appliesTo.packId bate; satisfiesRange da versão), faz merge aditivo de declarations detectando conflito de id cross-source, incorpora declarations/steps dos modules ativos por extension, e faz topological sort dos steps (Kahn, com dependsOn resolvido same-source→global e ordenação estável) detectando ciclos. Devolve Result<ComposedManifest, ComposeError> com erros discriminados (kind_mismatch, version_incompatible, duplicate_declaration, circular_dependency, unknown_module, …).

Approval workflow (diff antes de sobrescrever)

Quando o generator roda sob o app Electron (subprocess non-TTY), o pack não sobrescreve arquivos do usuário em silêncio. O engine só liga o canal (PLANUZE_APPROVAL=ndjson) quando o app cabeia onInput (há quem responda); sem isso, CLI/headless caem no merge determinístico automático.

  • writeWithApproval(opts) (generator-tools/write-with-approval.ts) cobre steps diretos: arquivo novo → escreve; arquivo existente e igual → no-op; existente e diferente + canal ligado → reinjeta os blocos //#ignore do disco (merge determinístico, com lowConfidence para blocos reposicionados sem âncora), emite file.diff_proposed e bloqueia até file.write_decision. Ações: overwrite (aplica o merge mostrado), skip, ai-merge (conflitos //#ignore via LLM, só com PLANUZE_AI_KEY), approve-all/reject-all (batch por run). Degrada para writeIfChanged se o diff falhar ou o app fechar o stdin.
  • O caminho do Plop tem seu próprio write action em generator-tools/plop-write/ (write-action, diff, ai-merge, prompt) sobre o mesmo protocolo.
  • ndjson-approval (generator-tools/plop-write/ndjson-approval.ts) é o cliente NDJSON no lado do subprocess: um readline persistente único multiplexado por decisionId/promptId (criar um readline por prompt faria a 2ª aprovação nunca receber o line), com teardown auto-disparado no beforeExit (o main() do generator é código do pack, sem finally nosso). requestPrompt suporta confirm/input/list/checkbox/password.

Generator tools (subpath @planuze/pack-runtime/generator-tools)

Toolkit importado pelos steps do generator dentro do subprocesso: emit-event, file-io, format (prettier), glob-files (fast-glob), render-hbs + hbs-helpers (handlebars), ignore-utils (blocos //#ignore), collect-project-schema, write-with-approval + plop-write/*, e resolveInside/resolveInsideRealpath. Este subpath é toolkit-puro: o gate check:no-domain-imports proíbe hardcodar nomes de domínio ('show', 'String', 'cpf', 'prisma') aqui (§4.18, ADR-0241).

API pública (subpath exports)

| Import | Conteúdo | |---|---| | @planuze/pack-runtime | download / decrypt / engine / install / orchestrator / registry / uninstall / compose | | @planuze/pack-runtime/generator-tools | toolkit dos steps do generator | | @planuze/pack-runtime/registry | registry + descoberta de packs instalados | | @planuze/pack-runtime/handle | handleFromFilesystem / materialize | | @planuze/pack-runtime/scaffold | copy/rename/render de scaffold |

Boundaries e build

  • Gate check:cruiser (regra pack-runtime-*): só depende de @planuze/core-kit, @planuze/platform-core e @planuze/pack-format (+ rxjs, tar-stream, fast-glob, handlebars, diff, prettier, @inquirer/prompts).
  • Toda operação com falha esperada devolve Result<_, AppError | PackFormatError> (§4.6); classes com recursos externos são IDisposable (§4.5).
pnpm --filter @planuze/pack-runtime build       # tsc modular → dist/
pnpm --filter @planuze/pack-runtime test        # vitest run
pnpm --filter @planuze/pack-runtime typecheck   # tsc --noEmit