From 81dc74734b8041d752a3fcd7551ab60b7b4af722 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 4 Sep 2026 02:45:42 +0200 Subject: [PATCH] registry, verdaccio, portainer: full nox modules (ADR 0044/0046) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit registry (docker v2): catalog/tags/delete tools, emits image.pushed (a build's image is now pullable). verdaccio (npm): list/info tools, emits package.published. portainer: endpoints/stacks/containers tools — tools-only, since its only events are the underlying containers' lifecycle, which the host owns. Typecheck; manifests parse. --- modules/portainer/client.ts | 97 ++++++++++++++++++++++++++++++++ modules/portainer/package.json | 14 +++++ modules/portainer/tools/index.ts | 50 ++++++++++++++++ modules/portainer/tsconfig.json | 12 ++++ modules/registry/client.ts | 81 ++++++++++++++++++++++++++ modules/registry/index.ts | 51 +++++++++++++++++ modules/registry/module.json | 6 ++ modules/registry/package.json | 14 +++++ modules/registry/tools/index.ts | 59 +++++++++++++++++++ modules/registry/tsconfig.json | 12 ++++ modules/verdaccio/client.ts | 81 ++++++++++++++++++++++++++ modules/verdaccio/index.ts | 45 +++++++++++++++ modules/verdaccio/module.json | 6 ++ modules/verdaccio/package.json | 14 +++++ modules/verdaccio/tools/index.ts | 35 ++++++++++++ modules/verdaccio/tsconfig.json | 12 ++++ 16 files changed, 589 insertions(+) create mode 100644 modules/portainer/client.ts create mode 100644 modules/portainer/package.json create mode 100644 modules/portainer/tools/index.ts create mode 100644 modules/portainer/tsconfig.json create mode 100644 modules/registry/client.ts create mode 100644 modules/registry/index.ts create mode 100644 modules/registry/package.json create mode 100644 modules/registry/tools/index.ts create mode 100644 modules/registry/tsconfig.json create mode 100644 modules/verdaccio/client.ts create mode 100644 modules/verdaccio/index.ts create mode 100644 modules/verdaccio/package.json create mode 100644 modules/verdaccio/tools/index.ts create mode 100644 modules/verdaccio/tsconfig.json diff --git a/modules/portainer/client.ts b/modules/portainer/client.ts new file mode 100644 index 0000000..79eaa29 --- /dev/null +++ b/modules/portainer/client.ts @@ -0,0 +1,97 @@ +// The Portainer API client — portainer's own code, living in the module (novox/hq ADR 0044). +// portainer is tools-only: its "events" would really be the underlying containers' lifecycle, +// which the host owns and emits — so this module reads Portainer's own resources (endpoints, +// stacks, containers) and exposes them, and stops there. + +export interface PortainerEndpoint { + id: number; + name: string; + type: number; + url: string; + status: number; +} + +export interface PortainerStack { + id: number; + name: string; + type: number; + endpointId: number; + status: number; +} + +export interface PortainerContainer { + id: string; + names: string[]; + image: string; + state: string; + status: string; +} + +export class PortainerClient { + readonly baseUrl: string; + + constructor( + url: string, + private readonly token: string, + ) { + this.baseUrl = url.replace(/\/+$/, ""); + } + + /** + * Build from the module's resolved environment. The URL is MESH_PORTAINER_URL (or the local + * dashboard port) and the API token is MESH_PORTAINER_TOKEN — an access token minted in + * Portainer, sent as X-API-Key. Throws when no token is configured, so a misconfigured module + * exposes nothing rather than calling Portainer unauthenticated. + */ + static fromEnv(env: NodeJS.ProcessEnv = process.env): PortainerClient { + const url = env.MESH_PORTAINER_URL ?? `https://127.0.0.1:${env.PORTAINER_PORT ?? "9443"}`; + const token = env.MESH_PORTAINER_TOKEN; + if (!token) throw new Error("no Portainer token — set MESH_PORTAINER_TOKEN"); + return new PortainerClient(url, token); + } + + private async get(path: string): Promise { + const res = await fetch(`${this.baseUrl}${path}`, { headers: { "X-API-Key": this.token } }); + if (!res.ok) throw new Error(`Portainer ${path}: ${res.status} ${await res.text()}`); + return res.json() as Promise; + } + + /** The environments (endpoints) Portainer manages — each a Docker host or cluster it talks to. */ + async listEndpoints(): Promise { + const raw = await this.get("/api/endpoints"); + return (raw ?? []).map((e) => ({ + id: e.Id, + name: e.Name, + type: e.Type, + url: e.URL, + status: e.Status, + })); + } + + /** The stacks (compose/swarm deployments) Portainer knows about. */ + async listStacks(): Promise { + const raw = await this.get("/api/stacks"); + return (raw ?? []).map((s) => ({ + id: s.Id, + name: s.Name, + type: s.Type, + endpointId: s.EndpointId, + status: s.Status, + })); + } + + /** + * The containers on one endpoint, read through Portainer's Docker API proxy. Includes stopped + * containers, so the caller sees the whole picture rather than only what is running. + */ + async listContainers(endpointId: number): Promise { + const raw = await this.get(`/api/endpoints/${endpointId}/docker/containers/json?all=1`); + return (raw ?? []).map((c) => ({ + id: c.Id, + names: c.Names ?? [], + image: c.Image, + state: c.State, + status: c.Status, + })); + } +} diff --git a/modules/portainer/package.json b/modules/portainer/package.json new file mode 100644 index 0000000..77ee328 --- /dev/null +++ b/modules/portainer/package.json @@ -0,0 +1,14 @@ +{ + "name": "@novox/module-portainer", + "version": "0.1.0", + "description": "portainer — container management UI. Its API client and tools live here (novox/hq ADR 0044).", + "type": "module", + "private": true, + "dependencies": { + "@novox/mesh-sdk": "^0.1.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.6.0" + } +} diff --git a/modules/portainer/tools/index.ts b/modules/portainer/tools/index.ts new file mode 100644 index 0000000..192d830 --- /dev/null +++ b/modules/portainer/tools/index.ts @@ -0,0 +1,50 @@ +// portainer's tools — its own code (novox/hq ADR 0044), importing portainer's own client. They +// return structured data; the mesh serves them through the sdk's tool harness. portainer is +// tools-only (no events entrypoint): a container starting or stopping is the host's signal to emit, +// not Portainer's to re-announce. + +import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools"; +import { PortainerClient } from "../client.js"; + +export function getPortainerTools(portainer: PortainerClient): ToolDefinition[] { + return [ + { + name: "portainer_endpoints", + description: "List the environments (endpoints) Portainer manages — each a Docker host or cluster.", + input: {}, + run: async () => { + const endpoints = await portainer.listEndpoints(); + return { count: endpoints.length, endpoints }; + }, + }, + { + name: "portainer_stacks", + description: "List the stacks (compose/swarm deployments) Portainer knows about.", + input: {}, + run: async () => { + const stacks = await portainer.listStacks(); + return { count: stacks.length, stacks }; + }, + }, + { + name: "portainer_containers", + description: "List the containers on one Portainer endpoint, including stopped ones.", + input: { endpoint: { type: "number", description: "the endpoint id (see portainer_endpoints)" } }, + run: async (args) => { + const endpointId = Number(args.endpoint); + const containers = await portainer.listContainers(endpointId); + return { endpointId, count: containers.length, containers }; + }, + }, + ]; +} + +// The tools exist only when a token is configured; without one, portainer contributes none rather +// than failing the whole runtime. +registerModuleTools("portainer", (env) => { + try { + return getPortainerTools(PortainerClient.fromEnv(env)); + } catch { + return []; + } +}); diff --git a/modules/portainer/tsconfig.json b/modules/portainer/tsconfig.json new file mode 100644 index 0000000..426d382 --- /dev/null +++ b/modules/portainer/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["client.ts", "tools/index.ts"] +} diff --git a/modules/registry/client.ts b/modules/registry/client.ts new file mode 100644 index 0000000..d78d61f --- /dev/null +++ b/modules/registry/client.ts @@ -0,0 +1,81 @@ +// The Docker Registry v2 client — registry's own code, living in the module (novox/hq ADR 0044). +// Ported from the shared hal sdk, where a change to the registry API rebuilt everything; here it +// rebuilds only registry. Both this module's tools and its events entrypoint import it. + +export interface RegistryImage { + repo: string; + tag: string; + digest: string; +} + +export class RegistryClient { + readonly baseUrl: string; + + // Auth is optional: a mesh-internal registry often runs open on the node, so a Basic header is + // sent only when credentials were configured — an empty one would look like a failed login. + constructor( + url: string, + private readonly authHeader?: string, + ) { + this.baseUrl = url.replace(/\/+$/, ""); + } + + /** + * Build from the module's resolved environment. The URL is MESH_REGISTRY_URL (or the local + * registry port), and credentials — if the registry requires them — are MESH_REGISTRY_USER and + * MESH_REGISTRY_PASSWORD. Throws when no URL is configured, so a misconfigured module exposes + * nothing rather than talking to the wrong place. + */ + static fromEnv(env: NodeJS.ProcessEnv = process.env): RegistryClient { + const url = env.MESH_REGISTRY_URL ?? `http://127.0.0.1:${env.REGISTRY_PORT ?? "5000"}`; + if (!url) throw new Error("no registry URL — set MESH_REGISTRY_URL"); + const user = env.MESH_REGISTRY_USER; + const password = env.MESH_REGISTRY_PASSWORD; + const authHeader = + user && password ? `Basic ${Buffer.from(`${user}:${password}`).toString("base64")}` : undefined; + return new RegistryClient(url, authHeader); + } + + private headers(extra: Record = {}): Record { + return { ...(this.authHeader ? { Authorization: this.authHeader } : {}), ...extra }; + } + + private async getJson(path: string): Promise { + const res = await fetch(`${this.baseUrl}${path}`, { headers: this.headers() }); + if (!res.ok) throw new Error(`Registry ${path}: ${res.status} ${await res.text()}`); + return res.json() as Promise; + } + + /** The catalog — every repository the registry holds. */ + async listRepositories(): Promise { + const data = await this.getJson<{ repositories: string[] | null }>("/v2/_catalog"); + return data.repositories ?? []; + } + + /** The tags of one repository. */ + async listTags(repo: string): Promise { + const data = await this.getJson<{ tags: string[] | null }>(`/v2/${repo}/tags/list`); + return data.tags ?? []; + } + + /** The content digest of a repo:tag — the stable identity a tag currently points at. */ + async getManifestDigest(repo: string, tag: string): Promise { + const res = await fetch(`${this.baseUrl}/v2/${repo}/manifests/${tag}`, { + method: "HEAD", + headers: this.headers({ Accept: "application/vnd.docker.distribution.manifest.v2+json" }), + }); + if (!res.ok) throw new Error(`Registry manifest ${repo}:${tag}: ${res.status} ${await res.text()}`); + const digest = res.headers.get("docker-content-digest"); + if (!digest) throw new Error(`no Docker-Content-Digest for ${repo}:${tag}`); + return digest; + } + + /** Delete a manifest by digest. Garbage collection reclaims the storage later. */ + async deleteManifest(repo: string, digest: string): Promise { + const res = await fetch(`${this.baseUrl}/v2/${repo}/manifests/${digest}`, { + method: "DELETE", + headers: this.headers({ Accept: "application/vnd.docker.distribution.manifest.v2+json" }), + }); + if (!res.ok) throw new Error(`Registry delete ${repo}@${digest}: ${res.status} ${await res.text()}`); + } +} diff --git a/modules/registry/index.ts b/modules/registry/index.ts new file mode 100644 index 0000000..6bdd54f --- /dev/null +++ b/modules/registry/index.ts @@ -0,0 +1,51 @@ +// registry's events. The tool runtime imports this once the broker is bound. +// +// Emits (novox/hq ADR 0046/0047): +// module.registry.image.pushed — a new image (repo:tag) was published to the registry +// +// This is a genuinely useful signal: a build finished and its image is now pullable, so anything +// on the mesh that redeploys, mirrors or announces releases can react without polling the registry +// itself. It is discovered by diffing the catalog and each repo's tags — the registry has no push +// webhook of its own, so the module watches for it. +// +// The polling is deliberately unhurried: a new image a minute late is still the event, whereas +// hammering the registry's catalog for immediacy nobody asked for is not. + +import { emit } from "@novox/mesh-sdk/events"; +import { RegistryClient } from "./client.js"; + +const registry = RegistryClient.fromEnv(); + +// Every repo:tag we have already accounted for. Primed silently on the first look so a registry +// that was already full when this started does not announce its whole history as freshly pushed. +const seen = new Set(); +let primed = false; + +async function pollCatalog(): Promise { + const repos = await registry.listRepositories(); + for (const repo of repos) { + let tags: string[]; + try { + tags = await registry.listTags(repo); + } catch { + continue; // a repo can vanish between catalog and tag read — skip it, catch it next tick + } + for (const tag of tags) { + const id = `${repo}:${tag}`; + if (!seen.has(id)) { + if (primed) await emit("module.registry.image.pushed", { repo, tag }); + seen.add(id); + } + } + } + primed = true; +} + +const tick = (fn: () => Promise, everyMs: number): void => { + const run = (): void => void fn().catch((err) => console.error(`[registry] ${err}`)); + setInterval(run, everyMs); + run(); +}; +tick(pollCatalog, 60_000); + +console.log("[registry] watching the catalog for newly pushed images"); diff --git a/modules/registry/module.json b/modules/registry/module.json index 6fe2ba0..0aa752e 100644 --- a/modules/registry/module.json +++ b/modules/registry/module.json @@ -16,6 +16,12 @@ "capabilities": [ "container-runtime" ], + "emits": [ + "module.registry.image.pushed" + ], + "own-secrets": { + "broker": "/var/lib/registry/broker" + }, "serves": { "artifact-store": { "port": 5000 diff --git a/modules/registry/package.json b/modules/registry/package.json new file mode 100644 index 0000000..74a4955 --- /dev/null +++ b/modules/registry/package.json @@ -0,0 +1,14 @@ +{ + "name": "@novox/module-registry", + "version": "0.1.0", + "description": "registry — private Docker image registry. Its API client, tools and events live here (novox/hq ADR 0044).", + "type": "module", + "private": true, + "dependencies": { + "@novox/mesh-sdk": "^0.1.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.6.0" + } +} diff --git a/modules/registry/tools/index.ts b/modules/registry/tools/index.ts new file mode 100644 index 0000000..e354baf --- /dev/null +++ b/modules/registry/tools/index.ts @@ -0,0 +1,59 @@ +// registry's tools — moved here from the shared sdk (novox/hq ADR 0044), importing registry's own +// client. They return structured data; the mesh serves them through the sdk's tool harness. + +import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools"; +import { RegistryClient } from "../client.js"; + +export function getRegistryTools(registry: RegistryClient): ToolDefinition[] { + return [ + { + name: "registry_list", + description: "List every repository in the Docker registry (the catalog).", + input: {}, + run: async () => { + const repositories = await registry.listRepositories(); + return { count: repositories.length, repositories }; + }, + }, + { + name: "registry_tags", + description: "List the tags of one repository in the Docker registry.", + input: { repo: { type: "string", description: "the repository name, e.g. 'novox/mesh'" } }, + run: async (args) => { + const repo = String(args.repo); + const tags = await registry.listTags(repo); + return { repo, count: tags.length, tags }; + }, + }, + { + name: "registry_delete_image", + description: + "Delete an image tag from the registry (DESTRUCTIVE). Removes the manifest; storage is reclaimed by garbage collection later. Requires confirm: true.", + input: { + repo: { type: "string", description: "the repository name, e.g. 'novox/mesh'" }, + tag: { type: "string", description: "the tag to delete, e.g. 'latest'" }, + confirm: { type: "boolean", description: "must be true to actually delete" }, + }, + run: async (args) => { + const repo = String(args.repo); + const tag = String(args.tag); + if (args.confirm !== true) { + return { deleted: false, reason: "confirm must be true to delete an image" }; + } + const digest = await registry.getManifestDigest(repo, tag); + await registry.deleteManifest(repo, digest); + return { deleted: true, repo, tag, digest, note: "run registry garbage collection to reclaim storage" }; + }, + }, + ]; +} + +// The tools exist only when a registry URL is configured; otherwise registry contributes none +// rather than failing the whole runtime. +registerModuleTools("registry", (env) => { + try { + return getRegistryTools(RegistryClient.fromEnv(env)); + } catch { + return []; + } +}); diff --git a/modules/registry/tsconfig.json b/modules/registry/tsconfig.json new file mode 100644 index 0000000..3677859 --- /dev/null +++ b/modules/registry/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["client.ts", "index.ts", "tools/index.ts"] +} diff --git a/modules/verdaccio/client.ts b/modules/verdaccio/client.ts new file mode 100644 index 0000000..9948fdf --- /dev/null +++ b/modules/verdaccio/client.ts @@ -0,0 +1,81 @@ +// The Verdaccio (npm registry) client — verdaccio's own code, living in the module (novox/hq +// ADR 0044). Both this module's tools and its events entrypoint import it, and nothing outside +// verdaccio does. + +export interface VerdaccioPackage { + name: string; + version?: string; + description?: string; + time?: string; +} + +export interface PackageInfo { + name: string; + latest?: string; + versions: string[]; + description?: string; + modified?: string; +} + +export class VerdaccioClient { + readonly baseUrl: string; + + // A bearer token is optional: package listing and reading are public on most registries, so the + // token is sent only when configured, for a registry that gates reads behind auth. + constructor( + url: string, + private readonly token?: string, + ) { + this.baseUrl = url.replace(/\/+$/, ""); + } + + /** + * Build from the module's resolved environment. The URL is MESH_VERDACCIO_URL (or the local + * port); an optional MESH_VERDACCIO_TOKEN authenticates. Throws when no URL is configured. + */ + static fromEnv(env: NodeJS.ProcessEnv = process.env): VerdaccioClient { + const url = env.MESH_VERDACCIO_URL ?? `http://127.0.0.1:${env.VERDACCIO_PORT ?? "4873"}`; + if (!url) throw new Error("no verdaccio URL — set MESH_VERDACCIO_URL"); + return new VerdaccioClient(url, env.MESH_VERDACCIO_TOKEN); + } + + private async getJson(path: string): Promise { + const res = await fetch(`${this.baseUrl}${path}`, { + headers: { + Accept: "application/json", + ...(this.token ? { Authorization: `Bearer ${this.token}` } : {}), + }, + }); + if (!res.ok) throw new Error(`Verdaccio ${path}: ${res.status} ${await res.text()}`); + return res.json() as Promise; + } + + /** + * Every package the registry hosts, from Verdaccio's own web API — the same list its UI shows. + * Each entry carries the latest version and the time it was last published. + */ + async listPackages(): Promise { + const raw = await this.getJson("/-/verdaccio/data/packages"); + return (raw ?? []).map((p) => ({ + name: p.name, + version: p.version ?? p["dist-tags"]?.latest, + description: p.description, + time: p.time?.modified ?? p.time, + })); + } + + /** + * The full detail of one package — its dist-tags, every published version, and timestamps — + * from the standard npm packument endpoint (`GET /`). + */ + async getPackageInfo(name: string): Promise { + const doc = await this.getJson(`/${encodeURIComponent(name).replace(/%2F/g, "/")}`); + return { + name: doc.name ?? name, + latest: doc["dist-tags"]?.latest, + versions: Object.keys(doc.versions ?? {}), + description: doc.description, + modified: doc.time?.modified, + }; + } +} diff --git a/modules/verdaccio/index.ts b/modules/verdaccio/index.ts new file mode 100644 index 0000000..6eeb889 --- /dev/null +++ b/modules/verdaccio/index.ts @@ -0,0 +1,45 @@ +// verdaccio's events. The tool runtime imports this once the broker is bound. +// +// Emits (novox/hq ADR 0046/0047): +// module.verdaccio.package.published — a new package version was published to the registry +// +// A genuinely useful signal: a package was just published, so anything on the mesh that pins, +// mirrors or announces dependency releases can react without polling the registry. Verdaccio has +// no publish webhook, so the module discovers it by diffing the package list's latest versions. +// +// The polling is deliberately unhurried: a publish a minute late is still the event, whereas +// hammering the registry for immediacy nobody asked for is not. + +import { emit } from "@novox/mesh-sdk/events"; +import { VerdaccioClient } from "./client.js"; + +const verdaccio = VerdaccioClient.fromEnv(); + +// The latest version we have seen per package name. Primed silently on the first look so a registry +// that was already populated when this started does not announce its whole catalog as freshly +// published. +const latest = new Map(); +let primed = false; + +async function pollPackages(): Promise { + const packages = await verdaccio.listPackages(); + for (const pkg of packages) { + if (!pkg.version) continue; + const known = latest.get(pkg.name); + if (known !== pkg.version) { + // A name we have not seen, or a name whose latest version moved — both are a publish. + if (primed) await emit("module.verdaccio.package.published", { name: pkg.name, version: pkg.version }); + latest.set(pkg.name, pkg.version); + } + } + primed = true; +} + +const tick = (fn: () => Promise, everyMs: number): void => { + const run = (): void => void fn().catch((err) => console.error(`[verdaccio] ${err}`)); + setInterval(run, everyMs); + run(); +}; +tick(pollPackages, 60_000); + +console.log("[verdaccio] watching the registry for newly published packages"); diff --git a/modules/verdaccio/module.json b/modules/verdaccio/module.json index 34196f2..f8887d7 100644 --- a/modules/verdaccio/module.json +++ b/modules/verdaccio/module.json @@ -4,6 +4,12 @@ "capabilities": [ "container-runtime" ], + "emits": [ + "module.verdaccio.package.published" + ], + "own-secrets": { + "broker": "/var/lib/verdaccio/broker" + }, "listens": [ { "port": 4873, diff --git a/modules/verdaccio/package.json b/modules/verdaccio/package.json new file mode 100644 index 0000000..644ef27 --- /dev/null +++ b/modules/verdaccio/package.json @@ -0,0 +1,14 @@ +{ + "name": "@novox/module-verdaccio", + "version": "0.1.0", + "description": "verdaccio — private npm registry. Its API client, tools and events live here (novox/hq ADR 0044).", + "type": "module", + "private": true, + "dependencies": { + "@novox/mesh-sdk": "^0.1.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.6.0" + } +} diff --git a/modules/verdaccio/tools/index.ts b/modules/verdaccio/tools/index.ts new file mode 100644 index 0000000..055745d --- /dev/null +++ b/modules/verdaccio/tools/index.ts @@ -0,0 +1,35 @@ +// verdaccio's tools — its own code (novox/hq ADR 0044), importing verdaccio's own client. They +// return structured data; the mesh serves them through the sdk's tool harness. + +import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools"; +import { VerdaccioClient } from "../client.js"; + +export function getVerdaccioTools(verdaccio: VerdaccioClient): ToolDefinition[] { + return [ + { + name: "verdaccio_list_packages", + description: "List every package hosted on the private npm registry, with each one's latest version.", + input: {}, + run: async () => { + const packages = await verdaccio.listPackages(); + return { count: packages.length, packages }; + }, + }, + { + name: "verdaccio_package_info", + description: "Details of one package on the registry: its latest tag, all published versions, and description.", + input: { name: { type: "string", description: "the package name, e.g. '@novox/mesh-sdk'" } }, + run: async (args) => verdaccio.getPackageInfo(String(args.name)), + }, + ]; +} + +// The tools exist only when a registry URL is configured; otherwise verdaccio contributes none +// rather than failing the whole runtime. +registerModuleTools("verdaccio", (env) => { + try { + return getVerdaccioTools(VerdaccioClient.fromEnv(env)); + } catch { + return []; + } +}); diff --git a/modules/verdaccio/tsconfig.json b/modules/verdaccio/tsconfig.json new file mode 100644 index 0000000..3677859 --- /dev/null +++ b/modules/verdaccio/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["client.ts", "index.ts", "tools/index.ts"] +}