From d4791f3d357634daf506fb8f91cc5332a794c421 Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Fri, 20 Jun 2025 14:53:38 -0700 Subject: Move to nodejs --- .ci/ci.json | 2 +- .ci/ci.ts | 139 ++- .eslintrc.js | 35 + .gitignore | 6 + .prettierignore | 72 ++ .prettierrc | 29 + .zed/settings.json | 36 +- Dockerfile | 6 +- deno.json | 4 - index.ts | 37 + mise.toml | 2 +- mod.ts | 35 - model/deno.json | 5 - model/index.ts | 2 + model/job.ts | 67 -- model/job/index.ts | 11 + model/job/jobs.ts | 52 + model/mod.ts | 2 - model/package.json | 27 + model/pipeline.ts | 89 -- model/pipeline/builder.ts | 53 + model/pipeline/impl.ts | 20 + model/pipeline/index.ts | 19 + model/tsconfig.json | 15 + package-lock.json | 1966 ++++++++++++++++++++++++++++++++++ package.json | 38 + server/Dockerfile | 2 +- server/ci.ts | 89 +- server/deno.json | 4 - server/health.ts | 48 +- server/index.ts | 31 + server/job.ts | 185 ---- server/job/index.ts | 2 + server/job/queue.ts | 74 ++ server/job/run_activity.ts | 94 ++ server/mod.ts | 22 - server/package.json | 33 + server/tsconfig.json | 15 + tsconfig.json | 29 + u/deno.json | 5 - u/fn/callable.ts | 8 +- u/fn/either.ts | 180 ++-- u/fn/index.ts | 2 + u/fn/mod.ts | 2 - u/index.ts | 6 + u/leftpadesque/debug.ts | 15 +- u/leftpadesque/index.ts | 4 + u/leftpadesque/memoize.ts | 22 +- u/leftpadesque/mod.ts | 4 - u/leftpadesque/object.ts | 3 +- u/leftpadesque/prepend.ts | 8 +- u/mod.ts | 6 - u/package.json | 47 + u/process/argv.ts | 77 +- u/process/env.ts | 53 +- u/process/index.ts | 4 + u/process/mod.ts | 4 - u/process/run.ts | 102 +- u/process/validate_identifier.ts | 23 +- u/server/activity/fourohfour.ts | 37 +- u/server/activity/health.ts | 108 +- u/server/activity/index.ts | 8 + u/server/activity/mod.ts | 13 - u/server/filter/index.ts | 34 + u/server/filter/json.ts | 92 +- u/server/filter/method.ts | 67 +- u/server/filter/mod.ts | 35 - u/server/index.ts | 7 + u/server/mod.ts | 7 - u/server/request.ts | 74 +- u/server/response.ts | 139 ++- u/trace/index.ts | 5 + u/trace/itrace.ts | 120 +-- u/trace/logger.ts | 169 ++- u/trace/metrics.ts | 229 ++-- u/trace/mod.ts | 5 - u/trace/trace.ts | 117 +- u/trace/util.ts | 76 +- u/tsconfig.json | 15 + worker/deno.json | 4 - worker/executor.ts | 161 ++- worker/index.ts | 2 + worker/mod.ts | 2 - worker/package.json | 28 + worker/scripts/ansible_playbook.ts | 193 ++-- worker/scripts/build_docker_image.ts | 259 ++--- worker/scripts/checkout_ci.ts | 294 +++-- worker/secret.ts | 286 +++-- worker/tsconfig.json | 15 + 89 files changed, 4309 insertions(+), 2264 deletions(-) create mode 100644 .eslintrc.js create mode 100644 .gitignore create mode 100644 .prettierignore create mode 100644 .prettierrc delete mode 100644 deno.json create mode 100755 index.ts delete mode 100755 mod.ts delete mode 100644 model/deno.json create mode 100644 model/index.ts delete mode 100644 model/job.ts create mode 100644 model/job/index.ts create mode 100644 model/job/jobs.ts delete mode 100644 model/mod.ts create mode 100644 model/package.json delete mode 100644 model/pipeline.ts create mode 100644 model/pipeline/builder.ts create mode 100644 model/pipeline/impl.ts create mode 100644 model/pipeline/index.ts create mode 100644 model/tsconfig.json create mode 100644 package-lock.json create mode 100644 package.json delete mode 100644 server/deno.json create mode 100644 server/index.ts delete mode 100644 server/job.ts create mode 100644 server/job/index.ts create mode 100644 server/job/queue.ts create mode 100644 server/job/run_activity.ts delete mode 100644 server/mod.ts create mode 100644 server/package.json create mode 100644 server/tsconfig.json create mode 100644 tsconfig.json delete mode 100644 u/deno.json create mode 100644 u/fn/index.ts delete mode 100644 u/fn/mod.ts create mode 100644 u/index.ts create mode 100644 u/leftpadesque/index.ts delete mode 100644 u/leftpadesque/mod.ts delete mode 100644 u/mod.ts create mode 100644 u/package.json create mode 100644 u/process/index.ts delete mode 100644 u/process/mod.ts create mode 100644 u/server/activity/index.ts delete mode 100644 u/server/activity/mod.ts create mode 100644 u/server/filter/index.ts delete mode 100644 u/server/filter/mod.ts create mode 100644 u/server/index.ts delete mode 100644 u/server/mod.ts create mode 100644 u/trace/index.ts delete mode 100644 u/trace/mod.ts create mode 100644 u/tsconfig.json delete mode 100644 worker/deno.json create mode 100644 worker/index.ts delete mode 100644 worker/mod.ts create mode 100644 worker/package.json create mode 100644 worker/tsconfig.json diff --git a/.ci/ci.json b/.ci/ci.json index dc29f8d..e10df80 100644 --- a/.ci/ci.json +++ b/.ci/ci.json @@ -1,3 +1,3 @@ { - "pipeline": ".ci/ci.ts" + "workflow": ".ci/ci.js" } diff --git a/.ci/ci.ts b/.ci/ci.ts index 89bad35..7f06b26 100644 --- a/.ci/ci.ts +++ b/.ci/ci.ts @@ -1,84 +1,83 @@ -#!/usr/bin/env -S deno run --allow-env +#!/usr/bin/env ts-node import { - AnsiblePlaybookJob, - BuildDockerImageJob, - DefaultGitHookPipelineBuilder, - FetchCodeJob, -} from "@emprespresso/ci_model"; + AnsiblePlaybookJob, + BuildDockerImageJob, + DefaultGitHookPipelineBuilder, + FetchCodeJob, +} from '../model/index.js'; -const REGISTRY = "oci.liz.coffee"; -const NAMESPACE = "emprespresso"; -const IMG = "ci"; -const REMOTE = "ssh://src.liz.coffee:2222"; +const REGISTRY = 'oci.liz.coffee'; +const NAMESPACE = 'emprespresso'; +const IMG = 'ci'; +const REMOTE = 'ssh://src.liz.coffee:2222'; const getPipeline = () => { - const gitHookPipeline = new DefaultGitHookPipelineBuilder(); - const branch = gitHookPipeline.getBranch(); - if (!branch) return gitHookPipeline.build(); + const gitHookPipeline = new DefaultGitHookPipelineBuilder(); + const branch = gitHookPipeline.getBranch(); + if (!branch) return gitHookPipeline.build(); - const commonBuildArgs = { - registry: REGISTRY, - namespace: NAMESPACE, - imageTag: branch, - }; + const commonBuildArgs = { + registry: REGISTRY, + namespace: NAMESPACE, + imageTag: branch, + }; - const baseCiPackageBuild: BuildDockerImageJob = { - type: "build_docker_image.ts", - arguments: { - ...commonBuildArgs, - context: gitHookPipeline.getSourceDestination(), - repository: IMG + "_base", - buildTarget: IMG + "_base", - dockerfile: "Dockerfile", - }, - }; - gitHookPipeline.addStage({ - parallelJobs: [baseCiPackageBuild], - }); + const baseCiPackageBuild: BuildDockerImageJob = { + type: 'build_docker_image.ts', + arguments: { + ...commonBuildArgs, + context: gitHookPipeline.getSourceDestination(), + repository: IMG + '_base', + buildTarget: IMG + '_base', + dockerfile: 'Dockerfile', + }, + }; + gitHookPipeline.addStage({ + parallelJobs: [baseCiPackageBuild], + }); - const subPackages = ["worker", "hooks"].map((_package) => ({ - type: "build_docker_image.ts", - arguments: { - ...commonBuildArgs, - repository: `${IMG}_${_package}`, - buildTarget: _package, - dockerfile: `${_package}/Dockerfile`, - }, - })); - gitHookPipeline.addStage({ - parallelJobs: subPackages, - }); + const subPackages = ['worker', 'hooks'].map((_package) => ({ + type: 'build_docker_image.ts', + arguments: { + ...commonBuildArgs, + repository: `${IMG}_${_package}`, + buildTarget: _package, + dockerfile: `${_package}/Dockerfile`, + }, + })); + gitHookPipeline.addStage({ + parallelJobs: subPackages, + }); - const isRelease = branch === "release"; - if (!isRelease) { - return gitHookPipeline.build(); - } + const isRelease = branch === 'release'; + if (!isRelease) { + return gitHookPipeline.build(); + } - const fetchAnsibleCode: FetchCodeJob = { - type: "fetch_code.ts", - arguments: { - remoteUrl: `${REMOTE}/infra`, - checkout: "main", - path: "infra", - }, - }; - const thenDeploy: AnsiblePlaybookJob = { - type: "ansible_playbook.ts", - arguments: { - path: "infra", - playbooks: "playbooks/ci.yml", - }, - }; - [fetchAnsibleCode, thenDeploy].forEach((deploymentStage) => - gitHookPipeline.addStage({ parallelJobs: [deploymentStage] }), - ); + const fetchAnsibleCode: FetchCodeJob = { + type: 'fetch_code.ts', + arguments: { + remoteUrl: `${REMOTE}/infra`, + checkout: 'main', + path: 'infra', + }, + }; + const thenDeploy: AnsiblePlaybookJob = { + type: 'ansible_playbook.ts', + arguments: { + path: 'infra', + playbooks: 'playbooks/ci.yml', + }, + }; + [fetchAnsibleCode, thenDeploy].forEach((deploymentStage) => + gitHookPipeline.addStage({ parallelJobs: [deploymentStage] }), + ); - return gitHookPipeline.build(); + return gitHookPipeline.build(); }; -if (import.meta.main) { - const encoder = new TextEncoder(); - const data = encoder.encode(getPipeline().serialize()); - await Deno.stdout.write(data); +if (import.meta.url === `file://${process.argv[1]}`) { + const data = getPipeline().serialize(); + process.stdout.write(data); } diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..a4878e3 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,35 @@ +module.exports = { + root: true, + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint', 'prettier'], + extends: [ + 'eslint:recommended', + '@typescript-eslint/recommended', + '@typescript-eslint/recommended-requiring-type-checking', + 'prettier', + 'plugin:prettier/recommended', + ], + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + project: './tsconfig.json', + }, + env: { + node: true, + es2022: true, + }, + rules: { + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-non-null-assertion': 'warn', + '@typescript-eslint/prefer-nullish-coalescing': 'error', + '@typescript-eslint/prefer-optional-chain': 'error', + '@typescript-eslint/no-floating-promises': 'error', + 'no-console': 'warn', + 'prefer-const': 'error', + 'prettier/prettier': 'error', + }, + ignorePatterns: ['dist/', 'node_modules/', '*.js', '*.d.ts'], +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c4d9f6d --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +**/.env +**/node_modules +**/dist + +.DS_Store +Thumbs.db diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..23da47f --- /dev/null +++ b/.prettierignore @@ -0,0 +1,72 @@ +# Dependencies +node_modules/ +package-lock.json +yarn.lock +pnpm-lock.yaml + +# Build outputs +dist/ +build/ +.next/ +.nuxt/ +.vuepress/dist/ +.serverless/ +.vercel/ + +# Logs +*.log +logs/ + +# Runtime data +pids/ +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# Generated files +*.d.ts +*.tsbuildinfo + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# Temporary files +*.tmp +*.temp + +# Docker +Dockerfile* +.dockerignore + +# Git +.git/ +.gitignore + +# CI/CD +.github/ +.gitlab-ci.yml + +# Config files that might have specific formatting +.env* +*.conf +*.config.js +*.config.ts + +# Markdown files that might be auto-generated +CHANGELOG.md \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..70bc078 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,29 @@ +{ + "semi": true, + "trailingComma": "all", + "singleQuote": true, + "printWidth": 120, + "tabWidth": 4, + "useTabs": false, + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "always", + "endOfLine": "lf", + "quoteProps": "as-needed", + "jsxSingleQuote": true, + "overrides": [ + { + "files": "*.json", + "options": { + "printWidth": 120 + } + }, + { + "files": "*.md", + "options": { + "printWidth": 100, + "proseWrap": "preserve" + } + } + ] +} diff --git a/.zed/settings.json b/.zed/settings.json index 78b2d5d..77c7e9a 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -1,22 +1,24 @@ { - "formatter": "prettier", - "lsp": { - "deno": { - "settings": { - "deno": { - "enable": true + "formatter": "prettier", + "lsp": { + "typescript-language-server": { + "settings": { + "typescript": { + "preferences": { + "includePackageJsonAutoImports": "on" + } + } + } } - } - } - }, - "languages": { - "TypeScript": { - "language_servers": ["deno", "!typescript-language-server"], - "formatter": "prettier" }, - "TSX": { - "language_servers": ["deno", "!typescript-language-server"], - "formatter": "prettier" + "languages": { + "TypeScript": { + "language_servers": ["typescript-language-server"], + "formatter": "prettier" + }, + "TSX": { + "language_servers": ["typescript-language-server"], + "formatter": "prettier" + } } - } } diff --git a/Dockerfile b/Dockerfile index 33c6062..5c87c65 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,7 +25,7 @@ RUN cmake -B /opt/laminar/build -S /opt/laminar/src -G Ninja \ # -- -- # -- -- -FROM denoland/deno:debian AS ci_base +FROM node:22-slim AS ci_base RUN apt-get update -yqq && apt-get install -yqq libcapnp-0.9.2 \ libsqlite3-0 zlib1g curl bash @@ -38,7 +38,11 @@ COPY --from=laminar_bin /usr/share/bash-completion/completions/laminarc /usr/sha COPY --from=laminar_bin /usr/share/zsh/site-functions/_laminarc /usr/share/zsh/site-functions/_laminarc WORKDIR /app +COPY package*.json ./ +RUN npm ci --only=production + COPY . /app +RUN npm run build ENTRYPOINT [ "/bin/bash", "-c" ] # -- -- diff --git a/deno.json b/deno.json deleted file mode 100644 index adeae7b..0000000 --- a/deno.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "package": "@emprespresso/ci", - "workspace": ["./*"] -} diff --git a/index.ts b/index.ts new file mode 100755 index 0000000..a9defca --- /dev/null +++ b/index.ts @@ -0,0 +1,37 @@ +#!/usr/bin/env node + +import { argv, IEither, Either } from '@emprespresso/pengueno'; +import { runServer } from '@emprespresso/ci_server'; + +const main = (_argv = process.argv.slice(2)): Promise> => + argv( + ['--run-server', '--port', '--host'], + { + '--run-server': { absent: false, unspecified: true, present: () => true }, + '--port': { absent: 9000, present: (port) => parseInt(port) }, + '--host': { absent: '0.0.0.0', present: (host) => host }, + }, + _argv, + ) + .mapRight((args) => ({ + server_mode: args['--run-server'], + port: args['--port'], + host: args['--host'], + })) + .flatMapAsync((runConfig) => { + if (runConfig.server_mode) { + return runServer(runConfig.port, runConfig.host); + } + return Promise.resolve(Either.right(0)); + }); + +if (process.argv[1] === import.meta.filename) { + await main().then((eitherDone) => + eitherDone.fold(({ isLeft, value }) => { + if (!isLeft) return; + + console.error(`failed to start`, value); + process.exit(1); + }), + ); +} diff --git a/mise.toml b/mise.toml index ac7a209..24429f1 100644 --- a/mise.toml +++ b/mise.toml @@ -1,2 +1,2 @@ [tools] -deno = "latest" +node = "22.16.0" diff --git a/mod.ts b/mod.ts deleted file mode 100755 index b43fff3..0000000 --- a/mod.ts +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env -S deno run --allow-env --allow-net --allow-run - -import { argv, IEither, Either } from "@emprespresso/pengueno"; -import { runServer } from "@emprespresso/ci_server"; - -const main = ( - _argv = Deno.args, -): Promise> => - argv(["--run-server", "--port", "--host"], { - "--run-server": { absent: false, unspecified: true, present: () => true }, - "--port": { absent: 9000, present: (port) => parseInt(port) }, - "--host": { absent: "0.0.0.0", present: (host) => host }, - }, _argv) - .mapRight((args) => ({ - server_mode: args["--run-server"], - port: args["--port"], - host: args["--host"], - })) - .flatMapAsync((runConfig) => { - if (runConfig.server_mode) { - return runServer(runConfig.port, runConfig.host); - } - return Promise.resolve(Either.right(0)); - }); - -if (import.meta.main) { - await main().then((eitherDone) => - eitherDone.fold(({ isLeft, value }) => { - if (!isLeft) return; - - console.error(`Failed to start`, value); - Deno.exit(1); - }), - ); -} diff --git a/model/deno.json b/model/deno.json deleted file mode 100644 index 5f5dacf..0000000 --- a/model/deno.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "@emprespresso/ci_model", - "version": "0.1.0", - "exports": "./mod.ts" -} diff --git a/model/index.ts b/model/index.ts new file mode 100644 index 0000000..094c693 --- /dev/null +++ b/model/index.ts @@ -0,0 +1,2 @@ +export * from './job'; +export * from './pipeline'; diff --git a/model/job.ts b/model/job.ts deleted file mode 100644 index 187ed56..0000000 --- a/model/job.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { isObject } from "@emprespresso/pengueno"; - -export type JobArgT = Record; -export interface Job { - readonly type: string; - readonly arguments: JobArgT; -} -export const isJob = (j: unknown): j is Job => - !!( - isObject(j) && - "arguments" in j && - isObject(j.arguments) && - "type" in j && - typeof j.type === "string" && - j - ); - -export interface FetchCodeJobProps extends JobArgT { - readonly remoteUrl: string; - readonly checkout: string; - readonly path: string; -} - -export interface FetchCodeJob { - readonly type: "fetch_code.ts"; - readonly arguments: FetchCodeJobProps; -} - -export interface BuildDockerImageJobProps extends JobArgT { - readonly registry: string; - readonly namespace: string; - readonly repository: string; - readonly imageTag: string; - - readonly context: string; - readonly dockerfile: string; - readonly buildTarget: string; -} - -export interface BuildDockerImageJob extends Job { - readonly type: "build_docker_image.ts"; - readonly arguments: BuildDockerImageJobProps; -} - -export interface AnsiblePlaybookJobProps extends JobArgT { - readonly path: string; - readonly playbooks: string; -} - -export interface AnsiblePlaybookJob extends Job { - readonly type: "ansible_playbook.ts"; - readonly arguments: AnsiblePlaybookJobProps; -} - -export interface CheckoutCiJobProps extends JobArgT { - readonly remote: string; - readonly refname: string; - readonly rev: string; - - readonly run: string; - readonly returnPath: string; -} - -export interface CheckoutCiJob extends Job { - readonly type: "checkout_ci.ts"; - readonly arguments: CheckoutCiJobProps; -} diff --git a/model/job/index.ts b/model/job/index.ts new file mode 100644 index 0000000..78f69d6 --- /dev/null +++ b/model/job/index.ts @@ -0,0 +1,11 @@ +import { isObject } from '@emprespresso/pengueno'; + +export type JobArgT = Record; +export interface Job { + readonly type: string; + readonly arguments: JobArgT; +} +export const isJob = (j: unknown): j is Job => + !!(isObject(j) && 'arguments' in j && isObject(j.arguments) && 'type' in j && typeof j.type === 'string' && j); + +export * from './jobs'; diff --git a/model/job/jobs.ts b/model/job/jobs.ts new file mode 100644 index 0000000..e201c4d --- /dev/null +++ b/model/job/jobs.ts @@ -0,0 +1,52 @@ +import { Job, JobArgT } from '.'; + +export interface FetchCodeJobProps extends JobArgT { + readonly remoteUrl: string; + readonly checkout: string; + readonly path: string; +} + +export interface FetchCodeJob { + readonly type: 'fetch_code.ts'; + readonly arguments: FetchCodeJobProps; +} + +export interface BuildDockerImageJobProps extends JobArgT { + readonly registry: string; + readonly namespace: string; + readonly repository: string; + readonly imageTag: string; + + readonly context: string; + readonly dockerfile: string; + readonly buildTarget: string; +} + +export interface BuildDockerImageJob extends Job { + readonly type: 'build_docker_image.ts'; + readonly arguments: BuildDockerImageJobProps; +} + +export interface AnsiblePlaybookJobProps extends JobArgT { + readonly path: string; + readonly playbooks: string; +} + +export interface AnsiblePlaybookJob extends Job { + readonly type: 'ansible_playbook.ts'; + readonly arguments: AnsiblePlaybookJobProps; +} + +export interface CheckoutCiJobProps extends JobArgT { + readonly remote: string; + readonly refname: string; + readonly rev: string; + + readonly run: string; + readonly returnPath: string; +} + +export interface CheckoutCiJob extends Job { + readonly type: 'checkout_ci.ts'; + readonly arguments: CheckoutCiJobProps; +} diff --git a/model/mod.ts b/model/mod.ts deleted file mode 100644 index 944ab7d..0000000 --- a/model/mod.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./job.ts"; -export * from "./pipeline.ts"; diff --git a/model/package.json b/model/package.json new file mode 100644 index 0000000..23cfd52 --- /dev/null +++ b/model/package.json @@ -0,0 +1,27 @@ +{ + "name": "@emprespresso/ci_model", + "version": "0.1.0", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "clean": "rm -rf dist", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "@emprespresso/pengueno": "*" + }, + "files": [ + "dist/**/*", + "package.json", + "README.md" + ] +} diff --git a/model/pipeline.ts b/model/pipeline.ts deleted file mode 100644 index edc8337..0000000 --- a/model/pipeline.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { Either, type IEither, isObject } from "@emprespresso/pengueno"; -import { type FetchCodeJob, isJob, type Job } from "@emprespresso/ci_model"; - -export interface PipelineStage { - readonly parallelJobs: Array; -} -export const isPipelineStage = (t: unknown): t is PipelineStage => - isObject(t) && - "parallelJobs" in t && - Array.isArray(t.parallelJobs) && - t.parallelJobs.every((j) => isJob(j)); - -export interface Pipeline { - readonly serialJobs: Array; - serialize(): string; -} -export const isPipeline = (t: unknown): t is Pipeline => - isObject(t) && - "serialJobs" in t && - Array.isArray(t.serialJobs) && - t.serialJobs.every((p) => isPipelineStage(p)); - -export interface PipelineBuilder { - addStage(stage: PipelineStage): PipelineBuilder; - build(): Pipeline; -} - -export class PipelineImpl implements Pipeline { - constructor(public readonly serialJobs: Array) {} - - public serialize() { - return JSON.stringify(this.serialJobs); - } - - public static from(s: string): IEither { - return Either.fromFailable(() => JSON.parse(s)) - .flatMap((eitherPipelineJson) => - isPipeline(eitherPipelineJson) - ? Either.right(eitherPipelineJson) - : Either.left(new Error("oh noes D: its a bad pipewine :((")), - ) - .mapRight((pipeline) => new PipelineImpl(pipeline.serialJobs)); - } -} - -abstract class BasePipelineBuilder implements PipelineBuilder { - protected readonly stages: Array = []; - - public addStage(stage: PipelineStage): PipelineBuilder { - this.stages.push(stage); - return this; - } - - public build() { - return new PipelineImpl(this.stages); - } -} - -export class DefaultGitHookPipelineBuilder extends BasePipelineBuilder { - constructor( - private readonly remoteUrl = Deno.env.get("remote")!, - rev = Deno.env.get("rev")!, - private readonly ref = Deno.env.get("ref")!, - ) { - super(); - - this.addStage({ - parallelJobs: [ - { - type: "fetch_code", - arguments: { - remoteUrl, - checkout: rev, - path: this.getSourceDestination(), - }, - }, - ], - }); - } - - public getSourceDestination() { - return this.remoteUrl.split("/").at(-1) ?? "src"; - } - - public getBranch(): string | undefined { - const branchRefPrefix = "refs/heads/"; - return this.ref.split(branchRefPrefix).at(1); - } -} diff --git a/model/pipeline/builder.ts b/model/pipeline/builder.ts new file mode 100644 index 0000000..e95e89c --- /dev/null +++ b/model/pipeline/builder.ts @@ -0,0 +1,53 @@ +import { Pipeline, PipelineStage } from '.'; +import { FetchCodeJob } from '../job'; +import { PipelineImpl } from './impl'; + +export interface PipelineBuilder { + addStage(stage: PipelineStage): PipelineBuilder; + build(): Pipeline; +} + +export abstract class BasePipelineBuilder implements PipelineBuilder { + protected readonly stages: Array = []; + + public addStage(stage: PipelineStage): PipelineBuilder { + this.stages.push(stage); + return this; + } + + public build() { + return new PipelineImpl(this.stages); + } +} + +export class DefaultGitHookPipelineBuilder extends BasePipelineBuilder { + constructor( + private readonly remoteUrl = process.env.remote!, + rev = process.env.rev!, + private readonly ref = process.env.ref!, + ) { + super(); + + this.addStage({ + parallelJobs: [ + { + type: 'fetch_code.ts', + arguments: { + remoteUrl, + checkout: rev, + path: this.getSourceDestination(), + }, + }, + ], + }); + } + + public getSourceDestination() { + return this.remoteUrl.split('/').at(-1) ?? 'src'; + } + + public getBranch(): string | undefined { + const branchRefPrefix = 'refs/heads/'; + return this.ref.split(branchRefPrefix).at(1); + } +} diff --git a/model/pipeline/impl.ts b/model/pipeline/impl.ts new file mode 100644 index 0000000..2e08d6e --- /dev/null +++ b/model/pipeline/impl.ts @@ -0,0 +1,20 @@ +import { Either, IEither } from '@emprespresso/pengueno'; +import { isPipeline, Pipeline, PipelineStage } from '.'; + +export class PipelineImpl implements Pipeline { + constructor(public readonly serialJobs: Array) {} + + public serialize() { + return JSON.stringify(this.serialJobs); + } + + public static from(s: string): IEither { + return Either.fromFailable(() => JSON.parse(s)) + .flatMap((eitherPipelineJson) => + isPipeline(eitherPipelineJson) + ? Either.right(eitherPipelineJson) + : Either.left(new Error('oh noes D: its a bad pipewine :((')), + ) + .mapRight((pipeline) => new PipelineImpl(pipeline.serialJobs)); + } +} diff --git a/model/pipeline/index.ts b/model/pipeline/index.ts new file mode 100644 index 0000000..adf902b --- /dev/null +++ b/model/pipeline/index.ts @@ -0,0 +1,19 @@ +import { isObject } from '@emprespresso/pengueno'; +import { isJob, Job } from '../job'; + +export interface PipelineStage { + readonly parallelJobs: Array; +} +export const isPipelineStage = (t: unknown): t is PipelineStage => + isObject(t) && 'parallelJobs' in t && Array.isArray(t.parallelJobs) && t.parallelJobs.every((j) => isJob(j)); + +export const isPipeline = (t: unknown): t is Pipeline => + isObject(t) && 'serialJobs' in t && Array.isArray(t.serialJobs) && t.serialJobs.every((p) => isPipelineStage(p)); + +export interface Pipeline { + readonly serialJobs: Array; + serialize(): string; +} + +export * from './builder'; +export * from './impl'; diff --git a/model/tsconfig.json b/model/tsconfig.json new file mode 100644 index 0000000..7ad21ad --- /dev/null +++ b/model/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./", + "composite": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "noEmit": false + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"], + "references": [{ "path": "../u" }] +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d6b31c9 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1966 @@ +{ + "name": "@emprespresso/ci", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@emprespresso/ci", + "version": "0.1.0", + "workspaces": [ + "u", + "model", + "server", + "worker", + ".ci" + ], + "devDependencies": { + "@types/node": "^24.0.3", + "@typescript-eslint/eslint-plugin": "^8.34.1", + "@typescript-eslint/parser": "^8.34.1", + "eslint": "^8.34.1", + "eslint-config-prettier": "^10.1.5", + "eslint-plugin-prettier": "^5.5.0", + "prettier": "^3.5.3", + "typescript": "^5.8.3" + }, + "engines": { + "node": ">=22.16.0", + "npm": ">=10.0.0" + } + }, + "model": { + "name": "@emprespresso/ci_model", + "version": "0.1.0", + "dependencies": { + "@emprespresso/pengueno": "*" + } + }, + "node_modules/@emprespresso/ci_model": { + "resolved": "model", + "link": true + }, + "node_modules/@emprespresso/ci_server": { + "resolved": "server", + "link": true + }, + "node_modules/@emprespresso/ci_worker": { + "resolved": "worker", + "link": true + }, + "node_modules/@emprespresso/pengueno": { + "resolved": "u", + "link": true + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@hono/node-server": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.14.4.tgz", + "integrity": "sha512-DnxpshhYewr2q9ZN8ez/M5mmc3sucr8CT1sIgIy1bkeUXut9XWDkqHoFHRhWIQgkYnKpVRxunyhK7WzpJeJ6qQ==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz", + "integrity": "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@types/node": { + "version": "24.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.3.tgz", + "integrity": "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz", + "integrity": "sha512-STXcN6ebF6li4PxwNeFnqF8/2BNDvBupf2OPx2yWNzr6mKNGF7q49VM00Pz5FaomJyqvbXpY6PhO+T9w139YEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.34.1", + "@typescript-eslint/type-utils": "8.34.1", + "@typescript-eslint/utils": "8.34.1", + "@typescript-eslint/visitor-keys": "8.34.1", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.34.1", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.1.tgz", + "integrity": "sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.34.1", + "@typescript-eslint/types": "8.34.1", + "@typescript-eslint/typescript-estree": "8.34.1", + "@typescript-eslint/visitor-keys": "8.34.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.1.tgz", + "integrity": "sha512-nuHlOmFZfuRwLJKDGQOVc0xnQrAmuq1Mj/ISou5044y1ajGNp2BNliIqp7F2LPQ5sForz8lempMFCovfeS1XoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.34.1", + "@typescript-eslint/types": "^8.34.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.1.tgz", + "integrity": "sha512-beu6o6QY4hJAgL1E8RaXNC071G4Kso2MGmJskCFQhRhg8VOH/FDbC8soP8NHN7e/Hdphwp8G8cE6OBzC8o41ZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.34.1", + "@typescript-eslint/visitor-keys": "8.34.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.1.tgz", + "integrity": "sha512-K4Sjdo4/xF9NEeA2khOb7Y5nY6NSXBnod87uniVYW9kHP+hNlDV8trUSFeynA2uxWam4gIWgWoygPrv9VMWrYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.1.tgz", + "integrity": "sha512-Tv7tCCr6e5m8hP4+xFugcrwTOucB8lshffJ6zf1mF1TbU67R+ntCc6DzLNKM+s/uzDyv8gLq7tufaAhIBYeV8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.34.1", + "@typescript-eslint/utils": "8.34.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.1.tgz", + "integrity": "sha512-rjLVbmE7HR18kDsjNIZQHxmv9RZwlgzavryL5Lnj2ujIRTeXlKtILHgRNmQ3j4daw7zd+mQgy+uyt6Zo6I0IGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.1.tgz", + "integrity": "sha512-rjCNqqYPuMUF5ODD+hWBNmOitjBWghkGKJg6hiCHzUvXRy6rK22Jd3rwbP2Xi+R7oYVvIKhokHVhH41BxPV5mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.34.1", + "@typescript-eslint/tsconfig-utils": "8.34.1", + "@typescript-eslint/types": "8.34.1", + "@typescript-eslint/visitor-keys": "8.34.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.1.tgz", + "integrity": "sha512-mqOwUdZ3KjtGk7xJJnLbHxTuWVn3GO2WZZuM+Slhkun4+qthLdXx32C8xIXbO1kfCECb3jIs3eoxK3eryk7aoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.34.1", + "@typescript-eslint/types": "8.34.1", + "@typescript-eslint/typescript-estree": "8.34.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.1.tgz", + "integrity": "sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.34.1", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.5", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz", + "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.0.tgz", + "integrity": "sha512-8qsOYwkkGrahrgoUv76NZi23koqXOGiiEzXMrT8Q7VcYaUISR+5MorIUxfWqYXN0fN/31WbSrxCxFkVQ43wwrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hono": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.8.1.tgz", + "integrity": "sha512-ErA2ifywnSmcnB5XDuFqGDfXJ9xuAJR2C/8cZAk6vDaOCzofB8eNlha/wZWIiamREzWk94S9Z7wHsnKQHn7Niw==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "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.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/synckit": { + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz", + "integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.4" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "server": { + "name": "@emprespresso/ci_server", + "version": "0.1.0", + "dependencies": { + "@emprespresso/ci_model": "*", + "@emprespresso/pengueno": "*", + "@hono/node-server": "^1.14.0", + "hono": "^4.8.0" + } + }, + "u": { + "name": "@emprespresso/pengueno", + "version": "0.1.0", + "dependencies": { + "hono": "^4.8.0" + } + }, + "worker": { + "name": "@emprespresso/ci_worker", + "version": "0.1.0", + "dependencies": { + "@emprespresso/ci_model": "*", + "@emprespresso/pengueno": "*" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..8107bdf --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "@emprespresso/ci", + "version": "0.1.0", + "private": true, + "type": "module", + "workspaces": [ + "u", + "model", + "server", + "worker", + ".ci" + ], + "scripts": { + "build": "npm run build --workspaces --if-present && tsc", + "dev": "npm run dev --workspaces --if-present", + "lint": "eslint . --ext .ts,.js", + "lint:fix": "eslint . --ext .ts,.js --fix", + "format": "prettier --write .", + "format:check": "prettier --check .", + "type-check": "tsc --noEmit", + "clean": "npm run clean --workspaces --if-present && rm -rf dist", + "start": "node dist/index.js" + }, + "devDependencies": { + "@types/node": "^24.0.3", + "@typescript-eslint/eslint-plugin": "^8.34.1", + "@typescript-eslint/parser": "^8.34.1", + "eslint": "^8.34.1", + "eslint-config-prettier": "^10.1.5", + "eslint-plugin-prettier": "^5.5.0", + "prettier": "^3.5.3", + "typescript": "^5.8.3" + }, + "engines": { + "node": ">=22.16.0", + "npm": ">=10.0.0" + } +} diff --git a/server/Dockerfile b/server/Dockerfile index b610907..1b9b0ed 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,5 +1,5 @@ # -- -- FROM oci.liz.coffee/emprespresso/ci_base:release AS server -CMD [ "/app/mod.ts --run-server --port=9000" ] +CMD [ "node", "/app/dist/index.js", "--run-server", "--port=9000" ] # -- -- diff --git a/server/ci.ts b/server/ci.ts index f8d4a17..f57c426 100644 --- a/server/ci.ts +++ b/server/ci.ts @@ -1,56 +1,47 @@ import { - FourOhFourActivityImpl, - getRequiredEnv, - HealthCheckActivityImpl, - type HealthChecker, - type IFourOhFourActivity, - type IHealthCheckActivity, - type ITraceable, - PenguenoRequest, - type ServerTrace, - TraceUtil, -} from "@emprespresso/pengueno"; -import type { Job } from "@emprespresso/ci_model"; -import { - healthCheck as _healthCheck, - type IJobHookActivity, - type IJobQueuer, - JobHookActivityImpl, - LaminarJobQueuer, -} from "@emprespresso/ci_server"; + FourOhFourActivityImpl, + getRequiredEnv, + HealthCheckActivityImpl, + type HealthChecker, + type IFourOhFourActivity, + type IHealthCheckActivity, + type ITraceable, + PenguenoRequest, + type ServerTrace, + TraceUtil, +} from '@emprespresso/pengueno'; +import type { Job } from '@emprespresso/ci_model'; +import { type IJobHookActivity, type IJobQueuer, JobHookActivityImpl, LaminarJobQueuer } from './job'; +import { healthCheck as _healthCheck } from '.'; + +export const DEFAULT_CI_SERVER = 'https://ci.liz.coffee'; export class CiHookServer { - constructor( - healthCheck: HealthChecker = _healthCheck, - jobQueuer: IJobQueuer> = new LaminarJobQueuer( - getRequiredEnv("LAMINAR_URL").fold(({ isLeft, value }) => - isLeft ? "https://ci.liz.coffee" : value, - ), - ), - private readonly healthCheckActivity: IHealthCheckActivity = new HealthCheckActivityImpl( - healthCheck, - ), - private readonly jobHookActivity: IJobHookActivity = new JobHookActivityImpl( - jobQueuer, - ), - private readonly fourOhFourActivity: IFourOhFourActivity = new FourOhFourActivityImpl(), - ) {} + constructor( + healthCheck: HealthChecker = _healthCheck, + jobQueuer: IJobQueuer> = new LaminarJobQueuer( + getRequiredEnv('LAMINAR_URL').fold(({ isLeft, value }) => (isLeft ? DEFAULT_CI_SERVER : value)), + ), + private readonly healthCheckActivity: IHealthCheckActivity = new HealthCheckActivityImpl(healthCheck), + private readonly jobHookActivity: IJobHookActivity = new JobHookActivityImpl(jobQueuer), + private readonly fourOhFourActivity: IFourOhFourActivity = new FourOhFourActivityImpl(), + ) {} - private route(req: ITraceable) { - const url = new URL(req.get().url); - if (url.pathname === "/health") { - return this.healthCheckActivity.checkHealth(req); + private route(req: ITraceable) { + const url = new URL(req.get().url); + if (url.pathname === '/health') { + return this.healthCheckActivity.checkHealth(req); + } + if (url.pathname === '/job') { + return this.jobHookActivity.processHook(req); + } + return this.fourOhFourActivity.fourOhFour(req); } - if (url.pathname === "/job") { - return this.jobHookActivity.processHook(req); - } - return this.fourOhFourActivity.fourOhFour(req); - } - public serve(req: Request): Promise { - return PenguenoRequest.from(req) - .bimap(TraceUtil.withClassTrace(this)) - .map((req) => this.route(req)) - .get(); - } + public serve(req: Request): Promise { + return PenguenoRequest.from(req) + .bimap(TraceUtil.withClassTrace(this)) + .map((req) => this.route(req)) + .get(); + } } diff --git a/server/deno.json b/server/deno.json deleted file mode 100644 index c86c9a7..0000000 --- a/server/deno.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "@emprespresso/ci_server", - "exports": "./mod.ts" -} diff --git a/server/health.ts b/server/health.ts index e69077b..8435865 100644 --- a/server/health.ts +++ b/server/health.ts @@ -1,32 +1,24 @@ import { - getRequiredEnv, - getStdout, - type HealthChecker, - type HealthCheckInput, - HealthCheckOutput, - type IEither, - type ITraceable, - type ServerTrace, - TraceUtil, -} from "@emprespresso/pengueno"; + getRequiredEnv, + getStdout, + type HealthChecker, + type HealthCheckInput, + HealthCheckOutput, + type IEither, + type ITraceable, + type ServerTrace, + TraceUtil, +} from '@emprespresso/pengueno'; export const healthCheck: HealthChecker = ( - input: ITraceable, + input: ITraceable, ): Promise> => - input - .bimap(TraceUtil.withFunctionTrace(healthCheck)) - .move(getRequiredEnv("LAMINAR_HOST")) - // ensure LAMINAR_HOST is propagated to getStdout for other procedures - .map((tEitherEnv) => - tEitherEnv - .get() - .flatMapAsync((_hasEnv) => - getStdout(tEitherEnv.move(["laminarc", "show-jobs"])), - ), - ) - .map( - TraceUtil.promiseify((stdout) => - stdout.get().moveRight(HealthCheckOutput.YAASSSLAYQUEEN), - ), - ) - .get(); + input + .bimap(TraceUtil.withFunctionTrace(healthCheck)) + .move(getRequiredEnv('LAMINAR_HOST')) + // ensure LAMINAR_HOST is propagated to getStdout for other procedures + .map((tEitherEnv) => + tEitherEnv.get().flatMapAsync((_hasEnv) => getStdout(tEitherEnv.move(['laminarc', 'show-jobs']))), + ) + .map(TraceUtil.promiseify((stdout) => stdout.get().moveRight(HealthCheckOutput.YAASSSLAYQUEEN))) + .get(); diff --git a/server/index.ts b/server/index.ts new file mode 100644 index 0000000..c33b43e --- /dev/null +++ b/server/index.ts @@ -0,0 +1,31 @@ +#!/usr/bin/env node + +export * from './job'; +export * from './ci'; +export * from './health'; + +import { CiHookServer } from '.'; +import { Either, type IEither } from '@emprespresso/pengueno'; +import { serve } from '@hono/node-server'; +import { Hono } from 'hono'; + +const server = new CiHookServer(); + +const neverEndingPromise = new Promise>(() => {}); +export const runServer = (port: number, host: string): Promise> => + Either.fromFailable(() => { + const app = new Hono(); + + app.all('*', async (c) => { + const response = await server.serve(c.req.raw); + return response; + }); + + serve({ + fetch: app.fetch, + port, + hostname: host, + }); + + console.log(`server running on http://${host}:${port} :D`); + }).flatMapAsync(() => neverEndingPromise); diff --git a/server/job.ts b/server/job.ts deleted file mode 100644 index 620a083..0000000 --- a/server/job.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { - getStdout, - type Mapper, - memoize, - Either, - ErrorSource, - type IActivity, - type IEither, - type ITraceable, - jsonModel, - JsonResponse, - LogLevel, - Metric, - PenguenoError, - type PenguenoRequest, - type ServerTrace, - TraceUtil, - validateExecutionEntries, -} from "@emprespresso/pengueno"; -import { isJob, type Job } from "@emprespresso/ci_model"; - -// -- -- -const wellFormedJobMetric = Metric.fromName("Job.WellFormed"); - -const jobJsonTransformer = ( - j: ITraceable, -): IEither => - j - .bimap(TraceUtil.withMetricTrace(wellFormedJobMetric)) - .map((tJson): IEither => { - const tJob = tJson.get(); - if (!isJob(tJob) || !validateExecutionEntries(tJob)) { - const err = "seems like a pwetty mawfomed job (-.-)"; - tJson.trace.addTrace(LogLevel.WARN).trace(err); - return Either.left(new PenguenoError(err, 400)); - } - return Either.right(tJob); - }) - .peek((tJob) => - tJob.trace.trace( - tJob - .get() - .fold(({ isLeft }) => - isLeft ? wellFormedJobMetric.failure : wellFormedJobMetric.success, - ), - ), - ) - .get(); - -export interface IJobHookActivity { - processHook: IActivity; -} - -const jobHookRequestMetric = Metric.fromName("JobHook.process"); -export class JobHookActivityImpl implements IJobHookActivity { - constructor( - private readonly queuer: IJobQueuer>, - ) {} - - private trace(r: ITraceable) { - return r - .bimap(TraceUtil.withClassTrace(this)) - .bimap(TraceUtil.withMetricTrace(jobHookRequestMetric)); - } - - public processHook(r: ITraceable) { - return this.trace(r) - .map(jsonModel(jobJsonTransformer)) - .map(async (tEitherJobJson) => { - const eitherJob = await tEitherJobJson.get(); - return eitherJob.flatMapAsync(async (job) => { - const eitherQueued = await tEitherJobJson - .move(job) - .map((job) => this.queuer.queue(job)) - .get(); - return eitherQueued.mapLeft((e) => new PenguenoError(e.message, 500)); - }); - }) - .peek( - TraceUtil.promiseify((tJob) => - tJob.get().fold(({ isRight, value }) => { - if (isRight) { - tJob.trace.trace(jobHookRequestMetric.success); - tJob.trace.trace(`all queued up and weady to go :D !! ${value}`); - return; - } - - tJob.trace.trace( - value.source === ErrorSource.SYSTEM - ? jobHookRequestMetric.failure - : jobHookRequestMetric.warn, - ); - tJob.trace.addTrace(value.source).trace(`${value}`); - }), - ), - ) - .map( - TraceUtil.promiseify( - (tEitherQueuedJob) => - new JsonResponse(r, tEitherQueuedJob.get(), { - status: tEitherQueuedJob - .get() - .fold(({ isRight, value }) => (isRight ? 200 : value.status)), - }), - ), - ) - .get(); - } -} - -// -- -- - -// -- -- -type QueuePosition = string; -export class QueueError extends Error {} -export interface IJobQueuer { - queue: Mapper>>; -} - -export class LaminarJobQueuer - implements IJobQueuer> -{ - constructor(private readonly queuePositionPrefix: string) {} - - private static GetJobTypeTrace = (jobType: string) => - `LaminarJobQueue.Queue.${jobType}`; - private static JobTypeMetrics = memoize((jobType: string) => - Metric.fromName(LaminarJobQueuer.GetJobTypeTrace(jobType)), - ); - - public queue(j: ITraceable) { - const { type: jobType } = j.get(); - const trace = LaminarJobQueuer.GetJobTypeTrace(jobType); - const metric = LaminarJobQueuer.JobTypeMetrics(jobType); - - return j - .bimap(TraceUtil.withTrace(trace)) - .bimap(TraceUtil.withMetricTrace(metric)) - .map((j) => { - const { type: jobType, arguments: args } = j.get(); - const laminarCommand = [ - "laminarc", - "queue", - jobType, - ...Object.entries(args).map(([key, val]) => `"${key}"="${val}"`), - ]; - return laminarCommand; - }) - .peek((c) => - c.trace.trace( - `im so excited to see how this queue job will end!! (>ᴗ<): ${c - .get() - .toString()}`, - ), - ) - .map(getStdout) - .peek( - TraceUtil.promiseify((q) => - q.trace.trace( - q - .get() - .fold(({ isLeft }) => (isLeft ? metric.failure : metric.success)), - ), - ), - ) - .map( - TraceUtil.promiseify((q) => - q.get().fold(({ isLeft, value }) => { - if (isLeft) { - q.trace.addTrace(LogLevel.ERROR).trace(value.toString()); - return Either.left(value); - } - q.trace.addTrace(LogLevel.DEBUG).trace(`stdout ${value}`); - const [jobName, jobId] = value.split(":"); - const jobUrl = `${this.queuePositionPrefix}/jobs/${jobName}/${jobId}`; - - q.trace.trace(`all queued up and weady to go~ (˘ω˘) => ${jobUrl}`); - return Either.right(jobUrl); - }), - ), - ) - .get(); - } -} -// -- -- diff --git a/server/job/index.ts b/server/job/index.ts new file mode 100644 index 0000000..ecf0984 --- /dev/null +++ b/server/job/index.ts @@ -0,0 +1,2 @@ +export * from './queue'; +export * from './run_activity'; diff --git a/server/job/queue.ts b/server/job/queue.ts new file mode 100644 index 0000000..2392222 --- /dev/null +++ b/server/job/queue.ts @@ -0,0 +1,74 @@ +import { + getStdout, + type Mapper, + memoize, + Either, + type IEither, + type ITraceable, + LogLevel, + Metric, + type ServerTrace, + TraceUtil, +} from '@emprespresso/pengueno'; +import { type Job } from '@emprespresso/ci_model'; + +type QueuePosition = string; +export class QueueError extends Error {} +export interface IJobQueuer { + queue: Mapper>>; +} + +export class LaminarJobQueuer implements IJobQueuer> { + constructor(private readonly queuePositionPrefix: string) {} + + private static GetJobTypeTrace = (jobType: string) => `LaminarJobQueue.Queue.${jobType}`; + private static JobTypeMetrics = memoize((jobType: string) => + Metric.fromName(LaminarJobQueuer.GetJobTypeTrace(jobType)), + ); + + public queue(j: ITraceable) { + const { type: jobType } = j.get(); + const trace = LaminarJobQueuer.GetJobTypeTrace(jobType); + const metric = LaminarJobQueuer.JobTypeMetrics(jobType); + + return j + .bimap(TraceUtil.withTrace(trace)) + .bimap(TraceUtil.withMetricTrace(metric)) + .map((j) => { + const { type: jobType, arguments: args } = j.get(); + const laminarCommand = [ + 'laminarc', + 'queue', + jobType, + ...Object.entries(args).map(([key, val]) => `"${key}"="${val}"`), + ]; + return laminarCommand; + }) + .peek((c) => + c.trace.trace(`im so excited to see how this queue job will end!! (>ᴗ<): ${c.get().toString()}`), + ) + .map(getStdout) + .peek( + TraceUtil.promiseify((q) => + q.trace.trace(q.get().fold(({ isLeft }) => (isLeft ? metric.failure : metric.success))), + ), + ) + .map( + TraceUtil.promiseify((q) => + q.get().fold(({ isLeft, value }) => { + if (isLeft) { + q.trace.addTrace(LogLevel.ERROR).trace(value.toString()); + return Either.left(value); + } + q.trace.addTrace(LogLevel.DEBUG).trace(`stdout ${value}`); + const [jobName, jobId] = value.split(':'); + const jobUrl = `${this.queuePositionPrefix}/jobs/${jobName}/${jobId}`; + + q.trace.trace(`all queued up and weady to go~ (˘ω˘) => ${jobUrl}`); + return Either.right(jobUrl); + }), + ), + ) + .get(); + } +} diff --git a/server/job/run_activity.ts b/server/job/run_activity.ts new file mode 100644 index 0000000..9f25cf8 --- /dev/null +++ b/server/job/run_activity.ts @@ -0,0 +1,94 @@ +import { + Either, + ErrorSource, + type IActivity, + type IEither, + type ITraceable, + jsonModel, + JsonResponse, + LogLevel, + Metric, + PenguenoError, + type PenguenoRequest, + type ServerTrace, + TraceUtil, + validateExecutionEntries, +} from '@emprespresso/pengueno'; +import { isJob, type Job } from '@emprespresso/ci_model'; +import { IJobQueuer } from './queue'; + +const wellFormedJobMetric = Metric.fromName('Job.WellFormed'); + +const jobJsonTransformer = (j: ITraceable): IEither => + j + .bimap(TraceUtil.withMetricTrace(wellFormedJobMetric)) + .map((tJson): IEither => { + const tJob = tJson.get(); + if (!isJob(tJob) || !validateExecutionEntries(tJob)) { + const err = 'seems like a pwetty mawfomed job (-.-)'; + tJson.trace.addTrace(LogLevel.WARN).trace(err); + return Either.left(new PenguenoError(err, 400)); + } + return Either.right(tJob); + }) + .peek((tJob) => + tJob.trace.trace( + tJob.get().fold(({ isLeft }) => (isLeft ? wellFormedJobMetric.failure : wellFormedJobMetric.success)), + ), + ) + .get(); + +export interface IJobHookActivity { + processHook: IActivity; +} + +const jobHookRequestMetric = Metric.fromName('JobHook.process'); +export class JobHookActivityImpl implements IJobHookActivity { + constructor(private readonly queuer: IJobQueuer>) {} + + private trace(r: ITraceable) { + return r.bimap(TraceUtil.withClassTrace(this)).bimap(TraceUtil.withMetricTrace(jobHookRequestMetric)); + } + + public processHook(r: ITraceable) { + return this.trace(r) + .map(jsonModel(jobJsonTransformer)) + .map(async (tEitherJobJson) => { + const eitherJob = await tEitherJobJson.get(); + return eitherJob.flatMapAsync(async (job) => { + const eitherQueued = await tEitherJobJson + .move(job) + .map((job) => this.queuer.queue(job)) + .get(); + return eitherQueued.mapLeft((e) => new PenguenoError(e.message, 500)); + }); + }) + .peek( + TraceUtil.promiseify((tJob) => + tJob.get().fold(({ isRight, value }) => { + if (isRight) { + tJob.trace.trace(jobHookRequestMetric.success); + tJob.trace.trace(`all queued up and weady to go :D !! ${value}`); + return; + } + + tJob.trace.trace( + value.source === ErrorSource.SYSTEM + ? jobHookRequestMetric.failure + : jobHookRequestMetric.warn, + ); + tJob.trace.addTrace(value.source).trace(`${value}`); + }), + ), + ) + .map( + TraceUtil.promiseify( + (tEitherQueuedJob) => + new JsonResponse(r, tEitherQueuedJob.get(), { + status: tEitherQueuedJob.get().fold(({ isRight, value }) => (isRight ? 200 : value.status)), + }), + ), + ) + .get(); + } +} diff --git a/server/mod.ts b/server/mod.ts deleted file mode 100644 index 1d168d2..0000000 --- a/server/mod.ts +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env -S deno run --allow-env --allow-net --allow-run - -export * from "./ci.ts"; -export * from "./health.ts"; -export * from "./job.ts"; - -import { CiHookServer } from "./mod.ts"; -import { Either, type IEither } from "@emprespresso/pengueno"; -const server = new CiHookServer(); - -export const runServer = ( - port: number, - host: string, -): Promise> => { - const serverConfig = { - host, - port, - }; - return Either.fromFailable(() => - Deno.serve(serverConfig, (req) => server.serve(req)), - ).flatMapAsync((server) => Either.fromFailableAsync(() => server.finished.then(() => 0))); -}; diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..644f267 --- /dev/null +++ b/server/package.json @@ -0,0 +1,33 @@ +{ + "name": "@emprespresso/ci_server", + "version": "0.1.0", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "clean": "rm -rf dist", + "type-check": "tsc --noEmit", + "start": "node dist/index.js", + "format": "prettier --write .", + "format:check": "prettier --check ." + }, + "dependencies": { + "@emprespresso/pengueno": "*", + "@emprespresso/ci_model": "*", + "hono": "^4.8.0", + "@hono/node-server": "^1.14.0" + }, + "files": [ + "dist/**/*", + "package.json", + "README.md" + ] +} diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 0000000..58e9147 --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./", + "composite": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "noEmit": false + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"], + "references": [{ "path": "../u" }, { "path": "../model" }] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..105c510 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "outDir": "./dist", + "moduleResolution": "node", + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "lib": ["ES2022"], + "paths": { + "@emprespresso/pengueno": ["./u/index.ts"], + "@emprespresso/ci_model": ["./model/index.ts"], + "@emprespresso/ci_server": ["./server/index.ts"], + "@emprespresso/ci_worker": ["./worker/index.ts"], + } + }, + "include": ["**/*.ts", "**/*.js"], + "exclude": ["node_modules", "dist", "**/*.d.ts"], + "references": [{ "path": "./u" }, { "path": "./model" }, { "path": "./server" }, { "path": "./worker" }] +} diff --git a/u/deno.json b/u/deno.json deleted file mode 100644 index b277873..0000000 --- a/u/deno.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "@emprespresso/pengueno", - "version": "0.1.0", - "exports": "./mod.ts" -} diff --git a/u/fn/callable.ts b/u/fn/callable.ts index cfb7d00..8a61057 100644 --- a/u/fn/callable.ts +++ b/u/fn/callable.ts @@ -1,18 +1,18 @@ // deno-lint-ignore no-explicit-any export interface Callable { - (...args: Array): T; + (...args: Array): T; } export interface Supplier extends Callable { - (): T; + (): T; } export interface Mapper extends Callable { - (t: T): U; + (t: T): U; } export interface BiMapper extends Callable { - (t: T, u: U): R; + (t: T, u: U): R; } export interface SideEffect extends Mapper {} diff --git a/u/fn/either.ts b/u/fn/either.ts index ffe8033..8c47b64 100644 --- a/u/fn/either.ts +++ b/u/fn/either.ts @@ -1,114 +1,100 @@ -import { type Mapper, type Supplier, isObject } from "@emprespresso/pengueno"; +import { type Mapper, type Supplier, isObject } from '@emprespresso/pengueno'; -type IEitherTag = "IEither"; -const iEitherTag: IEitherTag = "IEither"; +type IEitherTag = 'IEither'; +const iEitherTag: IEitherTag = 'IEither'; export interface _Either { - readonly isLeft: LeftT; - readonly isRight: RightT; - readonly value: T; + readonly isLeft: LeftT; + readonly isRight: RightT; + readonly value: T; } export type Left = _Either; export type Right = _Either; export interface IEither { - readonly _tag: IEitherTag; - - mapBoth: <_E, _T>( - errBranch: Mapper, - okBranch: Mapper, - ) => IEither<_E, _T>; - fold: <_T>(folder: Mapper | Right, _T>) => _T; - moveRight: <_T>(t: _T) => IEither; - mapRight: <_T>(mapper: Mapper) => IEither; - mapLeft: <_E>(mapper: Mapper) => IEither<_E, T>; - flatMap: <_T>(mapper: Mapper>) => IEither; - flatMapAsync: <_T>( - mapper: Mapper>>, - ) => Promise>; + readonly _tag: IEitherTag; + + mapBoth: <_E, _T>(errBranch: Mapper, okBranch: Mapper) => IEither<_E, _T>; + fold: <_T>(folder: Mapper | Right, _T>) => _T; + moveRight: <_T>(t: _T) => IEither; + mapRight: <_T>(mapper: Mapper) => IEither; + mapLeft: <_E>(mapper: Mapper) => IEither<_E, T>; + flatMap: <_T>(mapper: Mapper>) => IEither; + flatMapAsync: <_T>(mapper: Mapper>>) => Promise>; } export class Either implements IEither { - private readonly self: Left | Right; - - private constructor( - init: { err?: E; ok?: T }, - public readonly _tag: IEitherTag = iEitherTag, - ) { - this.self = | Right>{ - isLeft: "err" in init, - isRight: "ok" in init, - value: init.err ?? init.ok!, - }; - } - - public moveRight<_T>(t: _T) { - return this.mapRight(() => t); - } - - public fold<_T>(folder: Mapper | Right, _T>): _T { - return folder(this.self); - } - - public mapBoth<_E, _T>( - errBranch: Mapper, - okBranch: Mapper, - ): IEither<_E, _T> { - if (this.self.isLeft) return Either.left(errBranch(this.self.value)); - return Either.right(okBranch(this.self.value)); - } - - public flatMap<_T>(mapper: Mapper>): IEither { - if (this.self.isRight) return mapper(this.self.value); - return Either.left(this.self.value); - } - - public mapRight<_T>(mapper: Mapper): IEither { - if (this.self.isRight) return Either.right(mapper(this.self.value)); - return Either.left(this.self.value); - } - - public mapLeft<_E>(mapper: Mapper): IEither<_E, T> { - if (this.self.isLeft) return Either.left<_E, T>(mapper(this.self.value)); - return Either.right<_E, T>(this.self.value); - } - - public async flatMapAsync<_T>( - mapper: Mapper>>, - ): Promise> { - if (this.self.isLeft) { - return Promise.resolve(Either.left(this.self.value)); + private readonly self: Left | Right; + + private constructor( + init: { err?: E; ok?: T }, + public readonly _tag: IEitherTag = iEitherTag, + ) { + this.self = | Right>{ + isLeft: 'err' in init, + isRight: 'ok' in init, + value: init.err ?? init.ok!, + }; } - return await mapper(this.self.value).catch((err) => - Either.left(err), - ); - } - - static left(e: E): IEither { - return new Either({ err: e }); - } - - static right(t: T): IEither { - return new Either({ ok: t }); - } - - static fromFailable(s: Supplier): IEither { - try { - return Either.right(s()); - } catch (e) { - return Either.left(e as E); + + public moveRight<_T>(t: _T) { + return this.mapRight(() => t); + } + + public fold<_T>(folder: Mapper | Right, _T>): _T { + return folder(this.self); + } + + public mapBoth<_E, _T>(errBranch: Mapper, okBranch: Mapper): IEither<_E, _T> { + if (this.self.isLeft) return Either.left(errBranch(this.self.value)); + return Either.right(okBranch(this.self.value)); + } + + public flatMap<_T>(mapper: Mapper>): IEither { + if (this.self.isRight) return mapper(this.self.value); + return Either.left(this.self.value); + } + + public mapRight<_T>(mapper: Mapper): IEither { + if (this.self.isRight) return Either.right(mapper(this.self.value)); + return Either.left(this.self.value); + } + + public mapLeft<_E>(mapper: Mapper): IEither<_E, T> { + if (this.self.isLeft) return Either.left<_E, T>(mapper(this.self.value)); + return Either.right<_E, T>(this.self.value); + } + + public async flatMapAsync<_T>(mapper: Mapper>>): Promise> { + if (this.self.isLeft) { + return Promise.resolve(Either.left(this.self.value)); + } + return await mapper(this.self.value).catch((err) => Either.left(err)); + } + + static left(e: E): IEither { + return new Either({ err: e }); + } + + static right(t: T): IEither { + return new Either({ ok: t }); + } + + static fromFailable(s: Supplier): IEither { + try { + return Either.right(s()); + } catch (e) { + return Either.left(e as E); + } + } + + static async fromFailableAsync(s: Supplier> | Promise): Promise> { + return await (typeof s === 'function' ? s() : s) + .then((t: T) => Either.right(t)) + .catch((e: E) => Either.left(e)); } - } - - static async fromFailableAsync( - s: Supplier>, - ): Promise> { - return await s() - .then((t: T) => Either.right(t)) - .catch((e: E) => Either.left(e)); - } } export const isEither = (o: unknown): o is IEither => { - return isObject(o) && "_tag" in o && o._tag === "IEither"; + return isObject(o) && '_tag' in o && o._tag === 'IEither'; }; diff --git a/u/fn/index.ts b/u/fn/index.ts new file mode 100644 index 0000000..1ec71aa --- /dev/null +++ b/u/fn/index.ts @@ -0,0 +1,2 @@ +export * from './callable.js'; +export * from './either.js'; diff --git a/u/fn/mod.ts b/u/fn/mod.ts deleted file mode 100644 index f0fbe88..0000000 --- a/u/fn/mod.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./callable.ts"; -export * from "./either.ts"; diff --git a/u/index.ts b/u/index.ts new file mode 100644 index 0000000..0c8c760 --- /dev/null +++ b/u/index.ts @@ -0,0 +1,6 @@ +export * from './fn/index.js'; +export * from './leftpadesque/index.js'; +export * from './process/index.js'; +export * from './trace/index.js'; +export * from './server/index.js'; +export * from './history.js'; diff --git a/u/leftpadesque/debug.ts b/u/leftpadesque/debug.ts index e50b2e0..074e567 100644 --- a/u/leftpadesque/debug.ts +++ b/u/leftpadesque/debug.ts @@ -1,13 +1,8 @@ -const _hasEnv = !Deno.permissions.querySync({ name: "env" }); +const _hasEnv = true; // Node.js always has access to environment variables -const _env: "development" | "production" = - _hasEnv && (Deno.env.get("ENVIRONMENT") ?? "").toLowerCase().includes("prod") - ? "production" - : "development"; -export const isProd = () => _env === "production"; +const _env: 'development' | 'production' = + _hasEnv && (process.env.ENVIRONMENT ?? '').toLowerCase().includes('prod') ? 'production' : 'development'; +export const isProd = () => _env === 'production'; -const _debug = - !isProd() || - (_hasEnv && - ["y", "t"].some((Deno.env.get("DEBUG") ?? "").toLowerCase().startsWith)); +const _debug = !isProd() || (_hasEnv && ['y', 't'].some((process.env.DEBUG ?? '').toLowerCase().startsWith)); export const isDebug = () => _debug; diff --git a/u/leftpadesque/index.ts b/u/leftpadesque/index.ts new file mode 100644 index 0000000..6403e4a --- /dev/null +++ b/u/leftpadesque/index.ts @@ -0,0 +1,4 @@ +export * from './object.js'; +export * from './prepend.js'; +export * from './debug.js'; +export * from './memoize.js'; diff --git a/u/leftpadesque/memoize.ts b/u/leftpadesque/memoize.ts index 95e6019..541bd20 100644 --- a/u/leftpadesque/memoize.ts +++ b/u/leftpadesque/memoize.ts @@ -1,14 +1,14 @@ -import type { Callable } from "@emprespresso/pengueno"; +import type { Callable } from '@emprespresso/pengueno'; export const memoize = >(fn: F): F => { - const cache = new Map(); - return ((...args: unknown[]): R => { - const key = JSON.stringify(args); - if (cache.has(key)) { - return cache.get(key)!; - } - const res = fn.apply(args); - cache.set(key, res); - return res; - }) as F; + const cache = new Map(); + return ((...args: unknown[]): R => { + const key = JSON.stringify(args); + if (cache.has(key)) { + return cache.get(key)!; + } + const res = fn.apply(args); + cache.set(key, res); + return res; + }) as F; }; diff --git a/u/leftpadesque/mod.ts b/u/leftpadesque/mod.ts deleted file mode 100644 index 63d8d7a..0000000 --- a/u/leftpadesque/mod.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./object.ts"; -export * from "./prepend.ts"; -export * from "./debug.ts"; -export * from "./memoize.ts"; diff --git a/u/leftpadesque/object.ts b/u/leftpadesque/object.ts index 73f7f80..fe97999 100644 --- a/u/leftpadesque/object.ts +++ b/u/leftpadesque/object.ts @@ -1,2 +1 @@ -export const isObject = (o: unknown): o is object => - typeof o === "object" && !Array.isArray(o) && !!o; +export const isObject = (o: unknown): o is object => typeof o === 'object' && !Array.isArray(o) && !!o; diff --git a/u/leftpadesque/prepend.ts b/u/leftpadesque/prepend.ts index 0f1ce30..d80f6b6 100644 --- a/u/leftpadesque/prepend.ts +++ b/u/leftpadesque/prepend.ts @@ -1,5 +1,5 @@ export const prependWith = (arr: string[], prep: string) => - Array(arr.length * 2) - .fill(0) - .map((_, i) => i % 2 === 0) - .map((isPrep, i) => (isPrep ? prep : arr[i])); + Array(arr.length * 2) + .fill(0) + .map((_, i) => i % 2 === 0) + .map((isPrep, i) => (isPrep ? prep : arr[i]!)); diff --git a/u/mod.ts b/u/mod.ts deleted file mode 100644 index 2ab8f68..0000000 --- a/u/mod.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from "./fn/mod.ts"; -export * from "./leftpadesque/mod.ts"; -export * from "./process/mod.ts"; -export * from "./trace/mod.ts"; -export * from "./server/mod.ts"; -export * from "./history.ts"; diff --git a/u/package.json b/u/package.json new file mode 100644 index 0000000..d38ac4c --- /dev/null +++ b/u/package.json @@ -0,0 +1,47 @@ +{ + "name": "@emprespresso/pengueno", + "version": "0.1.0", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./fn": { + "types": "./dist/fn/index.d.ts", + "import": "./dist/fn/index.js" + }, + "./leftpadesque": { + "types": "./dist/leftpadesque/index.d.ts", + "import": "./dist/leftpadesque/index.js" + }, + "./process": { + "types": "./dist/process/index.d.ts", + "import": "./dist/process/index.js" + }, + "./trace": { + "types": "./dist/trace/index.d.ts", + "import": "./dist/trace/index.js" + }, + "./server": { + "types": "./dist/server/index.d.ts", + "import": "./dist/server/index.js" + } + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "clean": "rm -rf dist", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "hono": "^4.8.0" + }, + "files": [ + "dist/**/*", + "package.json", + "README.md" + ] +} diff --git a/u/process/argv.ts b/u/process/argv.ts index 45f8ff0..dcdba85 100644 --- a/u/process/argv.ts +++ b/u/process/argv.ts @@ -1,7 +1,6 @@ -import { Either, type Mapper, type IEither } from "@emprespresso/pengueno"; +import { Either, type Mapper, type IEither } from '@emprespresso/pengueno'; -export const isArgKey = (k: string): k is K => - k.startsWith("--"); +export const isArgKey = (k: string): k is K => k.startsWith('--'); interface ArgHandler { absent?: V; @@ -10,42 +9,42 @@ interface ArgHandler { } export const getArg = ( - arg: K, - argv: Array, - whenValue: ArgHandler, + arg: K, + argv: Array, + whenValue: ArgHandler, ): IEither => { - const value = argv.filter((_argv) => isArgKey(_argv) && _argv.split("=")[0] === arg).map((_argv, i) => { - const next = _argv.includes("=") ? _argv.split("=")[1] : argv.at(i + 1); - if (next) { - if (isArgKey(next)) return whenValue.unspecified; - return whenValue.present(next); - } - return whenValue.unspecified; - }).find(x => x) ?? whenValue.absent; + const value = + argv + .filter((_argv) => isArgKey(_argv) && _argv.split('=')[0] === arg) + .map((_argv, i) => { + const next = _argv.includes('=') ? _argv.split('=')[1] : argv.at(i + 1); + if (next) { + if (isArgKey(next)) return whenValue.unspecified; + return whenValue.present(next); + } + return whenValue.unspecified; + }) + .find((x) => x) ?? whenValue.absent; if (value === undefined) { - return Either.left(new Error("no value specified for " + arg)); + return Either.left(new Error('no value specified for ' + arg)); } return Either.right(value); }; type MappedArgs< - Args extends ReadonlyArray, - Handlers extends Partial>> + Args extends ReadonlyArray, + Handlers extends Partial>>, > = { - [K in Args[number]]: K extends keyof Handlers - ? Handlers[K] extends ArgHandler - ? T - : string - : string; + [K in Args[number]]: K extends keyof Handlers ? (Handlers[K] extends ArgHandler ? T : string) : string; }; export const argv = < - const Args extends ReadonlyArray, - const Handlers extends Partial>> + const Args extends ReadonlyArray, + const Handlers extends Partial>>, >( args: Args, handlers?: Handlers, - argv = Deno.args, + argv = process.argv.slice(2), ): IEither> => { type Result = MappedArgs; @@ -53,20 +52,20 @@ export const argv = < const processArg = (arg: Args[number]): IEither => { const handler = handlers?.[arg] ?? defaultHandler; - return getArg(arg, argv, handler).mapRight(value => [arg, value] as const); + return getArg(arg, argv, handler).mapRight((value) => [arg, value] as const); }; - const argResults = args.map(processArg); - - return argResults.reduce( - (acc: IEither>, current: IEither) => { - return acc.flatMap(accValue => - current.mapRight(([key, value]) => ({ - ...accValue, - [key]: value - })) - ); - }, - Either.right({} as Partial) - ).mapRight(result => result as Result); + return args + .map(processArg) + .reduce( + (acc: IEither>, current: IEither) => + acc.flatMap((accValue) => + current.mapRight(([key, value]) => ({ + ...accValue, + [key]: value, + })), + ), + Either.right(>{}), + ) + .mapRight((result) => result); }; diff --git a/u/process/env.ts b/u/process/env.ts index 5ba4189..1e4fd32 100644 --- a/u/process/env.ts +++ b/u/process/env.ts @@ -1,37 +1,30 @@ -import { Either, type IEither } from "@emprespresso/pengueno"; +import { Either, type IEither } from '@emprespresso/pengueno'; export const getRequiredEnv = (name: V): IEither => - Either.fromFailable( - () => Deno.env.get(name) as V | undefined, - ) // could throw when no permission. - .flatMap( - (v) => - (v && Either.right(v)) || - Either.left(new Error(`environment variable "${name}" is required D:`)), - ); + Either.fromFailable(() => process.env[name] as V | undefined) // could throw when no permission. + .flatMap( + (v) => (v && Either.right(v)) || Either.left(new Error(`environment variable "${name}" is required D:`)), + ); type ObjectFromList, V = string> = { - [K in T extends ReadonlyArray ? U : never]: V; + [K in T extends ReadonlyArray ? U : never]: V; }; export const getRequiredEnvVars = (vars: ReadonlyArray) => - vars - .map((envVar) => [envVar, getRequiredEnv(envVar)] as [V, IEither]) - .reduce( - ( - acc: IEither>, - x: [V, IEither], - ) => { - const [envVar, eitherVal] = x; - return acc.flatMap((args) => { - return eitherVal.mapRight( - (envValue) => - ({ - ...args, - [envVar]: envValue, - }) as ObjectFromList, - ); - }); - }, - Either.right({} as ObjectFromList), - ); + vars + .map((envVar) => [envVar, getRequiredEnv(envVar)] as [V, IEither]) + .reduce( + (acc: IEither>, x: [V, IEither]) => { + const [envVar, eitherVal] = x; + return acc.flatMap((args) => { + return eitherVal.mapRight( + (envValue) => + ({ + ...args, + [envVar]: envValue, + }) as ObjectFromList, + ); + }); + }, + Either.right({} as ObjectFromList), + ); diff --git a/u/process/index.ts b/u/process/index.ts new file mode 100644 index 0000000..4ffbf2a --- /dev/null +++ b/u/process/index.ts @@ -0,0 +1,4 @@ +export * from './env.js'; +export * from './run.js'; +export * from './validate_identifier.js'; +export * from './argv.js'; diff --git a/u/process/mod.ts b/u/process/mod.ts deleted file mode 100644 index 211e9a7..0000000 --- a/u/process/mod.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./env.ts"; -export * from "./run.ts"; -export * from "./validate_identifier.ts"; -export * from "./argv.ts"; diff --git a/u/process/run.ts b/u/process/run.ts index abe143c..e3c4c3d 100644 --- a/u/process/run.ts +++ b/u/process/run.ts @@ -1,68 +1,46 @@ import { - Either, - type IEither, - type ITraceable, - LogLevel, - type LogTraceSupplier, - TraceUtil, -} from "@emprespresso/pengueno"; + Either, + type IEither, + type ITraceable, + LogLevel, + type LogTraceSupplier, + TraceUtil, +} from '@emprespresso/pengueno'; +import { promisify } from 'node:util'; +import { exec as execCallback } from 'node:child_process'; +const exec = promisify(execCallback); export type Command = string[] | string; -type CommandOutputDecoded = { - code: number; - stdoutText: string; - stderrText: string; -}; +export type StdStreams = { stdout: string; stderr: string }; export const getStdout = ( - c: ITraceable, - options: Deno.CommandOptions = {}, + c: ITraceable, + options: { env?: Record; clearEnv?: boolean } = {}, ): Promise> => - c - .bimap(TraceUtil.withFunctionTrace(getStdout)) - .map((tCmd) => { - const cmd = tCmd.get(); - tCmd.trace.trace(`Command = ${cmd} :> im gonna run this command! `); - const [exec, ...args] = typeof cmd === "string" ? cmd.split(" ") : cmd; - return new Deno.Command(exec, { - args, - stdout: "piped", - stderr: "piped", - ...options, - }); - }) - .map((tCmd) => - Either.fromFailableAsync(() => - tCmd.get().output(), - ), - ) - .map( - TraceUtil.promiseify((tEitherOut) => - tEitherOut.get().flatMap(({ code, stderr, stdout }) => - Either.fromFailable(() => { - const stdoutText = new TextDecoder().decode(stdout); - const stderrText = new TextDecoder().decode(stderr); - return { code, stdoutText, stderrText }; - }) - .mapLeft((e) => { - tEitherOut.trace.addTrace(LogLevel.ERROR).trace(e); - return e; - }) - .flatMap((decodedOutput): IEither => { - const { code, stdoutText, stderrText } = decodedOutput; - if (stderrText) { - tEitherOut.trace - .addTrace(LogLevel.DEBUG) - .trace(`stderr: ${stderrText}`); - } - if (code !== 0) { - const msg = `i weceived an exit code of ${code} i wanna zewoooo :<`; - tEitherOut.trace.addTrace(LogLevel.ERROR).trace(msg); - return Either.left(new Error(msg)); - } - return Either.right(stdoutText); - }), - ), - ), - ) - .get(); + c + .bimap(TraceUtil.withFunctionTrace(getStdout)) + .bimap((tCmd) => { + const cmd = tCmd.get(); + tCmd.trace.trace(`Command = ${cmd} :> im gonna run this command! `); + + const _exec = typeof cmd === 'string' ? cmd : cmd.join(' '); + const env = options.clearEnv ? options.env : { ...process.env, ...options.env }; + + const p: Promise> = Either.fromFailableAsync(exec(_exec, { env })); + return [p, `Command = ${_exec}`]; + }) + .map( + TraceUtil.promiseify( + (tEitherProcess): IEither => + tEitherProcess.get().fold(({ isLeft, value }) => { + if (isLeft) { + return Either.left(value); + } + if (value.stderr) { + tEitherProcess.trace.addTrace(LogLevel.DEBUG).trace(`StdErr = ${value.stderr}`); + } + return Either.right(value.stdout); + }), + ), + ) + .get(); diff --git a/u/process/validate_identifier.ts b/u/process/validate_identifier.ts index 4e93728..1ff3791 100644 --- a/u/process/validate_identifier.ts +++ b/u/process/validate_identifier.ts @@ -1,23 +1,18 @@ -import { Either, type IEither } from "@emprespresso/pengueno"; +import { Either, type IEither } from '@emprespresso/pengueno'; export const validateIdentifier = (token: string) => { - return /^[a-zA-Z0-9_\-:. \/]+$/.test(token) && !token.includes(".."); + return /^[a-zA-Z0-9_\-:. \/]+$/.test(token) && !token.includes('..'); }; // ensure {@param obj} is a Record with stuff that won't // have the potential for shell injection, just to be super safe. type InvalidEntry = [K, T]; -export const validateExecutionEntries = < - T, - K extends symbol | number | string = symbol | number | string, ->( - obj: Record, +export const validateExecutionEntries = ( + obj: Record, ): IEither>, Record> => { - const invalidEntries = >>( - Object.entries(obj).filter( - (e) => !e.every((x) => typeof x === "string" && validateIdentifier(x)), - ) - ); - if (invalidEntries.length > 0) return Either.left(invalidEntries); - return Either.right(>obj); + const invalidEntries = >>( + Object.entries(obj).filter((e) => !e.every((x) => typeof x === 'string' && validateIdentifier(x))) + ); + if (invalidEntries.length > 0) return Either.left(invalidEntries); + return Either.right(>obj); }; diff --git a/u/server/activity/fourohfour.ts b/u/server/activity/fourohfour.ts index 33cfe5f..cd90ba0 100644 --- a/u/server/activity/fourohfour.ts +++ b/u/server/activity/fourohfour.ts @@ -1,29 +1,28 @@ import { - type IActivity, - type ITraceable, - JsonResponse, - type PenguenoRequest, - type ServerTrace, -} from "@emprespresso/pengueno"; + type IActivity, + type ITraceable, + JsonResponse, + type PenguenoRequest, + type ServerTrace, +} from '@emprespresso/pengueno'; const messages = [ - "D: meow-t found! your api call ran away!", - "404-bidden! but like...in a cute way >:3 !", - ":< your data went on a paw-sible vacation!", - "uwu~ not found, but found our hearts instead!", + 'D: meow-t found! your api call ran away!', + '404-bidden! but like...in a cute way >:3 !', + ':< your data went on a paw-sible vacation!', + 'uwu~ not found, but found our hearts instead!', ]; -const randomFourOhFour = () => - messages[Math.floor(Math.random() * messages.length)]; +const randomFourOhFour = () => messages[Math.floor(Math.random() * messages.length)]!; export interface IFourOhFourActivity { - fourOhFour: IActivity; + fourOhFour: IActivity; } export class FourOhFourActivityImpl implements IFourOhFourActivity { - public fourOhFour(req: ITraceable) { - return req - .move(new JsonResponse(req, randomFourOhFour(), { status: 404 })) - .map((resp) => Promise.resolve(resp.get())) - .get(); - } + public fourOhFour(req: ITraceable) { + return req + .move(new JsonResponse(req, randomFourOhFour(), { status: 404 })) + .map((resp) => Promise.resolve(resp.get())) + .get(); + } } diff --git a/u/server/activity/health.ts b/u/server/activity/health.ts index 95dfa97..b3ae559 100644 --- a/u/server/activity/health.ts +++ b/u/server/activity/health.ts @@ -1,71 +1,67 @@ import { - type IActivity, - type IEither, - type ITraceable, - JsonResponse, - LogLevel, - type Mapper, - Metric, - type PenguenoRequest, - type ServerTrace, - TraceUtil, -} from "@emprespresso/pengueno"; + type IActivity, + type IEither, + IMetric, + type ITraceable, + JsonResponse, + LogLevel, + type Mapper, + Metric, + type PenguenoRequest, + type ServerTrace, + TraceUtil, +} from '@emprespresso/pengueno'; export enum HealthCheckInput { - CHECK, + CHECK, } export enum HealthCheckOutput { - YAASSSLAYQUEEN, + YAASSSLAYQUEEN, } export interface IHealthCheckActivity { - checkHealth: IActivity; + checkHealth: IActivity; } -const healthCheckMetric = Metric.fromName("Health"); +const healthCheckMetric: IMetric = Metric.fromName('Health'); export interface HealthChecker - extends Mapper< - ITraceable, - Promise> - > {} + extends Mapper, Promise>> {} export class HealthCheckActivityImpl implements IHealthCheckActivity { - constructor(private readonly check: HealthChecker) {} + constructor(private readonly check: HealthChecker) {} - public checkHealth(req: ITraceable) { - return req - .bimap(TraceUtil.withFunctionTrace(this.checkHealth)) - .bimap(TraceUtil.withMetricTrace(healthCheckMetric)) - .flatMap((r) => - r.move(HealthCheckInput.CHECK).map((input) => this.check(input)), - ) - .peek( - TraceUtil.promiseify((h) => - h.get().fold(({ isLeft, value }) => { - if (!isLeft) { - h.trace.trace(healthCheckMetric.success); - return; - } - h.trace.trace(healthCheckMetric.failure); - h.trace.addTrace(LogLevel.ERROR).trace(value); - }), - ), - ) - .map( - TraceUtil.promiseify((h) => - h - .get() - .mapBoth( - () => "oh no, i need to eat more vegetables (。•́︿•̀。)...", - () => "think im healthy!! (✿˘◡˘) ready to do work~", + public checkHealth(req: ITraceable) { + return req + .bimap(TraceUtil.withFunctionTrace(this.checkHealth)) + .bimap(TraceUtil.withMetricTrace(healthCheckMetric)) + .flatMap((r) => r.move(HealthCheckInput.CHECK).map((input) => this.check(input))) + .peek( + TraceUtil.promiseify((h) => + h.get().fold(({ isLeft, value }) => { + if (!isLeft) { + h.trace.trace(healthCheckMetric.success); + return; + } + h.trace.trace(healthCheckMetric.failure); + h.trace.addTrace(LogLevel.ERROR).trace(value); + }), + ), ) - .fold( - ({ isLeft, value: message }) => - new JsonResponse(req, message, { - status: isLeft ? 500 : 200, - }), - ), - ), - ) - .get(); - } + .map( + TraceUtil.promiseify((h) => + h + .get() + .mapBoth( + () => 'oh no, i need to eat more vegetables (。•́︿•̀。)...', + () => 'think im healthy!! (✿˘◡˘) ready to do work~', + ) + .fold( + ({ isLeft, value: message }) => + new JsonResponse(req, message, { + status: isLeft ? 500 : 200, + }), + ), + ), + ) + .get(); + } } diff --git a/u/server/activity/index.ts b/u/server/activity/index.ts new file mode 100644 index 0000000..fa0a6b2 --- /dev/null +++ b/u/server/activity/index.ts @@ -0,0 +1,8 @@ +import type { ITraceable, PenguenoRequest, PenguenoResponse, ServerTrace } from '@emprespresso/pengueno'; + +export interface IActivity { + (req: ITraceable): Promise; +} + +export * from './health.js'; +export * from './fourohfour.js'; diff --git a/u/server/activity/mod.ts b/u/server/activity/mod.ts deleted file mode 100644 index 82d8ec4..0000000 --- a/u/server/activity/mod.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { - ITraceable, - PenguenoRequest, - PenguenoResponse, - ServerTrace, -} from "@emprespresso/pengueno"; - -export interface IActivity { - (req: ITraceable): Promise; -} - -export * from "./health.ts"; -export * from "./fourohfour.ts"; diff --git a/u/server/filter/index.ts b/u/server/filter/index.ts new file mode 100644 index 0000000..62a584d --- /dev/null +++ b/u/server/filter/index.ts @@ -0,0 +1,34 @@ +import { + type IEither, + type ITraceable, + LogLevel, + type PenguenoRequest, + type ServerTrace, +} from '@emprespresso/pengueno'; + +export enum ErrorSource { + USER = LogLevel.WARN, + SYSTEM = LogLevel.ERROR, +} + +export class PenguenoError extends Error { + public readonly source: ErrorSource; + constructor( + override readonly message: string, + public readonly status: number, + ) { + super(message); + this.source = Math.floor(status / 100) === 4 ? ErrorSource.USER : ErrorSource.SYSTEM; + } +} + +export interface RequestFilter< + T, + Err extends PenguenoError = PenguenoError, + RIn = ITraceable, +> { + (req: RIn): Promise>; +} + +export * from './method.js'; +export * from './json.js'; diff --git a/u/server/filter/json.ts b/u/server/filter/json.ts index 145d1be..527d483 100644 --- a/u/server/filter/json.ts +++ b/u/server/filter/json.ts @@ -1,54 +1,50 @@ import { - Either, - type IEither, - type ITraceable, - LogLevel, - Metric, - PenguenoError, - type PenguenoRequest, - type RequestFilter, - type ServerTrace, - TraceUtil, -} from "@emprespresso/pengueno"; + Either, + type IEither, + type ITraceable, + LogLevel, + Metric, + PenguenoError, + type PenguenoRequest, + type RequestFilter, + type ServerTrace, + TraceUtil, +} from '@emprespresso/pengueno'; export interface JsonTransformer { - (json: ITraceable): IEither; + (json: ITraceable): IEither; } -const ParseJsonMetric = Metric.fromName("JsonParse"); +const ParseJsonMetric = Metric.fromName('JsonParse'); export const jsonModel = - ( - jsonTransformer: JsonTransformer, - ): RequestFilter => - (r: ITraceable) => - r - .bimap(TraceUtil.withFunctionTrace(jsonModel)) - .bimap(TraceUtil.withMetricTrace(ParseJsonMetric)) - .map((j) => - Either.fromFailableAsync(() => j.get().json()).then( - (either) => - either.mapLeft((errReason) => { - j.trace.addTrace(LogLevel.WARN).trace(errReason); - return new PenguenoError( - "seems to be invalid JSON (>//<) can you fix?", - 400, - ); - }), - ), - ) - .peek( - TraceUtil.promiseify((traceableEither) => - traceableEither.get().fold(({ isLeft }) => - traceableEither.trace.trace(ParseJsonMetric[isLeft ? "failure" : "success"]) - ), - ), - ) - .map( - TraceUtil.promiseify((traceableEitherJson) => - traceableEitherJson - .get() - .mapRight((j) => traceableEitherJson.move(j)) - .flatMap(jsonTransformer), - ), - ) - .get(); + (jsonTransformer: JsonTransformer): RequestFilter => + (r: ITraceable) => + r + .bimap(TraceUtil.withFunctionTrace(jsonModel)) + .bimap(TraceUtil.withMetricTrace(ParseJsonMetric)) + .map((j) => + Either.fromFailableAsync(>j.get().json()).then((either) => + either.mapLeft((errReason) => { + j.trace.addTrace(LogLevel.WARN).trace(errReason); + return new PenguenoError('seems to be invalid JSON (>//<) can you fix?', 400); + }), + ), + ) + .peek( + TraceUtil.promiseify((traceableEither) => + traceableEither + .get() + .fold(({ isLeft }) => + traceableEither.trace.trace(ParseJsonMetric[isLeft ? 'failure' : 'success']), + ), + ), + ) + .map( + TraceUtil.promiseify((traceableEitherJson) => + traceableEitherJson + .get() + .mapRight((j) => traceableEitherJson.move(j)) + .flatMap(jsonTransformer), + ), + ) + .get(); diff --git a/u/server/filter/method.ts b/u/server/filter/method.ts index 9901c6f..5ca5716 100644 --- a/u/server/filter/method.ts +++ b/u/server/filter/method.ts @@ -1,43 +1,32 @@ import { - Either, - type ITraceable, - LogLevel, - PenguenoError, - type PenguenoRequest, - type RequestFilter, - type ServerTrace, - TraceUtil, -} from "@emprespresso/pengueno"; + Either, + type ITraceable, + LogLevel, + PenguenoError, + type PenguenoRequest, + type RequestFilter, + type ServerTrace, + TraceUtil, +} from '@emprespresso/pengueno'; -type HttpMethod = - | "POST" - | "GET" - | "HEAD" - | "PUT" - | "DELETE" - | "CONNECT" - | "OPTIONS" - | "TRACE" - | "PATCH"; +type HttpMethod = 'POST' | 'GET' | 'HEAD' | 'PUT' | 'DELETE' | 'CONNECT' | 'OPTIONS' | 'TRACE' | 'PATCH'; export const requireMethod = - (methods: Array): RequestFilter => - (req: ITraceable) => - req - .bimap(TraceUtil.withFunctionTrace(requireMethod)) - .move(Promise.resolve(req.get())) - .map( - TraceUtil.promiseify((t) => { - const { method: _method } = t.get(); - const method = _method; - if (!methods.includes(method)) { - const msg = "that's not how you pet me (⋟﹏⋞)~"; - t.trace.addTrace(LogLevel.WARN).trace(msg); - return Either.left( - new PenguenoError(msg, 405), - ); - } - return Either.right(method); - }), - ) - .get(); + (methods: Array): RequestFilter => + (req: ITraceable) => + req + .bimap(TraceUtil.withFunctionTrace(requireMethod)) + .move(Promise.resolve(req.get())) + .map( + TraceUtil.promiseify((t) => { + const { method: _method } = t.get(); + const method = _method; + if (!methods.includes(method)) { + const msg = "that's not how you pet me (⋟﹏⋞)~"; + t.trace.addTrace(LogLevel.WARN).trace(msg); + return Either.left(new PenguenoError(msg, 405)); + } + return Either.right(method); + }), + ) + .get(); diff --git a/u/server/filter/mod.ts b/u/server/filter/mod.ts deleted file mode 100644 index 0e0a4cb..0000000 --- a/u/server/filter/mod.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { - type IEither, - type ITraceable, - LogLevel, - type PenguenoRequest, - type ServerTrace, -} from "@emprespresso/pengueno"; - -export enum ErrorSource { - USER = LogLevel.WARN, - SYSTEM = LogLevel.ERROR, -} - -export class PenguenoError extends Error { - public readonly source: ErrorSource; - constructor( - override readonly message: string, - public readonly status: number, - ) { - super(message); - this.source = - Math.floor(status / 100) === 4 ? ErrorSource.USER : ErrorSource.SYSTEM; - } -} - -export interface RequestFilter< - T, - Err extends PenguenoError = PenguenoError, - RIn = ITraceable, -> { - (req: RIn): Promise>; -} - -export * from "./method.ts"; -export * from "./json.ts"; diff --git a/u/server/index.ts b/u/server/index.ts new file mode 100644 index 0000000..17cbbdf --- /dev/null +++ b/u/server/index.ts @@ -0,0 +1,7 @@ +import type { LogMetricTraceSupplier } from '@emprespresso/pengueno'; +export type ServerTrace = LogMetricTraceSupplier; + +export * from './activity/index.js'; +export * from './filter/index.js'; +export * from './response.js'; +export * from './request.js'; diff --git a/u/server/mod.ts b/u/server/mod.ts deleted file mode 100644 index 866b5f9..0000000 --- a/u/server/mod.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { LogMetricTraceSupplier } from "@emprespresso/pengueno"; -export type ServerTrace = LogMetricTraceSupplier; - -export * from "./activity/mod.ts"; -export * from "./filter/mod.ts"; -export * from "./response.ts"; -export * from "./request.ts"; diff --git a/u/server/request.ts b/u/server/request.ts index 72e812a..10610f1 100644 --- a/u/server/request.ts +++ b/u/server/request.ts @@ -1,49 +1,39 @@ -import { LogMetricTraceable, LogTraceable } from "@emprespresso/pengueno"; -import { TraceUtil } from "../trace/util.ts"; +import { TraceUtil, LogMetricTraceable, LogTraceable } from '@emprespresso/pengueno'; -const greetings = [ - "hewwo :D", - "hiya cutie", - "boop!", - "sending virtual hugs!", - "stay pawsitive", -]; -const penguenoGreeting = () => - greetings[Math.floor(Math.random() * greetings.length)]; +const greetings = ['hewwo :D', 'hiya cutie', 'boop!', 'sending virtual hugs!', 'stay pawsitive']; +const penguenoGreeting = () => greetings[Math.floor(Math.random() * greetings.length)]; export class PenguenoRequest extends Request { - private constructor( - _input: Request, - public readonly id: string, - public readonly at: Date, - ) { - super(_input); - } + private constructor( + _input: Request, + public readonly id: string, + public readonly at: Date, + ) { + super(_input); + } - public baseResponseHeaders(): Record { - const ServerRequestTime = this.at.getTime(); - const ServerResponseTime = Date.now(); - const DeltaTime = ServerResponseTime - ServerRequestTime; - const RequestId = this.id; + public baseResponseHeaders(): Record { + const ServerRequestTime = this.at.getTime(); + const ServerResponseTime = Date.now(); + const DeltaTime = ServerResponseTime - ServerRequestTime; + const RequestId = this.id; - return Object.entries({ - RequestId, - ServerRequestTime, - ServerResponseTime, - DeltaTime, - Hai: penguenoGreeting(), - }).reduce((acc, [key, val]) => ({ ...acc, [key]: val.toString() }), {}); - } + return Object.entries({ + RequestId, + ServerRequestTime, + ServerResponseTime, + DeltaTime, + Hai: penguenoGreeting(), + }).reduce((acc, [key, val]) => ({ ...acc, [key]: val!.toString() }), {}); + } - public static from(request: Request): LogMetricTraceable { - const id = crypto.randomUUID(); - const url = new URL(request.url); - const { pathname } = url; - const logTraceable = LogTraceable.of( - new PenguenoRequest(request, id, new Date()), - ).bimap( - TraceUtil.withTrace(`RequestId = ${id}, Method = ${request.method}, Path = ${pathname}`), - ); - return LogMetricTraceable.ofLogTraceable(logTraceable); - } + public static from(request: Request): LogMetricTraceable { + const id = crypto.randomUUID(); + const url = new URL(request.url); + const { pathname } = url; + const logTraceable = LogTraceable.of(new PenguenoRequest(request, id, new Date())).bimap( + TraceUtil.withTrace(`RequestId = ${id}, Method = ${request.method}, Path = ${pathname}`), + ); + return LogMetricTraceable.ofLogTraceable(logTraceable); + } } diff --git a/u/server/response.ts b/u/server/response.ts index 629dbb5..18d70b5 100644 --- a/u/server/response.ts +++ b/u/server/response.ts @@ -1,86 +1,83 @@ import { - type IEither, - isEither, - type ITraceable, - Metric, - type PenguenoRequest, - type ServerTrace, -} from "@emprespresso/pengueno"; + type IEither, + isEither, + type ITraceable, + Metric, + type PenguenoRequest, + type ServerTrace, +} from '@emprespresso/pengueno'; +export type BodyInit = + | ArrayBuffer + | AsyncIterable + | Blob + | FormData + | Iterable + | NodeJS.ArrayBufferView + | URLSearchParams + | null + | string; export type ResponseBody = object | string; -export type TResponseInit = ResponseInit & { - status: number; - headers?: Record; +export type TResponseInit = Omit & { + status: number; + headers?: Record; }; -const getResponse = ( - req: PenguenoRequest, - opts: TResponseInit, -): TResponseInit => { - return { - ...opts, - headers: { - ...req.baseResponseHeaders(), - ...opts?.headers, - "Content-Type": - (opts?.headers?.["Content-Type"] ?? "text/plain") + "; charset=utf-8", - }, - }; +const getResponse = (req: PenguenoRequest, opts: TResponseInit): ResponseInit => { + const baseHeaders = req.baseResponseHeaders(); + const optHeaders = opts.headers || {}; + + return { + ...opts, + headers: { + ...baseHeaders, + ...optHeaders, + 'Content-Type': (optHeaders['Content-Type'] ?? 'text/plain') + '; charset=utf-8', + } as Record, + }; }; -const ResponseCodeMetrics = [0, 1, 2, 3, 4, 5].map((x) => - Metric.fromName(`response.${x}xx`), -); -export const getResponseMetric = (status: number) => { - const index = Math.floor(status / 100); - return ResponseCodeMetrics[index] ?? ResponseCodeMetrics[5]; +const ResponseCodeMetrics = [0, 1, 2, 3, 4, 5].map((x) => Metric.fromName(`response.${x}xx`)); +export const getResponseMetrics = (status: number) => { + const index = Math.floor(status / 100); + return ResponseCodeMetrics.map((metric, i) => metric.count.withValue(i === index ? 1.0 : 0.0)); }; export class PenguenoResponse extends Response { - constructor( - req: ITraceable, - msg: BodyInit, - opts: TResponseInit, - ) { - const responseOpts = getResponse(req.get(), opts); - const resMetric = getResponseMetric(opts.status); - req.trace.trace(resMetric.count.withValue(1.0)); - responseOpts.headers; - super(msg, responseOpts); - } + constructor(req: ITraceable, msg: BodyInit, opts: TResponseInit) { + const responseOpts = getResponse(req.get(), opts); + for (const metric of getResponseMetrics(opts.status)) { + req.trace.trace(metric); + } + super(msg, responseOpts); + } } export class JsonResponse extends PenguenoResponse { - constructor( - req: ITraceable, - e: BodyInit | IEither, - opts: TResponseInit, - ) { - const optsWithJsonContentType = { - ...opts, - headers: { - ...opts?.headers, - "Content-Type": "application/json", - }, - }; - if (isEither(e)) { - super( - req, - JSON.stringify( - e.fold(({ isLeft, value }) => - isLeft ? { error: value } : { ok: value }, - ), - ), - optsWithJsonContentType, - ); - return; + constructor( + req: ITraceable, + e: BodyInit | IEither, + opts: TResponseInit, + ) { + const optsWithJsonContentType: TResponseInit = { + ...opts, + headers: { + ...opts.headers, + 'Content-Type': 'application/json', + }, + }; + if (isEither(e)) { + super( + req, + JSON.stringify(e.fold(({ isLeft, value }) => (isLeft ? { error: value } : { ok: value }))), + optsWithJsonContentType, + ); + return; + } + super( + req, + JSON.stringify(Math.floor(opts.status / 100) > 4 ? { error: e } : { ok: e }), + optsWithJsonContentType, + ); } - super( - req, - JSON.stringify( - Math.floor(opts.status / 100) > 4 ? { error: e } : { ok: e }, - ), - optsWithJsonContentType, - ); - } } diff --git a/u/trace/index.ts b/u/trace/index.ts new file mode 100644 index 0000000..18da87a --- /dev/null +++ b/u/trace/index.ts @@ -0,0 +1,5 @@ +export * from './itrace.js'; +export * from './util.js'; +export * from './logger.js'; +export * from './metrics.js'; +export * from './trace.js'; diff --git a/u/trace/itrace.ts b/u/trace/itrace.ts index fcfbe32..8cf123a 100644 --- a/u/trace/itrace.ts +++ b/u/trace/itrace.ts @@ -1,90 +1,72 @@ -import type { Mapper, SideEffect, Supplier } from "@emprespresso/pengueno"; +import type { Mapper, SideEffect, Supplier } from '@emprespresso/pengueno'; // the "thing" every Trace writer must "trace()" type BaseTraceWith = string; export type ITraceWith = BaseTraceWith | T; export interface ITrace { - addTrace: Mapper, ITrace>; - trace: SideEffect>; + addTrace: Mapper, ITrace>; + trace: SideEffect>; } export type ITraceableTuple = [T, BaseTraceWith | TraceWith]; -export type ITraceableMapper> = ( - w: W, -) => _T; +export type ITraceableMapper> = (w: W) => _T; export interface ITraceable { - readonly trace: ITrace; - get: Supplier; - move: <_T>(t: _T) => ITraceable<_T, Trace>; - map: <_T>(mapper: ITraceableMapper) => ITraceable<_T, Trace>; - bimap: <_T>( - mapper: ITraceableMapper< - T, - ITraceableTuple<_T, Array | Trace>, - Trace - >, - ) => ITraceable<_T, Trace>; - peek: (peek: ITraceableMapper) => ITraceable; - flatMap: <_T>( - mapper: ITraceableMapper, Trace>, - ) => ITraceable<_T, Trace>; - flatMapAsync<_T>( - mapper: ITraceableMapper>, Trace>, - ): ITraceable, Trace>; + readonly trace: ITrace; + get: Supplier; + move: <_T>(t: _T) => ITraceable<_T, Trace>; + map: <_T>(mapper: ITraceableMapper) => ITraceable<_T, Trace>; + bimap: <_T>(mapper: ITraceableMapper | Trace>, Trace>) => ITraceable<_T, Trace>; + peek: (peek: ITraceableMapper) => ITraceable; + flatMap: <_T>(mapper: ITraceableMapper, Trace>) => ITraceable<_T, Trace>; + flatMapAsync<_T>( + mapper: ITraceableMapper>, Trace>, + ): ITraceable, Trace>; } export class TraceableImpl implements ITraceable { - protected constructor( - private readonly item: T, - public readonly trace: ITrace, - ) {} + protected constructor( + private readonly item: T, + public readonly trace: ITrace, + ) {} - public map<_T>(mapper: ITraceableMapper) { - const result = mapper(this); - return new TraceableImpl(result, this.trace); - } + public map<_T>(mapper: ITraceableMapper) { + const result = mapper(this); + return new TraceableImpl(result, this.trace); + } - public flatMap<_T>( - mapper: ITraceableMapper, TraceWith>, - ): ITraceable<_T, TraceWith> { - return mapper(this); - } + public flatMap<_T>(mapper: ITraceableMapper, TraceWith>): ITraceable<_T, TraceWith> { + return mapper(this); + } - public flatMapAsync<_T>( - mapper: ITraceableMapper>, TraceWith>, - ): ITraceable, TraceWith> { - return new TraceableImpl( - mapper(this).then((t) => t.get()), - this.trace, - ); - } + public flatMapAsync<_T>( + mapper: ITraceableMapper>, TraceWith>, + ): ITraceable, TraceWith> { + return new TraceableImpl( + mapper(this).then((t) => t.get()), + this.trace, + ); + } - public peek(peek: ITraceableMapper) { - peek(this); - return this; - } + public peek(peek: ITraceableMapper) { + peek(this); + return this; + } - public move<_T>(t: _T): ITraceable<_T, TraceWith> { - return this.map(() => t); - } + public move<_T>(t: _T): ITraceable<_T, TraceWith> { + return this.map(() => t); + } - public bimap<_T>( - mapper: ITraceableMapper< - T, - ITraceableTuple<_T, Array | TraceWith>, - TraceWith - >, - ) { - const [item, trace] = mapper(this); - const traces = Array.isArray(trace) ? trace : [trace]; - return new TraceableImpl( - item, - traces.reduce((trace, _trace) => trace.addTrace(_trace), this.trace), - ); - } + public bimap<_T>(mapper: ITraceableMapper | TraceWith>, TraceWith>) { + const [item, trace] = mapper(this); + const traces = Array.isArray(trace) ? trace : [trace]; + return new TraceableImpl( + item, + traces.reduce((trace, _trace) => trace.addTrace(_trace), this.trace), + ); + } - public get() { - return this.item; - } + public get() { + return this.item; + } } diff --git a/u/trace/logger.ts b/u/trace/logger.ts index d8392eb..91432fe 100644 --- a/u/trace/logger.ts +++ b/u/trace/logger.ts @@ -1,112 +1,95 @@ -import { - isDebug, - type ITrace, - type ITraceWith, - type Supplier, -} from "@emprespresso/pengueno"; +import { isDebug, type ITrace, type ITraceWith, type Supplier } from '@emprespresso/pengueno'; export type LogTraceSupplier = ITraceWith | Error>; const defaultTrace = () => `TimeStamp = ${new Date().toISOString()}`; export class LogTrace implements ITrace { - constructor( - private readonly logger: ILogger = new LoggerImpl(), - private readonly traces: Array = [defaultTrace], - private readonly defaultLevel: LogLevel = LogLevel.INFO, - private readonly allowedLevels: Supplier< - Array - > = defaultAllowedLevels, - ) {} + constructor( + private readonly logger: ILogger = new LoggerImpl(), + private readonly traces: Array = [defaultTrace], + private readonly defaultLevel: LogLevel = LogLevel.INFO, + private readonly allowedLevels: Supplier> = defaultAllowedLevels, + ) {} - public addTrace(trace: LogTraceSupplier): ITrace { - return new LogTrace( - this.logger, - this.traces.concat(trace), - this.defaultLevel, - this.allowedLevels, - ); - } + public addTrace(trace: LogTraceSupplier): ITrace { + return new LogTrace(this.logger, this.traces.concat(trace), this.defaultLevel, this.allowedLevels); + } - public trace(trace: LogTraceSupplier) { - const { traces, level: _level } = this.foldTraces( - this.traces.concat(trace), - ); - if (!this.allowedLevels().includes(_level)) return; + public trace(trace: LogTraceSupplier) { + const { traces, level: _level } = this.foldTraces(this.traces.concat(trace)); + if (!this.allowedLevels().includes(_level)) return; - const level = _level === LogLevel.UNKNOWN ? this.defaultLevel : _level; - this.logger.log(level, ...traces); - } + const level = _level === LogLevel.UNKNOWN ? this.defaultLevel : _level; + this.logger.log(level, ...traces); + } - private foldTraces(_traces: Array) { - const _logTraces = _traces.map((trace) => - typeof trace === "function" ? trace() : trace, - ); - const _level = _logTraces - .filter((trace) => isLogLevel(trace)) - .reduce((acc, level) => Math.max(logLevelOrder.indexOf(level), acc), -1); - const level = logLevelOrder[_level] ?? LogLevel.UNKNOWN; + private foldTraces(_traces: Array) { + const _logTraces = _traces.map((trace) => (typeof trace === 'function' ? trace() : trace)); + const _level = _logTraces + .filter((trace) => isLogLevel(trace)) + .reduce((acc, level) => Math.max(logLevelOrder.indexOf(level), acc), -1); + const level = logLevelOrder[_level] ?? LogLevel.UNKNOWN; - const traces = _logTraces.filter((trace) => !isLogLevel(trace)).map((trace) => { - if (typeof trace === 'object') { - return `TracedException.Name = ${trace.name}, TracedException.Message = ${trace.message}, TracedException.Stack = ${trace.stack}` - } - return trace; - }); - return { - level, - traces, - }; - } + const traces = _logTraces + .filter((trace) => !isLogLevel(trace)) + .map((trace) => { + if (typeof trace === 'object') { + return `TracedException.Name = ${trace.name}, TracedException.Message = ${trace.message}, TracedException.Stack = ${trace.stack}`; + } + return trace; + }); + return { + level, + traces, + }; + } } export enum LogLevel { - UNKNOWN = "UNKNOWN", - INFO = "INFO", - WARN = "WARN", - DEBUG = "DEBUG", - ERROR = "ERROR", - SYS = "SYS", + UNKNOWN = 'UNKNOWN', + INFO = 'INFO', + WARN = 'WARN', + DEBUG = 'DEBUG', + ERROR = 'ERROR', + SYS = 'SYS', } -const logLevelOrder: Array = [ - LogLevel.DEBUG, - LogLevel.INFO, - LogLevel.WARN, - LogLevel.ERROR, - LogLevel.SYS, -]; +const logLevelOrder: Array = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR, LogLevel.SYS]; export const isLogLevel = (l: unknown): l is LogLevel => - typeof l === "string" && logLevelOrder.some((level) => level === l); - + typeof l === 'string' && logLevelOrder.some((level) => level === l); const defaultAllowedLevels = () => - [ - LogLevel.UNKNOWN, - ...(isDebug() ? [LogLevel.DEBUG] : []), - LogLevel.INFO, - LogLevel.WARN, - LogLevel.ERROR, - LogLevel.SYS, - ] as Array; + [ + LogLevel.UNKNOWN, + ...(isDebug() ? [LogLevel.DEBUG] : []), + LogLevel.INFO, + LogLevel.WARN, + LogLevel.ERROR, + LogLevel.SYS, + ] as Array; export interface ILogger { - readonly log: (level: LogLevel, ...args: string[]) => void; + readonly log: (level: LogLevel, ...args: string[]) => void; } class LoggerImpl implements ILogger { private readonly textEncoder = new TextEncoder(); public log(level: LogLevel, ...trace: string[]) { - const message = JSON.stringify({ - level, - trace, - }, null, 4); + const message = JSON.stringify( + { + level, + trace, + }, + null, + 4, + ); const styled = `${this.getStyle(level)}${message}${ANSI.RESET}\n`; - this.getStream(level).writeSync(this.textEncoder.encode(styled)); + this.getStream(level)(this.textEncoder.encode(styled)); } private getStream(level: LogLevel) { if (level === LogLevel.ERROR) { - return Deno.stderr; + return console.error; } - return Deno.stdout; + return console.log; } private getStyle(level: LogLevel) { @@ -127,17 +110,17 @@ class LoggerImpl implements ILogger { } export const ANSI = { - RESET: "\x1b[0m", - BOLD: "\x1b[1m", - DIM: "\x1b[2m", - RED: "\x1b[31m", - GREEN: "\x1b[32m", - YELLOW: "\x1b[33m", - BLUE: "\x1b[34m", - MAGENTA: "\x1b[35m", - CYAN: "\x1b[36m", - WHITE: "\x1b[37m", - BRIGHT_RED: "\x1b[91m", - BRIGHT_YELLOW: "\x1b[93m", - GRAY: "\x1b[90m", + RESET: '\x1b[0m', + BOLD: '\x1b[1m', + DIM: '\x1b[2m', + RED: '\x1b[31m', + GREEN: '\x1b[32m', + YELLOW: '\x1b[33m', + BLUE: '\x1b[34m', + MAGENTA: '\x1b[35m', + CYAN: '\x1b[36m', + WHITE: '\x1b[37m', + BRIGHT_RED: '\x1b[91m', + BRIGHT_YELLOW: '\x1b[93m', + GRAY: '\x1b[90m', }; diff --git a/u/trace/metrics.ts b/u/trace/metrics.ts index 822fc38..2301afd 100644 --- a/u/trace/metrics.ts +++ b/u/trace/metrics.ts @@ -1,151 +1,140 @@ import { - isObject, - type ITrace, - type ITraceWith, - type Mapper, - type SideEffect, - type Supplier, -} from "@emprespresso/pengueno"; + isObject, + type ITrace, + type ITraceWith, + type Mapper, + type SideEffect, + type Supplier, +} from '@emprespresso/pengueno'; export enum Unit { - COUNT = "COUNT", - MILLISECONDS = "MILLISECONDS", + COUNT = 'COUNT', + MILLISECONDS = 'MILLISECONDS', } export interface IMetric { - readonly count: IEmittableMetric; - readonly time: IEmittableMetric; - readonly failure?: IMetric; - readonly success?: IMetric; - readonly warn?: IMetric; - readonly children: Supplier>; - - readonly _tag: "IMetric"; + readonly count: IEmittableMetric; + readonly time: IEmittableMetric; + readonly failure: undefined | IMetric; + readonly success: undefined | IMetric; + readonly warn: undefined | IMetric; + readonly children: Supplier>; + + readonly _tag: 'IMetric'; } -export const isIMetric = (t: unknown): t is IMetric => - isObject(t) && "_tag" in t && t._tag === "IMetric"; +export const isIMetric = (t: unknown): t is IMetric => isObject(t) && '_tag' in t && t._tag === 'IMetric'; export interface IEmittableMetric { - readonly name: string; - readonly unit: Unit; - withValue: Mapper; + readonly name: string; + readonly unit: Unit; + withValue: Mapper; } export class EmittableMetric implements IEmittableMetric { - constructor( - public readonly name: string, - public readonly unit: Unit, - ) {} - - public withValue(value: number): MetricValue { - return { - name: this.name, - unit: this.unit, - emissionTimestamp: Date.now(), - value, - _tag: "MetricValue", - }; - } + constructor( + public readonly name: string, + public readonly unit: Unit, + ) {} + + public withValue(value: number): MetricValue { + return { + name: this.name, + unit: this.unit, + emissionTimestamp: Date.now(), + value, + _tag: 'MetricValue', + }; + } } export class Metric implements IMetric { - constructor( - public readonly count: IEmittableMetric, - public readonly time: IEmittableMetric, - public readonly failure?: Metric, - public readonly success?: Metric, - public readonly warn?: Metric, - public readonly _tag: "IMetric" = "IMetric", - ) {} - - public children() { - return [this.failure, this.success, this.warn].filter( - (x) => x, - ) as IMetric[]; - } - - static fromName(name: string, addChildren = true): Metric { - return new Metric( - new EmittableMetric(`${name}.count`, Unit.COUNT), - new EmittableMetric(`${name}.elapsed`, Unit.MILLISECONDS), - addChildren ? Metric.fromName(`${name}.failure`, false) : undefined, - addChildren ? Metric.fromName(`${name}.success`, false) : undefined, - addChildren ? Metric.fromName(`${name}.warn`, false) : undefined, - ); - } + constructor( + public readonly count: IEmittableMetric, + public readonly time: IEmittableMetric, + public readonly failure: undefined | Metric = undefined, + public readonly success: undefined | Metric = undefined, + public readonly warn: undefined | Metric = undefined, + public readonly _tag: 'IMetric' = 'IMetric', + ) {} + + public children() { + return [this.failure, this.success, this.warn].filter((x) => x) as IMetric[]; + } + + static fromName(name: string, addChildren = true): Metric { + return new Metric( + new EmittableMetric(`${name}.count`, Unit.COUNT), + new EmittableMetric(`${name}.elapsed`, Unit.MILLISECONDS), + addChildren ? Metric.fromName(`${name}.failure`, false) : undefined, + addChildren ? Metric.fromName(`${name}.success`, false) : undefined, + addChildren ? Metric.fromName(`${name}.warn`, false) : undefined, + ); + } } export interface MetricValue { - readonly name: string; - readonly unit: Unit; - readonly value: number; - readonly emissionTimestamp: number; - readonly _tag: "MetricValue"; + readonly name: string; + readonly unit: Unit; + readonly value: number; + readonly emissionTimestamp: number; + readonly _tag: 'MetricValue'; } -export const isMetricValue = (t: unknown): t is MetricValue => - isObject(t) && "_tag" in t && t._tag === "MetricValue"; +export const isMetricValue = (t: unknown): t is MetricValue => isObject(t) && '_tag' in t && t._tag === 'MetricValue'; -export const isMetricsTraceSupplier = (t: unknown): t is MetricsTraceSupplier => - isMetricValue(t) || isIMetric(t); +export const isMetricsTraceSupplier = (t: unknown): t is MetricsTraceSupplier => isMetricValue(t) || isIMetric(t); -export type MetricsTraceSupplier = ITraceWith< - IMetric | MetricValue | undefined ->; +export type MetricsTraceSupplier = ITraceWith; type MetricTracingTuple = [IMetric, Date]; export class MetricsTrace implements ITrace { - constructor( - private readonly metricConsumer: SideEffect>, - private readonly tracing: Array = [], - private readonly flushed: Set = new Set(), - ) {} - - public addTrace(trace: MetricsTraceSupplier) { - if (!isIMetric(trace)) return this; - return new MetricsTrace(this.metricConsumer)._nowTracing(trace); - } - - public trace(metric: MetricsTraceSupplier) { - if (typeof metric === "undefined" || typeof metric === "string") - return this; - if (isMetricValue(metric)) { - this.metricConsumer([metric]); - return this; + constructor( + private readonly metricConsumer: SideEffect>, + private readonly tracing: Array = [], + private readonly flushed: Set = new Set(), + ) {} + + public addTrace(trace: MetricsTraceSupplier) { + if (!isIMetric(trace)) return this; + return new MetricsTrace(this.metricConsumer)._nowTracing(trace); } - const foundMetricValues = this.tracing - .flatMap(([tracing, startedTracing]) => - [tracing, ...tracing.children()] - .filter((_tracing) => metric === _tracing) - .flatMap((metric) => [ - this.addMetric(metric, startedTracing), - this.addMetric(tracing, startedTracing), - ]), - ) - .flatMap((values) => values); - - if (foundMetricValues.length === 0) { - return this._nowTracing(metric); + public trace(metric: MetricsTraceSupplier) { + if (typeof metric === 'undefined' || typeof metric === 'string') return this; + if (isMetricValue(metric)) { + this.metricConsumer([metric]); + return this; + } + + const foundMetricValues = this.tracing + .flatMap(([tracing, startedTracing]) => + [tracing, ...tracing.children()] + .filter((_tracing) => metric === _tracing) + .flatMap((metric) => [ + this.addMetric(metric, startedTracing), + this.addMetric(tracing, startedTracing), + ]), + ) + .flatMap((values) => values); + + if (foundMetricValues.length === 0) { + return this._nowTracing(metric); + } + + this.metricConsumer(foundMetricValues); + return this; } - this.metricConsumer(foundMetricValues); - return this; - } + private addMetric(metric: IMetric, startedTracing: Date): Array { + if (this.flushed.has(metric)) { + return []; + } - private addMetric(metric: IMetric, startedTracing: Date): Array { - if (this.flushed.has(metric)) { - return []; + this.flushed.add(metric); + return [metric.count.withValue(1.0), metric.time.withValue(Date.now() - startedTracing.getTime())]; } - this.flushed.add(metric); - return [ - metric.count.withValue(1.0), - metric.time.withValue(Date.now() - startedTracing.getTime()), - ]; - } - - private _nowTracing(metric?: IMetric): MetricsTrace { - if (!metric) return this; - this.tracing.push([metric, new Date()]); - return this; - } + private _nowTracing(metric?: IMetric): MetricsTrace { + if (!metric) return this; + this.tracing.push([metric, new Date()]); + return this; + } } diff --git a/u/trace/mod.ts b/u/trace/mod.ts deleted file mode 100644 index 0f9b61b..0000000 --- a/u/trace/mod.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./itrace.ts"; -export * from "./util.ts"; -export * from "./logger.ts"; -export * from "./metrics.ts"; -export * from "./trace.ts"; diff --git a/u/trace/trace.ts b/u/trace/trace.ts index 5629c28..acc116f 100644 --- a/u/trace/trace.ts +++ b/u/trace/trace.ts @@ -1,86 +1,69 @@ import { - isMetricsTraceSupplier, - type ITrace, - type ITraceable, - type ITraceWith, - LogLevel, - LogTrace, - type LogTraceSupplier, - MetricsTrace, - type MetricsTraceSupplier, - type MetricValue, - TraceableImpl, -} from "@emprespresso/pengueno"; + isMetricsTraceSupplier, + type ITrace, + type ITraceable, + type ITraceWith, + LogLevel, + LogTrace, + type LogTraceSupplier, + MetricsTrace, + type MetricsTraceSupplier, + type MetricValue, + TraceableImpl, +} from '@emprespresso/pengueno'; export class LogTraceable extends TraceableImpl { - public static LogTrace = new LogTrace(); - static of(t: T) { - return new LogTraceable(t, LogTraceable.LogTrace); - } + public static LogTrace = new LogTrace(); + static of(t: T) { + return new LogTraceable(t, LogTraceable.LogTrace); + } } -const getEmbeddedMetricConsumer = - (logTrace: ITrace) => - (metrics: Array) => +const getEmbeddedMetricConsumer = (logTrace: ITrace) => (metrics: Array) => logTrace.addTrace(LogLevel.SYS).trace(`Metrics = ${JSON.stringify(metrics)}`); -export class EmbeddedMetricsTraceable extends TraceableImpl< - T, - MetricsTraceSupplier -> { - public static MetricsTrace = new MetricsTrace( - getEmbeddedMetricConsumer(LogTraceable.LogTrace), - ); +export class EmbeddedMetricsTraceable extends TraceableImpl { + public static MetricsTrace = new MetricsTrace(getEmbeddedMetricConsumer(LogTraceable.LogTrace)); - static of(t: T, metricsTrace = EmbeddedMetricsTraceable.MetricsTrace) { - return new EmbeddedMetricsTraceable(t, metricsTrace); - } + static of(t: T, metricsTrace = EmbeddedMetricsTraceable.MetricsTrace) { + return new EmbeddedMetricsTraceable(t, metricsTrace); + } } -export type LogMetricTraceSupplier = ITraceWith< - LogTraceSupplier | MetricsTraceSupplier ->; +export type LogMetricTraceSupplier = ITraceWith; export class LogMetricTrace implements ITrace { - constructor( - private logTrace: ITrace, - private metricsTrace: ITrace, - ) {} + constructor( + private logTrace: ITrace, + private metricsTrace: ITrace, + ) {} - public addTrace( - trace: LogTraceSupplier | MetricsTraceSupplier, - ): LogMetricTrace { - if (isMetricsTraceSupplier(trace)) { - this.metricsTrace = this.metricsTrace.addTrace(trace); - return this; + public addTrace(trace: LogTraceSupplier | MetricsTraceSupplier): LogMetricTrace { + if (isMetricsTraceSupplier(trace)) { + this.metricsTrace = this.metricsTrace.addTrace(trace); + return this; + } + this.logTrace = this.logTrace.addTrace(trace); + return this; } - this.logTrace = this.logTrace.addTrace(trace); - return this; - } - public trace(trace: LogTraceSupplier | MetricsTraceSupplier) { - if (isMetricsTraceSupplier(trace)) { - this.metricsTrace.trace(trace); - return this; + public trace(trace: LogTraceSupplier | MetricsTraceSupplier) { + if (isMetricsTraceSupplier(trace)) { + this.metricsTrace.trace(trace); + return this; + } + this.logTrace.trace(trace); + return this; } - this.logTrace.trace(trace); - return this; - } } -export class LogMetricTraceable extends TraceableImpl< - T, - MetricsTraceSupplier | LogTraceSupplier -> { - static ofLogTraceable(t: ITraceable) { - const metricsTrace = new MetricsTrace(getEmbeddedMetricConsumer(t.trace)); - return new LogMetricTraceable( - t.get(), - new LogMetricTrace(t.trace, metricsTrace), - ); - } +export class LogMetricTraceable extends TraceableImpl { + static ofLogTraceable(t: ITraceable) { + const metricsTrace = new MetricsTrace(getEmbeddedMetricConsumer(t.trace)); + return new LogMetricTraceable(t.get(), new LogMetricTrace(t.trace, metricsTrace)); + } - static of(t: T) { - const logTrace = LogTraceable.of(t); - return LogMetricTraceable.ofLogTraceable(logTrace); - } + static of(t: T) { + const logTrace = LogTraceable.of(t); + return LogMetricTraceable.ofLogTraceable(logTrace); + } } diff --git a/u/trace/util.ts b/u/trace/util.ts index e2200b9..db1db63 100644 --- a/u/trace/util.ts +++ b/u/trace/util.ts @@ -1,47 +1,45 @@ import { -ANSI, - type Callable, - type IMetric, - type ITraceableMapper, - type ITraceableTuple, - type MetricsTraceSupplier, -} from "@emprespresso/pengueno"; + ANSI, + type Callable, + type IMetric, + type ITraceableMapper, + type ITraceableTuple, + type MetricsTraceSupplier, +} from '@emprespresso/pengueno'; export class TraceUtil { - static withTrace( - trace: string, - ansi?: Array - ): ITraceableMapper>, Trace> { - if (ansi) { - return (t) => [t.get(), `${ansi.join("")}${trace}${ANSI.RESET}`]; - } - return (t) => [t.get(), trace]; - } + static withTrace( + trace: string, + ansi?: Array, + ): ITraceableMapper>, Trace> { + if (ansi) { + return (t) => [t.get(), `${ansi.join('')}${trace}${ANSI.RESET}`]; + } + return (t) => [t.get(), trace]; + } - static withMetricTrace( - metric: IMetric, - ): ITraceableMapper>, Trace> { - return (t) => [t.get(), metric as Trace]; - } + static withMetricTrace( + metric: IMetric, + ): ITraceableMapper>, Trace> { + return (t) => [t.get(), metric as Trace]; + } - static withFunctionTrace( - f: F, - ): ITraceableMapper>, Trace> { - return TraceUtil.withTrace(`fn.${f.name}`); - } + static withFunctionTrace( + f: F, + ): ITraceableMapper>, Trace> { + return TraceUtil.withTrace(`fn.${f.name}`); + } - static withClassTrace( - c: C, - ): ITraceableMapper>, Trace> { - return TraceUtil.withTrace(`class.${c.constructor.name}`); - } + static withClassTrace( + c: C, + ): ITraceableMapper>, Trace> { + return TraceUtil.withTrace(`class.${c.constructor.name}`); + } - static promiseify( - mapper: ITraceableMapper, - ): ITraceableMapper, Promise, Trace> { - return (traceablePromise) => - traceablePromise - .flatMapAsync(async (t) => t.move(await t.get()).map(mapper)) - .get(); - } + static promiseify( + mapper: ITraceableMapper, + ): ITraceableMapper, Promise, Trace> { + return (traceablePromise) => + traceablePromise.flatMapAsync(async (t) => t.move(await t.get()).map(mapper)).get(); + } } diff --git a/u/tsconfig.json b/u/tsconfig.json new file mode 100644 index 0000000..80e5ff4 --- /dev/null +++ b/u/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./", + "composite": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "noEmit": false, + "moduleResolution": "node" + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"] +} diff --git a/worker/deno.json b/worker/deno.json deleted file mode 100644 index c908330..0000000 --- a/worker/deno.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "@emprespresso/ci_worker", - "exports": "./mod.ts" -} diff --git a/worker/executor.ts b/worker/executor.ts index ea79995..f4b7906 100644 --- a/worker/executor.ts +++ b/worker/executor.ts @@ -1,101 +1,86 @@ import { - getStdout, - type ITraceable, - LogLevel, - type LogMetricTraceSupplier, - memoize, - Metric, - TraceUtil, - validateExecutionEntries, - Either, - type IEither, -} from "@emprespresso/pengueno"; -import type { Job, JobArgT, Pipeline } from "@emprespresso/ci_model"; + getStdout, + type ITraceable, + LogLevel, + type LogMetricTraceSupplier, + memoize, + Metric, + TraceUtil, + validateExecutionEntries, + Either, + type IEither, +} from '@emprespresso/pengueno'; +import type { Job, JobArgT, Pipeline } from '@emprespresso/ci_model'; // -- -- const jobTypeMetric = memoize((type: string) => Metric.fromName(`run.${type}`)); export const executeJob = (tJob: ITraceable) => - tJob - .bimap(TraceUtil.withMetricTrace(jobTypeMetric(tJob.get().type))) - .peek((tJob) => - tJob.trace.trace(`let's do this little job ok!! ${tJob.get()}`), - ) - .map((tJob) => - validateExecutionEntries(tJob.get().arguments) - .mapLeft((badEntries) => { - tJob.trace.addTrace(LogLevel.ERROR).trace(badEntries.toString()); - return new Error("invalid job arguments"); - }) - .flatMapAsync((args) => - getStdout(tJob.move(tJob.get().type), { env: args }), - ), - ) - .peek( - TraceUtil.promiseify((q) => - q.trace.trace( - q - .get() - .fold( - ({ isLeft }) => - jobTypeMetric(tJob.get().type)[isLeft ? "failure" : "success"], + tJob + .bimap(TraceUtil.withMetricTrace(jobTypeMetric(tJob.get().type))) + .peek((tJob) => tJob.trace.trace(`let's do this little job ok!! ${tJob.get()}`)) + .map((tJob) => + validateExecutionEntries(tJob.get().arguments) + .mapLeft((badEntries) => { + tJob.trace.addTrace(LogLevel.ERROR).trace(badEntries.toString()); + return new Error('invalid job arguments'); + }) + .flatMapAsync((args) => getStdout(tJob.move(tJob.get().type), { env: args })), + ) + .peek( + TraceUtil.promiseify((q) => + q.trace.trace( + q.get().fold(({ isLeft }) => jobTypeMetric(tJob.get().type)[isLeft ? 'failure' : 'success']), + ), ), - ), - ), - ) - .get(); + ) + .get(); // -- -- // -- -- -const pipelinesMetric = Metric.fromName("pipelines"); +const pipelinesMetric = Metric.fromName('pipelines'); export const executePipeline = ( - tPipeline: ITraceable, - baseEnv?: JobArgT, + tPipeline: ITraceable, + baseEnv?: JobArgT, ): Promise> => - tPipeline - .bimap(TraceUtil.withFunctionTrace(executePipeline)) - .bimap(TraceUtil.withMetricTrace(pipelinesMetric)) - .map(async (tJobs): Promise> => { - for (const [i, serialStage] of tJobs.get().serialJobs.entries()) { - tJobs.trace.trace( - `executing stage ${i}. do your best little stage :>\n${serialStage}`, - ); - const jobResults = await Promise.all( - serialStage.parallelJobs.map((job) => - tJobs - .bimap((_) => [job, `stage ${i}`]) - .map( - (tJob) => - { - ...tJob.get(), - arguments: { - ...baseEnv, - ...tJob.get().arguments, - }, - }, - ) - .map(executeJob) - .peek( - TraceUtil.promiseify((tEitherJobOutput) => - tEitherJobOutput - .get() - .mapRight((stdout) => - tEitherJobOutput.trace.addTrace("STDOUT").trace(stdout), + tPipeline + .bimap(TraceUtil.withFunctionTrace(executePipeline)) + .bimap(TraceUtil.withMetricTrace(pipelinesMetric)) + .map(async (tJobs): Promise> => { + for (const [i, serialStage] of tJobs.get().serialJobs.entries()) { + tJobs.trace.trace(`executing stage ${i}. do your best little stage :>\n${serialStage}`); + const jobResults = await Promise.all( + serialStage.parallelJobs.map((job) => + tJobs + .bimap((_) => [job, `stage ${i}`]) + .map( + (tJob) => + { + ...tJob.get(), + arguments: { + ...baseEnv, + ...tJob.get().arguments, + }, + }, + ) + .map(executeJob) + .peek( + TraceUtil.promiseify((tEitherJobOutput) => + tEitherJobOutput + .get() + .mapRight((stdout) => tEitherJobOutput.trace.addTrace('STDOUT').trace(stdout)), + ), + ) + .get(), ), - ), - ) - .get(), - ), - ); - const failures = jobResults.filter((e) => - e.fold(({ isLeft }) => isLeft), - ); - if (failures.length > 0) { - tJobs.trace.trace(pipelinesMetric.failure); - return Either.left(new Error(failures.toString())); - } - } - tJobs.trace.trace(pipelinesMetric.success); - return Either.right(undefined); - }) - .get(); + ); + const failures = jobResults.filter((e) => e.fold(({ isLeft }) => isLeft)); + if (failures.length > 0) { + tJobs.trace.trace(pipelinesMetric.failure); + return Either.left(new Error(failures.toString())); + } + } + tJobs.trace.trace(pipelinesMetric.success); + return Either.right(undefined); + }) + .get(); // -- -- diff --git a/worker/index.ts b/worker/index.ts new file mode 100644 index 0000000..9ad32b9 --- /dev/null +++ b/worker/index.ts @@ -0,0 +1,2 @@ +export * from './secret.js'; +export * from './executor.js'; diff --git a/worker/mod.ts b/worker/mod.ts deleted file mode 100644 index 97980a8..0000000 --- a/worker/mod.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./secret.ts"; -export * from "./executor.ts"; diff --git a/worker/package.json b/worker/package.json new file mode 100644 index 0000000..c49dfaf --- /dev/null +++ b/worker/package.json @@ -0,0 +1,28 @@ +{ + "name": "@emprespresso/ci_worker", + "version": "0.1.0", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "clean": "rm -rf dist", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "@emprespresso/pengueno": "*", + "@emprespresso/ci_model": "*" + }, + "files": [ + "dist/**/*", + "package.json", + "README.md" + ] +} diff --git a/worker/scripts/ansible_playbook.ts b/worker/scripts/ansible_playbook.ts index 0879dc5..c6d8f2c 100755 --- a/worker/scripts/ansible_playbook.ts +++ b/worker/scripts/ansible_playbook.ts @@ -1,113 +1,100 @@ -#!/usr/bin/env -S deno run --allow-env --allow-net --allow-run --allow-read --allow-write +#!/usr/bin/env node import { - Either, - getRequiredEnvVars, - getStdout, - type IEither, - LogTraceable, - LogMetricTraceable, - Metric, - prependWith, - TraceUtil, -} from "@emprespresso/pengueno"; -import type { AnsiblePlaybookJob } from "@emprespresso/ci_model"; -import { Bitwarden, type SecureNote } from "@emprespresso/ci_worker"; + Either, + getRequiredEnvVars, + getStdout, + type IEither, + LogTraceable, + LogMetricTraceable, + Metric, + prependWith, + TraceUtil, +} from '@emprespresso/pengueno'; +import type { AnsiblePlaybookJob } from '@emprespresso/ci_model'; +import { Bitwarden, type SecureNote } from '@emprespresso/ci_worker'; +import { writeFile, mkdtemp } from 'fs/promises'; +import { join } from 'path'; +import { tmpdir } from 'os'; -const eitherJob = getRequiredEnvVars([ - "path", - "playbooks", -]) - .mapRight((baseArgs) => ( - { - type: "ansible_playbook.ts", - arguments: baseArgs, - } - )); +const eitherJob = getRequiredEnvVars(['path', 'playbooks']).mapRight( + (baseArgs) => + { + type: 'ansible_playbook.ts', + arguments: baseArgs, + }, +); -const eitherVault = Bitwarden.getConfigFromEnvironment() - .mapRight((config) => new Bitwarden(config)); +const eitherVault = Bitwarden.getConfigFromEnvironment().mapRight((config) => new Bitwarden(config)); -const playbookMetric = Metric.fromName("ansiblePlaybook.playbook"); -const _logJob = LogTraceable.of(eitherJob).bimap(TraceUtil.withTrace("ansible_playbook")); -await LogMetricTraceable.ofLogTraceable(_logJob).bimap(TraceUtil.withMetricTrace(playbookMetric)) - .peek((tEitherJob) => - tEitherJob.trace.trace("starting ansible playbook job! (⑅˘꒳˘)") - ) - .map((tEitherJob) => - tEitherJob.get().flatMapAsync((job) => - eitherVault.flatMapAsync(async (vault) => { - const eitherKey = await vault.unlock(tEitherJob); - return eitherKey.mapRight((key) => ({ job, key, vault })); - }) +const playbookMetric = Metric.fromName('ansiblePlaybook.playbook'); +const _logJob = LogTraceable.of(eitherJob).bimap(TraceUtil.withTrace('ansible_playbook')); +await LogMetricTraceable.ofLogTraceable(_logJob) + .bimap(TraceUtil.withMetricTrace(playbookMetric)) + .peek((tEitherJob) => tEitherJob.trace.trace('starting ansible playbook job! (⑅˘꒳˘)')) + .map((tEitherJob) => + tEitherJob.get().flatMapAsync((job) => + eitherVault.flatMapAsync(async (vault) => { + const eitherKey = await vault.unlock(tEitherJob); + return eitherKey.mapRight((key) => ({ job, key, vault })); + }), + ), ) - ) - .map(async (tEitherJobVault) => { - tEitherJobVault.trace.trace( - "getting ansible secwets uwu~", - ); - const eitherJobVault = await tEitherJobVault.get(); - - const eitherSshKey = await eitherJobVault - .flatMapAsync(({ key, vault }) => - vault.fetchSecret(tEitherJobVault, key, "ssh_key") - ); - const eitherSshKeyFile = await eitherSshKey.mapRight(({ notes }) => notes) - .flatMapAsync(saveToTempFile); - const eitherAnsibleSecrets = await eitherJobVault - .flatMapAsync(({ key, vault }) => - vault.fetchSecret(tEitherJobVault, key, "ansible_playbooks") - ); - const eitherAnsibleSecretsFile = await eitherAnsibleSecrets.mapRight(( - { notes }, - ) => notes).flatMapAsync(saveToTempFile); + .map(async (tEitherJobVault) => { + tEitherJobVault.trace.trace('getting ansible secwets uwu~'); + const eitherJobVault = await tEitherJobVault.get(); - return eitherJobVault.flatMapAsync(async ({ job, vault, key }) => { - const eitherLocked = await vault.lock(tEitherJobVault, key); - return eitherLocked.flatMap((_locked) => - eitherSshKeyFile.flatMap((sshKeyFile) => - eitherAnsibleSecretsFile.mapRight((secretsFile) => ({ - job, - sshKeyFile, - secretsFile, - })) - ) - ); - }); - }) - .map(async (tEitherJobAndSecrets) => { - const eitherJobAndSecrets = await tEitherJobAndSecrets.get(); - return eitherJobAndSecrets.flatMapAsync( - ({ job, sshKeyFile, secretsFile }) => { - const volumes = [ - `${job.arguments.path}:/ansible`, - `${sshKeyFile}:/root/id_rsa`, - `${secretsFile}:/ansible/secrets.yml`, - ]; - const playbookCmd = - `ansible-playbook -e @secrets.yml ${job.arguments.playbooks}`; - const deployCmd = [ - "docker", - "run", - ...prependWith(volumes, "-v"), - "willhallonline/ansible:latest", - ...playbookCmd.split(" "), - ]; - tEitherJobAndSecrets.trace.trace( - `running ansible magic~ (◕ᴗ◕✿) ${deployCmd}`, + const eitherSshKey = await eitherJobVault.flatMapAsync(({ key, vault }) => + vault.fetchSecret(tEitherJobVault, key, 'ssh_key'), ); - return tEitherJobAndSecrets.move(deployCmd).map(getStdout).get(); - }, - ); - }) - .get(); + const eitherSshKeyFile = await eitherSshKey.mapRight(({ notes }) => notes).flatMapAsync(saveToTempFile); + const eitherAnsibleSecrets = await eitherJobVault.flatMapAsync(({ key, vault }) => + vault.fetchSecret(tEitherJobVault, key, 'ansible_playbooks'), + ); + const eitherAnsibleSecretsFile = await eitherAnsibleSecrets + .mapRight(({ notes }) => notes) + .flatMapAsync(saveToTempFile); + + return eitherJobVault.flatMapAsync(async ({ job, vault, key }) => { + const eitherLocked = await vault.lock(tEitherJobVault, key); + return eitherLocked.flatMap((_locked) => + eitherSshKeyFile.flatMap((sshKeyFile) => + eitherAnsibleSecretsFile.mapRight((secretsFile) => ({ + job, + sshKeyFile, + secretsFile, + })), + ), + ); + }); + }) + .map(async (tEitherJobAndSecrets) => { + const eitherJobAndSecrets = await tEitherJobAndSecrets.get(); + return eitherJobAndSecrets.flatMapAsync(({ job, sshKeyFile, secretsFile }) => { + const volumes = [ + `${job.arguments.path}:/ansible`, + `${sshKeyFile}:/root/id_rsa`, + `${secretsFile}:/ansible/secrets.yml`, + ]; + const playbookCmd = `ansible-playbook -e @secrets.yml ${job.arguments.playbooks}`; + const deployCmd = [ + 'docker', + 'run', + ...prependWith(volumes, '-v'), + 'willhallonline/ansible:latest', + ...playbookCmd.split(' '), + ]; + tEitherJobAndSecrets.trace.trace(`running ansible magic~ (◕ᴗ◕✿) ${deployCmd}`); + return tEitherJobAndSecrets.move(deployCmd).map(getStdout).get(); + }); + }) + .get(); const saveToTempFile = (text: string): Promise> => - Either.fromFailableAsync( - () => Deno.makeTempDir({ dir: Deno.cwd() }) - .then((dir) => Deno.makeTempFile({ dir })) - .then(async (f) => { - await Deno.writeTextFile(f, text); - return f; - }), - ); + Either.fromFailableAsync(() => + mkdtemp(join(tmpdir(), 'ci-')).then(async (dir) => { + const filePath = join(dir, 'temp-file'); + await writeFile(filePath, text); + return filePath; + }), + ); diff --git a/worker/scripts/build_docker_image.ts b/worker/scripts/build_docker_image.ts index 49abe41..228dfcc 100755 --- a/worker/scripts/build_docker_image.ts +++ b/worker/scripts/build_docker_image.ts @@ -1,162 +1,131 @@ -#!/usr/bin/env -S deno run --allow-env --allow-net --allow-run +#!/usr/bin/env node import { - getRequiredEnvVars, - getStdout, - LogLevel, - LogTraceable, - LogMetricTraceable, - Metric, - TraceUtil, -} from "@emprespresso/pengueno"; -import type { - BuildDockerImageJob, - BuildDockerImageJobProps, -} from "@emprespresso/ci_model"; -import { Bitwarden, type LoginItem } from "@emprespresso/ci_worker"; + getRequiredEnvVars, + getStdout, + LogLevel, + LogTraceable, + LogMetricTraceable, + Metric, + TraceUtil, +} from '@emprespresso/pengueno'; +import type { BuildDockerImageJob, BuildDockerImageJobProps } from '@emprespresso/ci_model'; +import { Bitwarden, type LoginItem } from '@emprespresso/ci_worker'; const eitherJob = getRequiredEnvVars([ - "registry", - "namespace", - "repository", - "imageTag", - "context", - "dockerfile", - "buildTarget", -]) - .mapRight((baseArgs) => ( - { - type: "build_docker_image.ts", - arguments: baseArgs, - } - )); -const eitherVault = Bitwarden.getConfigFromEnvironment() - .mapRight((config) => new Bitwarden(config)); + 'registry', + 'namespace', + 'repository', + 'imageTag', + 'context', + 'dockerfile', + 'buildTarget', +]).mapRight( + (baseArgs) => + { + type: 'build_docker_image.ts', + arguments: baseArgs, + }, +); +const eitherVault = Bitwarden.getConfigFromEnvironment().mapRight((config) => new Bitwarden(config)); -const buildImageMetric = Metric.fromName("dockerImage.build"); -const loginMetric = Metric.fromName("dockerRegistry.login"); -const _logJob = LogTraceable.of(eitherJob).bimap( - (tEitherJob) => { - const trace = "build_docker_image." + - tEitherJob.get().fold(({ isRight, value }) => isRight ? value.arguments.buildTarget : ""); - return [tEitherJob.get(), trace]; - }, - ); +const buildImageMetric = Metric.fromName('dockerImage.build'); +const loginMetric = Metric.fromName('dockerRegistry.login'); +const _logJob = LogTraceable.of(eitherJob).bimap((tEitherJob) => { + const trace = + 'build_docker_image.' + + tEitherJob.get().fold(({ isRight, value }) => (isRight ? value.arguments.buildTarget : '')); + return [tEitherJob.get(), trace]; +}); await LogMetricTraceable.ofLogTraceable(_logJob) - .bimap(TraceUtil.withMetricTrace(buildImageMetric)) - .bimap(TraceUtil.withMetricTrace(loginMetric)) - .peek((tEitherJob) => - tEitherJob.trace.trace("starting docker image build job! (⑅˘꒳˘)") - ) - .map((tEitherJob) => - tEitherJob.get() - .flatMapAsync((job) => - eitherVault.flatMapAsync(async (vault) => { - const eitherKey = await vault.unlock(tEitherJob); - return eitherKey.mapRight((key) => ({ job, key, vault })); - }) - ) - ) - .map(async (tEitherJobVault) => { - tEitherJobVault.trace.trace("logging into the wegistwy uwu~"); - const eitherJobVault = await tEitherJobVault.get(); - const eitherDockerRegistryLoginItem = await eitherJobVault.flatMapAsync(( - { job, key, vault }, - ) => - vault.fetchSecret(tEitherJobVault, key, job.arguments.registry) - .finally(() => vault.lock(tEitherJobVault, key)) - ); - return eitherDockerRegistryLoginItem.flatMapAsync(({ login }) => - eitherJobVault.flatMapAsync(async ({ job }) => { - const loginCommand = getDockerLoginCommand( - login.username, - job.arguments.registry, + .bimap(TraceUtil.withMetricTrace(buildImageMetric)) + .bimap(TraceUtil.withMetricTrace(loginMetric)) + .peek((tEitherJob) => tEitherJob.trace.trace('starting docker image build job! (⑅˘꒳˘)')) + .map((tEitherJob) => + tEitherJob.get().flatMapAsync((job) => + eitherVault.flatMapAsync(async (vault) => { + const eitherKey = await vault.unlock(tEitherJob); + return eitherKey.mapRight((key) => ({ job, key, vault })); + }), + ), + ) + .map(async (tEitherJobVault) => { + tEitherJobVault.trace.trace('logging into the wegistwy uwu~'); + const eitherJobVault = await tEitherJobVault.get(); + const eitherDockerRegistryLoginItem = await eitherJobVault.flatMapAsync(({ job, key, vault }) => + vault + .fetchSecret(tEitherJobVault, key, job.arguments.registry) + .finally(() => vault.lock(tEitherJobVault, key)), ); - const eitherLoggedIn = await tEitherJobVault.move(loginCommand).map(( - tLoginCmd, - ) => - getStdout(tLoginCmd, { env: { REGISTRY_PASSWORD: login.password } }) - ).get(); - return eitherLoggedIn.moveRight(job); - }) - ); - }) - .peek(async (tEitherWithAuthdRegistry) => { - const eitherWithAuthdRegistry = await tEitherWithAuthdRegistry.get(); - return tEitherWithAuthdRegistry.trace.trace( - eitherWithAuthdRegistry.fold(({ isLeft}) => - loginMetric[isLeft ? "failure" : "success"] - ), - ); - }) - .map(async (tEitherWithAuthdRegistryBuildJob) => { - const eitherWithAuthdRegistryBuildJob = - await tEitherWithAuthdRegistryBuildJob.get(); - tEitherWithAuthdRegistryBuildJob.trace.trace( - "finally building the image~ (◕ᴗ◕✿)", - ); - const eitherBuiltImage = await eitherWithAuthdRegistryBuildJob.flatMapAsync( - (job) => - tEitherWithAuthdRegistryBuildJob - .move(getBuildCommand(job.arguments)) - .map((tBuildCmd) => - getStdout(tBuildCmd, { - env: {}, - clearEnv: true, - }) - ) - .get(), - ); - return eitherBuiltImage.flatMap((buildOutput) => - eitherWithAuthdRegistryBuildJob.mapRight((job) => ({ buildOutput, job })) - ); - }) - .peek(async (tEitherWithBuiltImage) => { - const eitherWithBuiltImage = await tEitherWithBuiltImage.get(); - eitherWithBuiltImage.fold(({ isLeft, value}) => { - tEitherWithBuiltImage.trace.trace( - buildImageMetric[isLeft ? "failure" : "success"], - ); - if (isLeft) { - tEitherWithBuiltImage.trace.addTrace(LogLevel.ERROR).trace( - `oh nyoo we couldn't buiwd the img :(( ${value}`, + return eitherDockerRegistryLoginItem.flatMapAsync(({ login }) => + eitherJobVault.flatMapAsync(async ({ job }) => { + const loginCommand = getDockerLoginCommand(login.username, job.arguments.registry); + const eitherLoggedIn = await tEitherJobVault + .move(loginCommand) + .map((tLoginCmd) => getStdout(tLoginCmd, { env: { REGISTRY_PASSWORD: login.password } })) + .get(); + return eitherLoggedIn.moveRight(job); + }), ); - return; - } - tEitherWithBuiltImage.trace.addTrace("buildOutput").trace(value.buildOutput); - }); - }) - .map(async (tEitherWithBuiltImage) => { - const eitherWithBuiltImage = await tEitherWithBuiltImage.get(); - return eitherWithBuiltImage - .mapRight(({ job }) => - tEitherWithBuiltImage.move(getPushCommand(job.arguments.imageTag)) - ) - .flatMapAsync((tPushCommand) => getStdout(tPushCommand)); - }) - .get(); + }) + .peek(async (tEitherWithAuthdRegistry) => { + const eitherWithAuthdRegistry = await tEitherWithAuthdRegistry.get(); + return tEitherWithAuthdRegistry.trace.trace( + eitherWithAuthdRegistry.fold(({ isLeft }) => loginMetric[isLeft ? 'failure' : 'success']), + ); + }) + .map(async (tEitherWithAuthdRegistryBuildJob) => { + const eitherWithAuthdRegistryBuildJob = await tEitherWithAuthdRegistryBuildJob.get(); + tEitherWithAuthdRegistryBuildJob.trace.trace('finally building the image~ (◕ᴗ◕✿)'); + const eitherBuiltImage = await eitherWithAuthdRegistryBuildJob.flatMapAsync((job) => + tEitherWithAuthdRegistryBuildJob + .move(getBuildCommand(job.arguments)) + .map((tBuildCmd) => + getStdout(tBuildCmd, { + env: {}, + clearEnv: true, + }), + ) + .get(), + ); + return eitherBuiltImage.flatMap((buildOutput) => + eitherWithAuthdRegistryBuildJob.mapRight((job) => ({ buildOutput, job })), + ); + }) + .peek(async (tEitherWithBuiltImage) => { + const eitherWithBuiltImage = await tEitherWithBuiltImage.get(); + eitherWithBuiltImage.fold(({ isLeft, value }) => { + tEitherWithBuiltImage.trace.trace(buildImageMetric[isLeft ? 'failure' : 'success']); + if (isLeft) { + tEitherWithBuiltImage.trace + .addTrace(LogLevel.ERROR) + .trace(`oh nyoo we couldn't buiwd the img :(( ${value}`); + return; + } + tEitherWithBuiltImage.trace.addTrace('buildOutput').trace(value.buildOutput); + }); + }) + .map(async (tEitherWithBuiltImage) => { + const eitherWithBuiltImage = await tEitherWithBuiltImage.get(); + return eitherWithBuiltImage + .mapRight(({ job }) => tEitherWithBuiltImage.move(getPushCommand(job.arguments.imageTag))) + .flatMapAsync((tPushCommand) => getStdout(tPushCommand)); + }) + .get(); const getDockerLoginCommand = (username: string, registry: string) => - `docker login --username ${username} --password $REGISTRY_PASSWORD ${registry}` - .split(" "); + `docker login --username ${username} --password $REGISTRY_PASSWORD ${registry}`.split(' '); -const getBuildCommand = ( - { +const getBuildCommand = ({ buildTarget, imageTag, dockerfile, context }: BuildDockerImageJobProps) => [ + 'docker', + 'build', + '--target', buildTarget, + '-t', imageTag, + '-f', dockerfile, context, - }: BuildDockerImageJobProps, -) => [ - "docker", - "build", - "--target", - buildTarget, - "-t", - imageTag, - "-f", - dockerfile, - context, ]; -const getPushCommand = (tag: string) => ["docker", "push", tag]; +const getPushCommand = (tag: string) => ['docker', 'push', tag]; diff --git a/worker/scripts/checkout_ci.ts b/worker/scripts/checkout_ci.ts index efe74fb..8e4dcca 100755 --- a/worker/scripts/checkout_ci.ts +++ b/worker/scripts/checkout_ci.ts @@ -1,182 +1,152 @@ -#!/usr/bin/env -S deno run --allow-all +#!/usr/bin/env node import { - type Command, - Either, - LogTraceable, - getRequiredEnvVars, - getStdout, - isObject, - LogMetricTraceable, - Metric, - prependWith, - TraceUtil, -} from "@emprespresso/pengueno"; -import { - type CheckoutCiJob, - type FetchCodeJob, - PipelineImpl, -} from "@emprespresso/ci_model"; -import { executeJob, executePipeline } from "@emprespresso/ci_worker"; + type Command, + Either, + LogTraceable, + getRequiredEnvVars, + getStdout, + isObject, + LogMetricTraceable, + Metric, + prependWith, + TraceUtil, +} from '@emprespresso/pengueno'; +import { mkdir, readFile, rm } from 'fs/promises'; +import { join } from 'path'; +import { type CheckoutCiJob, type FetchCodeJob, PipelineImpl } from '@emprespresso/ci_model'; +import { executeJob, executePipeline } from '@emprespresso/ci_worker'; const run = Date.now().toString(); -const eitherJob = getRequiredEnvVars(["remote", "refname", "rev"]).mapRight( - (baseArgs) => - { - type: "checkout_ci.ts", - arguments: { - ...baseArgs, - run, - returnPath: Deno.cwd(), - }, - }, +const eitherJob = getRequiredEnvVars(['remote', 'refname', 'rev']).mapRight( + (baseArgs) => + { + type: 'checkout_ci.ts', + arguments: { + ...baseArgs, + run, + returnPath: process.cwd(), + }, + }, ); -const ciRunMetric = Metric.fromName("checkout_ci.run"); -const _logJob = LogTraceable.of(eitherJob).bimap( - TraceUtil.withTrace(`checkout_ci.${run}`), -); +const ciRunMetric = Metric.fromName('checkout_ci.run'); +const _logJob = LogTraceable.of(eitherJob).bimap(TraceUtil.withTrace(`checkout_ci.${run}`)); await LogMetricTraceable.ofLogTraceable(_logJob) - .bimap(TraceUtil.withMetricTrace(ciRunMetric)) - .map((tEitherJob) => - tEitherJob.get().flatMapAsync((ciJob) => { - const wd = getWorkingDirectoryForCiJob(ciJob); - const fetchPackageJob = { - type: "fetch_code.ts", - arguments: { - remoteUrl: ciJob.arguments.remote, - checkout: ciJob.arguments.rev, - path: getSrcDirectoryForCiJob(ciJob), - }, - }; - return Either.fromFailableAsync(() => - Deno.mkdir(wd) - .then(() => Deno.chdir(wd)) - .then(() => tEitherJob.move(fetchPackageJob).map(executeJob).get()) - .then(() => ciJob), - ); - }), - ) - .map((tEitherCiJob) => - tEitherCiJob.get().then((eitherCiJob) => - eitherCiJob.flatMapAsync<{ cmd: Command; job: CheckoutCiJob }>((ciJob) => - Either.fromFailableAsync(() => - Deno.readTextFile( - `${getSrcDirectoryForCiJob(ciJob)}/${CI_WORKFLOW_FILE}`, - ), - ).then((eitherWorkflowJson) => - eitherWorkflowJson - .flatMap((json) => - Either.fromFailable(JSON.parse(json)), - ) - .flatMap((eitherWorkflowParse) => { - if (isCiWorkflow(eitherWorkflowParse)) { - return Either.right({ - cmd: getPipelineGenerationCommand( - ciJob, - eitherWorkflowParse.workflow, - ), - job: ciJob, - }); - } - return Either.left( - new Error( - "couldn't find any valid ci configuration (。•́︿•̀。), that's okay~", + .bimap(TraceUtil.withMetricTrace(ciRunMetric)) + .map((tEitherJob) => + tEitherJob.get().flatMapAsync((ciJob) => { + const wd = getWorkingDirectoryForCiJob(ciJob); + const fetchPackageJob = { + type: 'fetch_code.ts', + arguments: { + remoteUrl: ciJob.arguments.remote, + checkout: ciJob.arguments.rev, + path: getSrcDirectoryForCiJob(ciJob), + }, + }; + return Either.fromFailableAsync(() => + mkdir(wd, { recursive: true }) + .then(() => process.chdir(wd)) + .then(() => tEitherJob.move(fetchPackageJob).map(executeJob).get()) + .then(() => ciJob), + ); + }), + ) + .map((tEitherCiJob) => + tEitherCiJob.get().then((eitherCiJob) => + eitherCiJob.flatMapAsync<{ cmd: Command; job: CheckoutCiJob }>((ciJob) => + Either.fromFailableAsync(() => + readFile(join(getSrcDirectoryForCiJob(ciJob), CI_WORKFLOW_FILE), 'utf-8'), + ).then((eitherWorkflowJson) => + eitherWorkflowJson + .flatMap((json) => Either.fromFailable(JSON.parse(json))) + .flatMap((eitherWorkflowParse) => { + if (isCiWorkflow(eitherWorkflowParse)) { + return Either.right({ + cmd: getPipelineGenerationCommand(ciJob, eitherWorkflowParse.workflow), + job: ciJob, + }); + } + return Either.left( + new Error("couldn't find any valid ci configuration (。•́︿•̀。), that's okay~"), + ); + }), ), - ); - }), + ), ), - ), - ), - ) - .map(async (tEitherPipelineGenerationCommand) => { - const eitherJobCommand = await tEitherPipelineGenerationCommand.get(); - const eitherPipeline = await eitherJobCommand.flatMapAsync((jobCommand) => - tEitherPipelineGenerationCommand - .move(jobCommand.cmd) - .map(getStdout) - .get(), - ); - return eitherPipeline - .flatMap(PipelineImpl.from) - .flatMap((pipeline) => - eitherJobCommand.mapRight(({ job }) => ({ job, pipeline })), - ); - }) - .peek( - TraceUtil.promiseify((tEitherPipeline) => - tEitherPipeline - .get() - .mapRight((val) => val.pipeline.serialize()) - .mapRight( - (pipeline) => - `built the pipeline~ (◕ᴗ◕✿) let's make something amazing! ${pipeline}`, - ) - .mapRight((msg) => tEitherPipeline.trace.trace(msg)), - ), - ) - .map(async (tEitherPipeline) => { - const eitherPipeline = await tEitherPipeline.get(); - return eitherPipeline.flatMapAsync(({ pipeline, job }) => - tEitherPipeline - .move(pipeline) - .map((p) => - executePipeline(p, { - HOME: getWorkingDirectoryForCiJob(job), - }), - ) - .get(), + ) + .map(async (tEitherPipelineGenerationCommand) => { + const eitherJobCommand = await tEitherPipelineGenerationCommand.get(); + const eitherPipeline = await eitherJobCommand.flatMapAsync((jobCommand) => + tEitherPipelineGenerationCommand.move(jobCommand.cmd).map(getStdout).get(), + ); + return eitherPipeline + .flatMap(PipelineImpl.from) + .flatMap((pipeline) => eitherJobCommand.mapRight(({ job }) => ({ job, pipeline }))); + }) + .peek( + TraceUtil.promiseify((tEitherPipeline) => + tEitherPipeline + .get() + .mapRight((val) => val.pipeline.serialize()) + .mapRight((pipeline) => `built the pipeline~ (◕ᴗ◕✿) let's make something amazing! ${pipeline}`) + .mapRight((msg) => tEitherPipeline.trace.trace(msg)), + ), + ) + .map(async (tEitherPipeline) => { + const eitherPipeline = await tEitherPipeline.get(); + return eitherPipeline.flatMapAsync(({ pipeline, job }) => + tEitherPipeline + .move(pipeline) + .map((p) => + executePipeline(p, { + HOME: getWorkingDirectoryForCiJob(job), + }), + ) + .get(), + ); + }) + .get() + .then((e) => + e + .flatMap(() => eitherJob) + .fold(({ isLeft, isRight, value }) => { + if (isLeft || !isRight) throw value; + return rm(getWorkingDirectoryForCiJob(value), { + recursive: true, + }); + }), ); - }) - .get() - .then((e) => - e - .flatMap(() => eitherJob) - .fold(({ isLeft, isRight, value }) => { - if (isLeft || !isRight) throw value; - return Deno.remove(getWorkingDirectoryForCiJob(value), { - recursive: true, - }); - }), - ); -const getWorkingDirectoryForCiJob = (job: CheckoutCiJob) => - `${job.arguments.returnPath}/${job.arguments.run}`; +const getWorkingDirectoryForCiJob = (job: CheckoutCiJob) => `${job.arguments.returnPath}/${job.arguments.run}`; -const getSrcDirectoryForCiJob = (job: CheckoutCiJob) => - `${job.arguments.returnPath}/${job.arguments.run}/src`; +const getSrcDirectoryForCiJob = (job: CheckoutCiJob) => `${job.arguments.returnPath}/${job.arguments.run}/src`; -const _runFlags = ( - "--rm --network none --cap-drop ALL" + "--security-opt no-new-privileges" -).split(" "); -const _image = "oci.liz.coffee/img/ci-worker:release"; +const _runFlags = ('--rm --network none --cap-drop ALL' + '--security-opt no-new-privileges').split(' '); +const _image = 'oci.liz.coffee/img/ci-worker:release'; const getPipelineGenerationCommand = ( - job: CheckoutCiJob, - pipelineGeneratorPath: string, - image = _image, - runFlags = _runFlags, + job: CheckoutCiJob, + pipelineGeneratorPath: string, + image = _image, + runFlags = _runFlags, ) => [ - "docker", - "run", - ...runFlags, - ...prependWith( - Object.entries(job.arguments).map(([key, val]) => `"${key}"="${val}"`), - "-e", - ), - "-v", - `${getSrcDirectoryForCiJob( - job, - )}/${pipelineGeneratorPath}:/pipeline_generator`, - image, - "/pipeline_generator", + 'docker', + 'run', + ...runFlags, + ...prependWith( + Object.entries(job.arguments).map(([key, val]) => `"${key}"="${val}"`), + '-e', + ), + '-v', + `${getSrcDirectoryForCiJob(job)}/${pipelineGeneratorPath}:/pipeline_generator`, + image, + '/pipeline_generator', ]; export interface CiWorkflow { - workflow: string; + workflow: string; } export const isCiWorkflow = (t: unknown): t is CiWorkflow => - isObject(t) && - "workflow" in t && - typeof t.workflow === "string" && - !t.workflow.includes(".."); -const CI_WORKFLOW_FILE = ".ci/ci.json"; + isObject(t) && 'workflow' in t && typeof t.workflow === 'string' && !t.workflow.includes('..'); +const CI_WORKFLOW_FILE = '.ci/ci.json'; diff --git a/worker/secret.ts b/worker/secret.ts index 951c539..e3edb2d 100644 --- a/worker/secret.ts +++ b/worker/secret.ts @@ -1,36 +1,32 @@ import { - Either, - getRequiredEnvVars, - getStdout, - type IEither, - type ITraceable, - type LogMetricTraceSupplier, - Metric, - TraceUtil, -} from "@emprespresso/pengueno"; + Either, + getRequiredEnvVars, + getStdout, + type IEither, + type ITraceable, + type LogMetricTraceSupplier, + Metric, + TraceUtil, +} from '@emprespresso/pengueno'; // -- -- export interface LoginItem { - login: { - username: string; - password: string; - }; + login: { + username: string; + password: string; + }; } export interface SecureNote { - notes: string; + notes: string; } export type SecretItem = LoginItem | SecureNote; export interface IVault { - unlock: (client: TClient) => Promise>; - lock: (client: TClient, key: TKey) => Promise>; + unlock: (client: TClient) => Promise>; + lock: (client: TClient, key: TKey) => Promise>; - fetchSecret: ( - client: TClient, - key: TKey, - item: TItemId, - ) => Promise>; + fetchSecret: (client: TClient, key: TKey, item: TItemId) => Promise>; } // -- -- @@ -39,156 +35,122 @@ type TClient = ITraceable; type TKey = string; type TItemId = string; export class Bitwarden implements IVault { - constructor(private readonly config: BitwardenConfig) {} + constructor(private readonly config: BitwardenConfig) {} - public unlock(client: TClient) { - return client - .move(this.config) - .bimap(TraceUtil.withMetricTrace(Bitwarden.loginMetric)) - .flatMap((tConfig) => - tConfig.move(`bw config server ${tConfig.get().server}`).map(getStdout), - ) - .map(async (tEitherWithConfig) => { - const eitherWithConfig = await tEitherWithConfig.get(); - tEitherWithConfig.trace.trace("logging in~ ^.^"); - return eitherWithConfig.flatMapAsync((_) => - tEitherWithConfig - .move("bw login --apikey --quiet") - .map(getStdout) - .get(), - ); - }) - .peek(async (tEitherWithAuthd) => { - const eitherWithAuthd = await tEitherWithAuthd.get(); - return tEitherWithAuthd.trace.trace( - eitherWithAuthd.fold( - ({ isLeft }) => - Bitwarden.loginMetric[isLeft ? "failure" : "success"], - ), - ); - }) - .map(async (tEitherWithAuthd) => { - const eitherWithAuthd = await tEitherWithAuthd.get(); - tEitherWithAuthd.trace.trace("unlocking the secret vault~ (◕ᴗ◕✿)"); - return eitherWithAuthd.flatMapAsync((_) => - tEitherWithAuthd - .move("bw unlock --passwordenv BW_PASSWORD --raw") - .map(getStdout) - .get(), - ); - }) - .peek(async (tEitherWithSession) => { - const eitherWithAuthd = await tEitherWithSession.get(); - return tEitherWithSession.trace.trace( - eitherWithAuthd.fold( - ({ isLeft }) => - Bitwarden.unlockVaultMetric[isLeft ? "failure" : "success"], - ), - ); - }) - .get(); - } + public unlock(client: TClient) { + return client + .move(this.config) + .bimap(TraceUtil.withMetricTrace(Bitwarden.loginMetric)) + .flatMap((tConfig) => tConfig.move(`bw config server ${tConfig.get().server}`).map(getStdout)) + .map(async (tEitherWithConfig) => { + const eitherWithConfig = await tEitherWithConfig.get(); + tEitherWithConfig.trace.trace('logging in~ ^.^'); + return eitherWithConfig.flatMapAsync((_) => + tEitherWithConfig.move('bw login --apikey --quiet').map(getStdout).get(), + ); + }) + .peek(async (tEitherWithAuthd) => { + const eitherWithAuthd = await tEitherWithAuthd.get(); + return tEitherWithAuthd.trace.trace( + eitherWithAuthd.fold(({ isLeft }) => Bitwarden.loginMetric[isLeft ? 'failure' : 'success']), + ); + }) + .map(async (tEitherWithAuthd) => { + const eitherWithAuthd = await tEitherWithAuthd.get(); + tEitherWithAuthd.trace.trace('unlocking the secret vault~ (◕ᴗ◕✿)'); + return eitherWithAuthd.flatMapAsync((_) => + tEitherWithAuthd.move('bw unlock --passwordenv BW_PASSWORD --raw').map(getStdout).get(), + ); + }) + .peek(async (tEitherWithSession) => { + const eitherWithAuthd = await tEitherWithSession.get(); + return tEitherWithSession.trace.trace( + eitherWithAuthd.fold(({ isLeft }) => Bitwarden.unlockVaultMetric[isLeft ? 'failure' : 'success']), + ); + }) + .get(); + } - public fetchSecret( - client: TClient, - key: string, - item: string, - ): Promise> { - return client - .move(key) - .bimap(TraceUtil.withMetricTrace(Bitwarden.fetchSecretMetric)) - .peek((tSession) => - tSession.trace.trace(`looking for your secret ${item} (⑅˘꒳˘)`), - ) - .flatMap((tSession) => - tSession.move("bw list items").map((listCmd) => - getStdout(listCmd, { - env: { BW_SESSION: tSession.get() }, - }), - ), - ) - .map( - TraceUtil.promiseify((tEitherItemsJson) => - tEitherItemsJson - .get() - .flatMap( - (itemsJson): IEither> => - Either.fromFailable(() => JSON.parse(itemsJson)), + public fetchSecret(client: TClient, key: string, item: string): Promise> { + return client + .move(key) + .bimap(TraceUtil.withMetricTrace(Bitwarden.fetchSecretMetric)) + .peek((tSession) => tSession.trace.trace(`looking for your secret ${item} (⑅˘꒳˘)`)) + .flatMap((tSession) => + tSession.move('bw list items').map((listCmd) => + getStdout(listCmd, { + env: { BW_SESSION: tSession.get() }, + }), + ), + ) + .map( + TraceUtil.promiseify((tEitherItemsJson) => + tEitherItemsJson + .get() + .flatMap( + (itemsJson): IEither> => + Either.fromFailable(() => JSON.parse(itemsJson)), + ) + .flatMap((itemsList): IEither => { + const secret = itemsList.find(({ name }) => name === item); + if (!secret) { + return Either.left(new Error(`couldn't find the item ${item} (。•́︿•̀。)`)); + } + return Either.right(secret); + }), + ), ) - .flatMap((itemsList): IEither => { - const secret = itemsList.find(({ name }) => name === item); - if (!secret) { - return Either.left( - new Error(`couldn't find the item ${item} (。•́︿•̀。)`), + .peek(async (tEitherWithSecret) => { + const eitherWithSecret = await tEitherWithSecret.get(); + return tEitherWithSecret.trace.trace( + eitherWithSecret.fold(({ isLeft }) => Bitwarden.fetchSecretMetric[isLeft ? 'failure' : 'success']), ); - } - return Either.right(secret); - }), - ), - ) - .peek(async (tEitherWithSecret) => { - const eitherWithSecret = await tEitherWithSecret.get(); - return tEitherWithSecret.trace.trace( - eitherWithSecret.fold( - ({ isLeft }) => - Bitwarden.fetchSecretMetric[isLeft ? "failure" : "success"], - ), - ); - }) - .get(); - } + }) + .get(); + } - public lock(client: TClient, key: TKey) { - return client - .move(key) - .bimap(TraceUtil.withMetricTrace(Bitwarden.lockVaultMetric)) - .peek((tSession) => - tSession.trace.trace(`taking care of locking the vault :3`), - ) - .flatMap((tSession) => - tSession.move("bw lock").map((lockCmd) => - getStdout(lockCmd, { - env: { BW_SESSION: tSession.get() }, - }), - ), - ) - .peek(async (tEitherWithLocked) => { - const eitherWithLocked = await tEitherWithLocked.get(); - return eitherWithLocked.fold(({ isLeft }) => { - tEitherWithLocked.trace.trace( - Bitwarden.lockVaultMetric[isLeft ? "failure" : "success"], - ); - if (isLeft) return; - tEitherWithLocked.trace.trace( - "all locked up and secure now~ (。•̀ᴗ-)✧", - ); - }); - }) - .get(); - } + public lock(client: TClient, key: TKey) { + return client + .move(key) + .bimap(TraceUtil.withMetricTrace(Bitwarden.lockVaultMetric)) + .peek((tSession) => tSession.trace.trace(`taking care of locking the vault :3`)) + .flatMap((tSession) => + tSession.move('bw lock').map((lockCmd) => + getStdout(lockCmd, { + env: { BW_SESSION: tSession.get() }, + }), + ), + ) + .peek(async (tEitherWithLocked) => { + const eitherWithLocked = await tEitherWithLocked.get(); + return eitherWithLocked.fold(({ isLeft }) => { + tEitherWithLocked.trace.trace(Bitwarden.lockVaultMetric[isLeft ? 'failure' : 'success']); + if (isLeft) return; + tEitherWithLocked.trace.trace('all locked up and secure now~ (。•̀ᴗ-)✧'); + }); + }) + .get(); + } - public static getConfigFromEnvironment(): IEither { - return getRequiredEnvVars([ - "BW_SERVER", - "BW_CLIENTSECRET", - "BW_CLIENTID", - "BW_PASSWORD", - ]).mapRight(({ BW_SERVER, BW_CLIENTSECRET, BW_CLIENTID }) => ({ - clientId: BW_CLIENTID, - secret: BW_CLIENTSECRET, - server: BW_SERVER, - })); - } + public static getConfigFromEnvironment(): IEither { + return getRequiredEnvVars(['BW_SERVER', 'BW_CLIENTSECRET', 'BW_CLIENTID', 'BW_PASSWORD']).mapRight( + ({ BW_SERVER, BW_CLIENTSECRET, BW_CLIENTID }) => ({ + clientId: BW_CLIENTID, + secret: BW_CLIENTSECRET, + server: BW_SERVER, + }), + ); + } - private static loginMetric = Metric.fromName("Bitwarden.login"); - private static unlockVaultMetric = Metric.fromName("Bitwarden.unlockVault"); - private static fetchSecretMetric = Metric.fromName("Bitwarden.fetchSecret"); - private static lockVaultMetric = Metric.fromName("Bitwarden.lock"); + private static loginMetric = Metric.fromName('Bitwarden.login'); + private static unlockVaultMetric = Metric.fromName('Bitwarden.unlockVault'); + private static fetchSecretMetric = Metric.fromName('Bitwarden.fetchSecret'); + private static lockVaultMetric = Metric.fromName('Bitwarden.lock'); } export interface BitwardenConfig { - server: string; - secret: string; - clientId: string; + server: string; + secret: string; + clientId: string; } // -- -- diff --git a/worker/tsconfig.json b/worker/tsconfig.json new file mode 100644 index 0000000..58e9147 --- /dev/null +++ b/worker/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./", + "composite": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "noEmit": false + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"], + "references": [{ "path": "../u" }, { "path": "../model" }] +} -- cgit v1.2.3-70-g09d2