diff options
author | Elizabeth Hunt <me@liz.coffee> | 2025-06-20 14:53:38 -0700 |
---|---|---|
committer | Elizabeth Hunt <me@liz.coffee> | 2025-06-20 14:53:38 -0700 |
commit | d4791f3d357634daf506fb8f91cc5332a794c421 (patch) | |
tree | 1bb01d2d4d8fa74d83bb6f99f2c8aa4146ca2d11 | |
parent | d7e8d31c94cd713a2f4cf799e20e993acc69e361 (diff) | |
download | ci-d4791f3d357634daf506fb8f91cc5332a794c421.tar.gz ci-d4791f3d357634daf506fb8f91cc5332a794c421.zip |
Move to nodejs
89 files changed, 4309 insertions, 2264 deletions
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" } @@ -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" + } } - } } @@ -25,7 +25,7 @@ RUN cmake -B /opt/laminar/build -S /opt/laminar/src -G Ninja \ # -- </laminar_bin> -- # -- <ci_base> -- -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" ] # -- </ci_base> -- 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<IEither<Error, 0>> => + 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); + }), + ); +} @@ -1,2 +1,2 @@ [tools] -deno = "latest" +node = "22.16.0" @@ -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<IEither<Error, 0>> => - 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<string, string>; -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<string, string>; +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<Job>; -} -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<PipelineStage>; - 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<PipelineStage>) {} - - public serialize() { - return JSON.stringify(this.serialJobs); - } - - public static from(s: string): IEither<Error, Pipeline> { - return Either.fromFailable<Error, unknown>(() => JSON.parse(s)) - .flatMap<Pipeline>((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<PipelineStage> = []; - - 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: [ - <FetchCodeJob>{ - 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<PipelineStage> = []; + + 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: [ + <FetchCodeJob>{ + 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<PipelineStage>) {} + + public serialize() { + return JSON.stringify(this.serialJobs); + } + + public static from(s: string): IEither<Error, Pipeline> { + return Either.fromFailable<Error, unknown>(() => JSON.parse(s)) + .flatMap<Pipeline>((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<Job>; +} +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<PipelineStage>; + 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 @@ # -- <ci_server> -- 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" ] # -- </ci_server> -- 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<ITraceable<Job, ServerTrace>> = 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<ITraceable<Job, ServerTrace>> = 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<PenguenoRequest, ServerTrace>) { - const url = new URL(req.get().url); - if (url.pathname === "/health") { - return this.healthCheckActivity.checkHealth(req); + private route(req: ITraceable<PenguenoRequest, ServerTrace>) { + 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<Response> { - return PenguenoRequest.from(req) - .bimap(TraceUtil.withClassTrace(this)) - .map((req) => this.route(req)) - .get(); - } + public serve(req: Request): Promise<Response> { + 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<HealthCheckInput, ServerTrace>, + input: ITraceable<HealthCheckInput, ServerTrace>, ): Promise<IEither<Error, HealthCheckOutput>> => - 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<IEither<Error, 0>>(() => {}); +export const runServer = (port: number, host: string): Promise<IEither<Error, 0>> => + Either.fromFailable<Error, void>(() => { + 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"; - -// -- <job.hook> -- -const wellFormedJobMetric = Metric.fromName("Job.WellFormed"); - -const jobJsonTransformer = ( - j: ITraceable<unknown, ServerTrace>, -): IEither<PenguenoError, Job> => - j - .bimap(TraceUtil.withMetricTrace(wellFormedJobMetric)) - .map((tJson): IEither<PenguenoError, Job> => { - 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<ITraceable<Job, ServerTrace>>, - ) {} - - private trace(r: ITraceable<PenguenoRequest, ServerTrace>) { - return r - .bimap(TraceUtil.withClassTrace(this)) - .bimap(TraceUtil.withMetricTrace(jobHookRequestMetric)); - } - - public processHook(r: ITraceable<PenguenoRequest, ServerTrace>) { - 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(); - } -} - -// -- </job.hook> -- - -// -- <job.queuer> -- -type QueuePosition = string; -export class QueueError extends Error {} -export interface IJobQueuer<TJob> { - queue: Mapper<TJob, Promise<IEither<QueueError, QueuePosition>>>; -} - -export class LaminarJobQueuer - implements IJobQueuer<ITraceable<Job, ServerTrace>> -{ - 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<Job, ServerTrace>) { - 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<Error, string>(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<Error, string>(jobUrl); - }), - ), - ) - .get(); - } -} -// -- </job.queuer> -- 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<TJob> { + queue: Mapper<TJob, Promise<IEither<QueueError, QueuePosition>>>; +} + +export class LaminarJobQueuer implements IJobQueuer<ITraceable<Job, ServerTrace>> { + 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<Job, ServerTrace>) { + 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<Error, string>(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<Error, string>(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<unknown, ServerTrace>): IEither<PenguenoError, Job> => + j + .bimap(TraceUtil.withMetricTrace(wellFormedJobMetric)) + .map((tJson): IEither<PenguenoError, Job> => { + 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<ITraceable<Job, ServerTrace>>) {} + + private trace(r: ITraceable<PenguenoRequest, ServerTrace>) { + return r.bimap(TraceUtil.withClassTrace(this)).bimap(TraceUtil.withMetricTrace(jobHookRequestMetric)); + } + + public processHook(r: ITraceable<PenguenoRequest, ServerTrace>) { + 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<IEither<Error, 0>> => { - const serverConfig = { - host, - port, - }; - return Either.fromFailable<Error, Deno.HttpServer>(() => - 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<T = any, ArgT = any> { - (...args: Array<ArgT>): T; + (...args: Array<ArgT>): T; } export interface Supplier<T> extends Callable<T, undefined> { - (): T; + (): T; } export interface Mapper<T, U> extends Callable<U, T> { - (t: T): U; + (t: T): U; } export interface BiMapper<T, U, R> extends Callable { - (t: T, u: U): R; + (t: T, u: U): R; } export interface SideEffect<T> extends Mapper<T, void> {} 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<LeftT, RightT, T> { - readonly isLeft: LeftT; - readonly isRight: RightT; - readonly value: T; + readonly isLeft: LeftT; + readonly isRight: RightT; + readonly value: T; } export type Left<E> = _Either<true, false, E>; export type Right<T> = _Either<false, true, T>; export interface IEither<E, T> { - readonly _tag: IEitherTag; - - mapBoth: <_E, _T>( - errBranch: Mapper<E, _E>, - okBranch: Mapper<T, _T>, - ) => IEither<_E, _T>; - fold: <_T>(folder: Mapper<Left<E> | Right<T>, _T>) => _T; - moveRight: <_T>(t: _T) => IEither<E, _T>; - mapRight: <_T>(mapper: Mapper<T, _T>) => IEither<E, _T>; - mapLeft: <_E>(mapper: Mapper<E, _E>) => IEither<_E, T>; - flatMap: <_T>(mapper: Mapper<T, IEither<E, _T>>) => IEither<E, _T>; - flatMapAsync: <_T>( - mapper: Mapper<T, Promise<IEither<E, _T>>>, - ) => Promise<IEither<E, _T>>; + readonly _tag: IEitherTag; + + mapBoth: <_E, _T>(errBranch: Mapper<E, _E>, okBranch: Mapper<T, _T>) => IEither<_E, _T>; + fold: <_T>(folder: Mapper<Left<E> | Right<T>, _T>) => _T; + moveRight: <_T>(t: _T) => IEither<E, _T>; + mapRight: <_T>(mapper: Mapper<T, _T>) => IEither<E, _T>; + mapLeft: <_E>(mapper: Mapper<E, _E>) => IEither<_E, T>; + flatMap: <_T>(mapper: Mapper<T, IEither<E, _T>>) => IEither<E, _T>; + flatMapAsync: <_T>(mapper: Mapper<T, Promise<IEither<E, _T>>>) => Promise<IEither<E, _T>>; } export class Either<E, T> implements IEither<E, T> { - private readonly self: Left<E> | Right<T>; - - private constructor( - init: { err?: E; ok?: T }, - public readonly _tag: IEitherTag = iEitherTag, - ) { - this.self = <Left<E> | Right<T>>{ - 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<Left<E> | Right<T>, _T>): _T { - return folder(this.self); - } - - public mapBoth<_E, _T>( - errBranch: Mapper<E, _E>, - okBranch: Mapper<T, _T>, - ): 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<T, IEither<E, _T>>): IEither<E, _T> { - if (this.self.isRight) return mapper(this.self.value); - return Either.left<E, _T>(this.self.value); - } - - public mapRight<_T>(mapper: Mapper<T, _T>): IEither<E, _T> { - if (this.self.isRight) return Either.right<E, _T>(mapper(this.self.value)); - return Either.left<E, _T>(this.self.value); - } - - public mapLeft<_E>(mapper: Mapper<E, _E>): 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<T, Promise<IEither<E, _T>>>, - ): Promise<IEither<E, _T>> { - if (this.self.isLeft) { - return Promise.resolve(Either.left<E, _T>(this.self.value)); + private readonly self: Left<E> | Right<T>; + + private constructor( + init: { err?: E; ok?: T }, + public readonly _tag: IEitherTag = iEitherTag, + ) { + this.self = <Left<E> | Right<T>>{ + isLeft: 'err' in init, + isRight: 'ok' in init, + value: init.err ?? init.ok!, + }; } - return await mapper(this.self.value).catch((err) => - Either.left<E, _T>(err), - ); - } - - static left<E, T>(e: E): IEither<E, T> { - return new Either<E, T>({ err: e }); - } - - static right<E, T>(t: T): IEither<E, T> { - return new Either<E, T>({ ok: t }); - } - - static fromFailable<E, T>(s: Supplier<T>): IEither<E, T> { - try { - return Either.right<E, T>(s()); - } catch (e) { - return Either.left<E, T>(e as E); + + public moveRight<_T>(t: _T) { + return this.mapRight(() => t); + } + + public fold<_T>(folder: Mapper<Left<E> | Right<T>, _T>): _T { + return folder(this.self); + } + + public mapBoth<_E, _T>(errBranch: Mapper<E, _E>, okBranch: Mapper<T, _T>): 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<T, IEither<E, _T>>): IEither<E, _T> { + if (this.self.isRight) return mapper(this.self.value); + return Either.left<E, _T>(this.self.value); + } + + public mapRight<_T>(mapper: Mapper<T, _T>): IEither<E, _T> { + if (this.self.isRight) return Either.right<E, _T>(mapper(this.self.value)); + return Either.left<E, _T>(this.self.value); + } + + public mapLeft<_E>(mapper: Mapper<E, _E>): 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<T, Promise<IEither<E, _T>>>): Promise<IEither<E, _T>> { + if (this.self.isLeft) { + return Promise.resolve(Either.left<E, _T>(this.self.value)); + } + return await mapper(this.self.value).catch((err) => Either.left<E, _T>(err)); + } + + static left<E, T>(e: E): IEither<E, T> { + return new Either<E, T>({ err: e }); + } + + static right<E, T>(t: T): IEither<E, T> { + return new Either<E, T>({ ok: t }); + } + + static fromFailable<E, T>(s: Supplier<T>): IEither<E, T> { + try { + return Either.right<E, T>(s()); + } catch (e) { + return Either.left<E, T>(e as E); + } + } + + static async fromFailableAsync<E, T>(s: Supplier<Promise<T>> | Promise<T>): Promise<IEither<E, T>> { + return await (typeof s === 'function' ? s() : s) + .then((t: T) => Either.right<E, T>(t)) + .catch((e: E) => Either.left<E, T>(e)); } - } - - static async fromFailableAsync<E, T>( - s: Supplier<Promise<T>>, - ): Promise<IEither<E, T>> { - return await s() - .then((t: T) => Either.right<E, T>(t)) - .catch((e: E) => Either.left<E, T>(e)); - } } export const isEither = <E, T>(o: unknown): o is IEither<E, T> => { - 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 = <R, F extends Callable<R>>(fn: F): F => { - const cache = new Map<string, R>(); - 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<string, R>(); + 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 extends string>(k: string): k is K => - k.startsWith("--"); +export const isArgKey = <K extends string>(k: string): k is K => k.startsWith('--'); interface ArgHandler<V> { absent?: V; @@ -10,42 +9,42 @@ interface ArgHandler<V> { } export const getArg = <K extends string, V>( - arg: K, - argv: Array<string>, - whenValue: ArgHandler<V>, + arg: K, + argv: Array<string>, + whenValue: ArgHandler<V>, ): IEither<Error, V> => { - 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<string>, - Handlers extends Partial<Record<Args[number], ArgHandler<unknown>>> + Args extends ReadonlyArray<string>, + Handlers extends Partial<Record<Args[number], ArgHandler<unknown>>>, > = { - [K in Args[number]]: K extends keyof Handlers - ? Handlers[K] extends ArgHandler<infer T> - ? T - : string - : string; + [K in Args[number]]: K extends keyof Handlers ? (Handlers[K] extends ArgHandler<infer T> ? T : string) : string; }; export const argv = < - const Args extends ReadonlyArray<string>, - const Handlers extends Partial<Record<Args[number], ArgHandler<unknown>>> + const Args extends ReadonlyArray<string>, + const Handlers extends Partial<Record<Args[number], ArgHandler<unknown>>>, >( args: Args, handlers?: Handlers, - argv = Deno.args, + argv = process.argv.slice(2), ): IEither<Error, MappedArgs<Args, Handlers>> => { type Result = MappedArgs<Args, Handlers>; @@ -53,20 +52,20 @@ export const argv = < const processArg = (arg: Args[number]): IEither<Error, [Args[number], unknown]> => { 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<Error, Partial<Result>>, current: IEither<Error, readonly [Args[number], unknown]>) => { - return acc.flatMap(accValue => - current.mapRight(([key, value]) => ({ - ...accValue, - [key]: value - })) - ); - }, - Either.right({} as Partial<Result>) - ).mapRight(result => result as Result); + return args + .map(processArg) + .reduce( + (acc: IEither<Error, Partial<Result>>, current: IEither<Error, readonly [Args[number], unknown]>) => + acc.flatMap((accValue) => + current.mapRight(([key, value]) => ({ + ...accValue, + [key]: value, + })), + ), + Either.right(<Partial<Result>>{}), + ) + .mapRight((result) => <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 = <V extends string>(name: V): IEither<Error, V> => - Either.fromFailable<Error, V | undefined>( - () => 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<Error, V | undefined>(() => 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<T extends ReadonlyArray<string>, V = string> = { - [K in T extends ReadonlyArray<infer U> ? U : never]: V; + [K in T extends ReadonlyArray<infer U> ? U : never]: V; }; export const getRequiredEnvVars = <V extends string>(vars: ReadonlyArray<V>) => - vars - .map((envVar) => [envVar, getRequiredEnv(envVar)] as [V, IEither<Error, V>]) - .reduce( - ( - acc: IEither<Error, ObjectFromList<typeof vars>>, - x: [V, IEither<Error, V>], - ) => { - const [envVar, eitherVal] = x; - return acc.flatMap((args) => { - return eitherVal.mapRight( - (envValue) => - ({ - ...args, - [envVar]: envValue, - }) as ObjectFromList<typeof vars>, - ); - }); - }, - Either.right({} as ObjectFromList<typeof vars>), - ); + vars + .map((envVar) => [envVar, getRequiredEnv(envVar)] as [V, IEither<Error, V>]) + .reduce( + (acc: IEither<Error, ObjectFromList<typeof vars>>, x: [V, IEither<Error, V>]) => { + const [envVar, eitherVal] = x; + return acc.flatMap((args) => { + return eitherVal.mapRight( + (envValue) => + ({ + ...args, + [envVar]: envValue, + }) as ObjectFromList<typeof vars>, + ); + }); + }, + Either.right({} as ObjectFromList<typeof vars>), + ); 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<Command, LogTraceSupplier>, - options: Deno.CommandOptions = {}, + c: ITraceable<Command, LogTraceSupplier>, + options: { env?: Record<string, string>; clearEnv?: boolean } = {}, ): Promise<IEither<Error, string>> => - 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<Error, Deno.CommandOutput>(() => - tCmd.get().output(), - ), - ) - .map( - TraceUtil.promiseify((tEitherOut) => - tEitherOut.get().flatMap(({ code, stderr, stdout }) => - Either.fromFailable<Error, CommandOutputDecoded>(() => { - 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<Error, string> => { - 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<IEither<Error, StdStreams>> = Either.fromFailableAsync(exec(_exec, { env })); + return [p, `Command = ${_exec}`]; + }) + .map( + TraceUtil.promiseify( + (tEitherProcess): IEither<Error, string> => + 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<string, string> with stuff that won't // have the potential for shell injection, just to be super safe. type InvalidEntry<K, T> = [K, T]; -export const validateExecutionEntries = < - T, - K extends symbol | number | string = symbol | number | string, ->( - obj: Record<K, T>, +export const validateExecutionEntries = <T, K extends symbol | number | string = symbol | number | string>( + obj: Record<K, T>, ): IEither<Array<InvalidEntry<K, T>>, Record<string, string>> => { - const invalidEntries = <Array<InvalidEntry<K, T>>>( - Object.entries(obj).filter( - (e) => !e.every((x) => typeof x === "string" && validateIdentifier(x)), - ) - ); - if (invalidEntries.length > 0) return Either.left(invalidEntries); - return Either.right(<Record<string, string>>obj); + const invalidEntries = <Array<InvalidEntry<K, T>>>( + Object.entries(obj).filter((e) => !e.every((x) => typeof x === 'string' && validateIdentifier(x))) + ); + if (invalidEntries.length > 0) return Either.left(invalidEntries); + return Either.right(<Record<string, string>>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<PenguenoRequest, ServerTrace>) { - return req - .move(new JsonResponse(req, randomFourOhFour(), { status: 404 })) - .map((resp) => Promise.resolve(resp.get())) - .get(); - } + public fourOhFour(req: ITraceable<PenguenoRequest, ServerTrace>) { + 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<HealthCheckInput, ServerTrace>, - Promise<IEither<Error, HealthCheckOutput>> - > {} + extends Mapper<ITraceable<HealthCheckInput, ServerTrace>, Promise<IEither<Error, HealthCheckOutput>>> {} export class HealthCheckActivityImpl implements IHealthCheckActivity { - constructor(private readonly check: HealthChecker) {} + constructor(private readonly check: HealthChecker) {} - public checkHealth(req: ITraceable<PenguenoRequest, ServerTrace>) { - 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<PenguenoRequest, ServerTrace>) { + 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<PenguenoRequest, ServerTrace>): Promise<PenguenoResponse>; +} + +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<PenguenoRequest, ServerTrace>): Promise<PenguenoResponse>; -} - -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<PenguenoRequest, ServerTrace>, +> { + (req: RIn): Promise<IEither<Err, T>>; +} + +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<R, ParsedJson = unknown> { - (json: ITraceable<ParsedJson, ServerTrace>): IEither<PenguenoError, R>; + (json: ITraceable<ParsedJson, ServerTrace>): IEither<PenguenoError, R>; } -const ParseJsonMetric = Metric.fromName("JsonParse"); +const ParseJsonMetric = Metric.fromName('JsonParse'); export const jsonModel = - <MessageT>( - jsonTransformer: JsonTransformer<MessageT>, - ): RequestFilter<MessageT> => - (r: ITraceable<PenguenoRequest, ServerTrace>) => - r - .bimap(TraceUtil.withFunctionTrace(jsonModel)) - .bimap(TraceUtil.withMetricTrace(ParseJsonMetric)) - .map((j) => - Either.fromFailableAsync<Error, MessageT>(() => 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(); + <MessageT>(jsonTransformer: JsonTransformer<MessageT>): RequestFilter<MessageT> => + (r: ITraceable<PenguenoRequest, ServerTrace>) => + r + .bimap(TraceUtil.withFunctionTrace(jsonModel)) + .bimap(TraceUtil.withMetricTrace(ParseJsonMetric)) + .map((j) => + Either.fromFailableAsync<Error, MessageT>(<Promise<MessageT>>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<HttpMethod>): RequestFilter<HttpMethod> => - (req: ITraceable<PenguenoRequest, ServerTrace>) => - req - .bimap(TraceUtil.withFunctionTrace(requireMethod)) - .move(Promise.resolve(req.get())) - .map( - TraceUtil.promiseify((t) => { - const { method: _method } = t.get(); - const method = <HttpMethod>_method; - if (!methods.includes(method)) { - const msg = "that's not how you pet me (⋟﹏⋞)~"; - t.trace.addTrace(LogLevel.WARN).trace(msg); - return Either.left<PenguenoError, HttpMethod>( - new PenguenoError(msg, 405), - ); - } - return Either.right<PenguenoError, HttpMethod>(method); - }), - ) - .get(); + (methods: Array<HttpMethod>): RequestFilter<HttpMethod> => + (req: ITraceable<PenguenoRequest, ServerTrace>) => + req + .bimap(TraceUtil.withFunctionTrace(requireMethod)) + .move(Promise.resolve(req.get())) + .map( + TraceUtil.promiseify((t) => { + const { method: _method } = t.get(); + const method = <HttpMethod>_method; + if (!methods.includes(method)) { + const msg = "that's not how you pet me (⋟﹏⋞)~"; + t.trace.addTrace(LogLevel.WARN).trace(msg); + return Either.left<PenguenoError, HttpMethod>(new PenguenoError(msg, 405)); + } + return Either.right<PenguenoError, HttpMethod>(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<PenguenoRequest, ServerTrace>, -> { - (req: RIn): Promise<IEither<Err, T>>; -} - -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<string, string> { - const ServerRequestTime = this.at.getTime(); - const ServerResponseTime = Date.now(); - const DeltaTime = ServerResponseTime - ServerRequestTime; - const RequestId = this.id; + public baseResponseHeaders(): Record<string, string> { + 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<PenguenoRequest> { - 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<PenguenoRequest> { + 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<Uint8Array> + | Blob + | FormData + | Iterable<Uint8Array> + | NodeJS.ArrayBufferView + | URLSearchParams + | null + | string; export type ResponseBody = object | string; -export type TResponseInit = ResponseInit & { - status: number; - headers?: Record<string, string>; +export type TResponseInit = Omit<ResponseInit, 'headers'> & { + status: number; + headers?: Record<string, string>; }; -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<string, string>, + }; }; -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<PenguenoRequest, ServerTrace>, - 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<PenguenoRequest, ServerTrace>, 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<PenguenoRequest, ServerTrace>, - e: BodyInit | IEither<ResponseBody, ResponseBody>, - opts: TResponseInit, - ) { - const optsWithJsonContentType = { - ...opts, - headers: { - ...opts?.headers, - "Content-Type": "application/json", - }, - }; - if (isEither<ResponseBody, ResponseBody>(e)) { - super( - req, - JSON.stringify( - e.fold(({ isLeft, value }) => - isLeft ? { error: value } : { ok: value }, - ), - ), - optsWithJsonContentType, - ); - return; + constructor( + req: ITraceable<PenguenoRequest, ServerTrace>, + e: BodyInit | IEither<ResponseBody, ResponseBody>, + opts: TResponseInit, + ) { + const optsWithJsonContentType: TResponseInit = { + ...opts, + headers: { + ...opts.headers, + 'Content-Type': 'application/json', + }, + }; + if (isEither<ResponseBody, ResponseBody>(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<T> = BaseTraceWith | T; export interface ITrace<TraceWith> { - addTrace: Mapper<ITraceWith<TraceWith>, ITrace<TraceWith>>; - trace: SideEffect<ITraceWith<TraceWith>>; + addTrace: Mapper<ITraceWith<TraceWith>, ITrace<TraceWith>>; + trace: SideEffect<ITraceWith<TraceWith>>; } export type ITraceableTuple<T, TraceWith> = [T, BaseTraceWith | TraceWith]; -export type ITraceableMapper<T, _T, TraceWith, W = ITraceable<T, TraceWith>> = ( - w: W, -) => _T; +export type ITraceableMapper<T, _T, TraceWith, W = ITraceable<T, TraceWith>> = (w: W) => _T; export interface ITraceable<T, Trace = BaseTraceWith> { - readonly trace: ITrace<Trace>; - get: Supplier<T>; - move: <_T>(t: _T) => ITraceable<_T, Trace>; - map: <_T>(mapper: ITraceableMapper<T, _T, Trace>) => ITraceable<_T, Trace>; - bimap: <_T>( - mapper: ITraceableMapper< - T, - ITraceableTuple<_T, Array<Trace> | Trace>, - Trace - >, - ) => ITraceable<_T, Trace>; - peek: (peek: ITraceableMapper<T, void, Trace>) => ITraceable<T, Trace>; - flatMap: <_T>( - mapper: ITraceableMapper<T, ITraceable<_T, Trace>, Trace>, - ) => ITraceable<_T, Trace>; - flatMapAsync<_T>( - mapper: ITraceableMapper<T, Promise<ITraceable<_T, Trace>>, Trace>, - ): ITraceable<Promise<_T>, Trace>; + readonly trace: ITrace<Trace>; + get: Supplier<T>; + move: <_T>(t: _T) => ITraceable<_T, Trace>; + map: <_T>(mapper: ITraceableMapper<T, _T, Trace>) => ITraceable<_T, Trace>; + bimap: <_T>(mapper: ITraceableMapper<T, ITraceableTuple<_T, Array<Trace> | Trace>, Trace>) => ITraceable<_T, Trace>; + peek: (peek: ITraceableMapper<T, void, Trace>) => ITraceable<T, Trace>; + flatMap: <_T>(mapper: ITraceableMapper<T, ITraceable<_T, Trace>, Trace>) => ITraceable<_T, Trace>; + flatMapAsync<_T>( + mapper: ITraceableMapper<T, Promise<ITraceable<_T, Trace>>, Trace>, + ): ITraceable<Promise<_T>, Trace>; } export class TraceableImpl<T, TraceWith> implements ITraceable<T, TraceWith> { - protected constructor( - private readonly item: T, - public readonly trace: ITrace<TraceWith>, - ) {} + protected constructor( + private readonly item: T, + public readonly trace: ITrace<TraceWith>, + ) {} - public map<_T>(mapper: ITraceableMapper<T, _T, TraceWith>) { - const result = mapper(this); - return new TraceableImpl(result, this.trace); - } + public map<_T>(mapper: ITraceableMapper<T, _T, TraceWith>) { + const result = mapper(this); + return new TraceableImpl(result, this.trace); + } - public flatMap<_T>( - mapper: ITraceableMapper<T, ITraceable<_T, TraceWith>, TraceWith>, - ): ITraceable<_T, TraceWith> { - return mapper(this); - } + public flatMap<_T>(mapper: ITraceableMapper<T, ITraceable<_T, TraceWith>, TraceWith>): ITraceable<_T, TraceWith> { + return mapper(this); + } - public flatMapAsync<_T>( - mapper: ITraceableMapper<T, Promise<ITraceable<_T, TraceWith>>, TraceWith>, - ): ITraceable<Promise<_T>, TraceWith> { - return new TraceableImpl( - mapper(this).then((t) => t.get()), - this.trace, - ); - } + public flatMapAsync<_T>( + mapper: ITraceableMapper<T, Promise<ITraceable<_T, TraceWith>>, TraceWith>, + ): ITraceable<Promise<_T>, TraceWith> { + return new TraceableImpl( + mapper(this).then((t) => t.get()), + this.trace, + ); + } - public peek(peek: ITraceableMapper<T, void, TraceWith>) { - peek(this); - return this; - } + public peek(peek: ITraceableMapper<T, void, TraceWith>) { + 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>, - 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<T, ITraceableTuple<_T, Array<TraceWith> | 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<Supplier<string> | Error>; const defaultTrace = () => `TimeStamp = ${new Date().toISOString()}`; export class LogTrace implements ITrace<LogTraceSupplier> { - constructor( - private readonly logger: ILogger = new LoggerImpl(), - private readonly traces: Array<LogTraceSupplier> = [defaultTrace], - private readonly defaultLevel: LogLevel = LogLevel.INFO, - private readonly allowedLevels: Supplier< - Array<LogLevel> - > = defaultAllowedLevels, - ) {} + constructor( + private readonly logger: ILogger = new LoggerImpl(), + private readonly traces: Array<LogTraceSupplier> = [defaultTrace], + private readonly defaultLevel: LogLevel = LogLevel.INFO, + private readonly allowedLevels: Supplier<Array<LogLevel>> = defaultAllowedLevels, + ) {} - public addTrace(trace: LogTraceSupplier): ITrace<LogTraceSupplier> { - return new LogTrace( - this.logger, - this.traces.concat(trace), - this.defaultLevel, - this.allowedLevels, - ); - } + public addTrace(trace: LogTraceSupplier): ITrace<LogTraceSupplier> { + 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<LogTraceSupplier>) { - 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<LogTraceSupplier>) { + 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> = [ - LogLevel.DEBUG, - LogLevel.INFO, - LogLevel.WARN, - LogLevel.ERROR, - LogLevel.SYS, -]; +const logLevelOrder: Array<LogLevel> = [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>; + [ + LogLevel.UNKNOWN, + ...(isDebug() ? [LogLevel.DEBUG] : []), + LogLevel.INFO, + LogLevel.WARN, + LogLevel.ERROR, + LogLevel.SYS, + ] as Array<LogLevel>; 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<Array<IMetric>>; - - readonly _tag: "IMetric"; + readonly count: IEmittableMetric; + readonly time: IEmittableMetric; + readonly failure: undefined | IMetric; + readonly success: undefined | IMetric; + readonly warn: undefined | IMetric; + readonly children: Supplier<Array<IMetric>>; + + 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<number, MetricValue>; + readonly name: string; + readonly unit: Unit; + withValue: Mapper<number, MetricValue>; } 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<IMetric | MetricValue | undefined>; type MetricTracingTuple = [IMetric, Date]; export class MetricsTrace implements ITrace<MetricsTraceSupplier> { - constructor( - private readonly metricConsumer: SideEffect<Array<MetricValue>>, - private readonly tracing: Array<MetricTracingTuple> = [], - private readonly flushed: Set<IMetric> = 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<Array<MetricValue>>, + private readonly tracing: Array<MetricTracingTuple> = [], + private readonly flushed: Set<IMetric> = 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<MetricValue> { + if (this.flushed.has(metric)) { + return []; + } - private addMetric(metric: IMetric, startedTracing: Date): Array<MetricValue> { - 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<T> extends TraceableImpl<T, LogTraceSupplier> { - public static LogTrace = new LogTrace(); - static of<T>(t: T) { - return new LogTraceable(t, LogTraceable.LogTrace); - } + public static LogTrace = new LogTrace(); + static of<T>(t: T) { + return new LogTraceable(t, LogTraceable.LogTrace); + } } -const getEmbeddedMetricConsumer = - (logTrace: ITrace<LogTraceSupplier>) => - (metrics: Array<MetricValue>) => +const getEmbeddedMetricConsumer = (logTrace: ITrace<LogTraceSupplier>) => (metrics: Array<MetricValue>) => logTrace.addTrace(LogLevel.SYS).trace(`Metrics = <metrics>${JSON.stringify(metrics)}</metrics>`); -export class EmbeddedMetricsTraceable<T> extends TraceableImpl< - T, - MetricsTraceSupplier -> { - public static MetricsTrace = new MetricsTrace( - getEmbeddedMetricConsumer(LogTraceable.LogTrace), - ); +export class EmbeddedMetricsTraceable<T> extends TraceableImpl<T, MetricsTraceSupplier> { + public static MetricsTrace = new MetricsTrace(getEmbeddedMetricConsumer(LogTraceable.LogTrace)); - static of<T>(t: T, metricsTrace = EmbeddedMetricsTraceable.MetricsTrace) { - return new EmbeddedMetricsTraceable(t, metricsTrace); - } + static of<T>(t: T, metricsTrace = EmbeddedMetricsTraceable.MetricsTrace) { + return new EmbeddedMetricsTraceable(t, metricsTrace); + } } -export type LogMetricTraceSupplier = ITraceWith< - LogTraceSupplier | MetricsTraceSupplier ->; +export type LogMetricTraceSupplier = ITraceWith<LogTraceSupplier | MetricsTraceSupplier>; export class LogMetricTrace implements ITrace<LogMetricTraceSupplier> { - constructor( - private logTrace: ITrace<LogTraceSupplier>, - private metricsTrace: ITrace<MetricsTraceSupplier>, - ) {} + constructor( + private logTrace: ITrace<LogTraceSupplier>, + private metricsTrace: ITrace<MetricsTraceSupplier>, + ) {} - 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<T> extends TraceableImpl< - T, - MetricsTraceSupplier | LogTraceSupplier -> { - static ofLogTraceable<T>(t: ITraceable<T, LogTraceSupplier>) { - const metricsTrace = new MetricsTrace(getEmbeddedMetricConsumer(t.trace)); - return new LogMetricTraceable( - t.get(), - new LogMetricTrace(t.trace, metricsTrace), - ); - } +export class LogMetricTraceable<T> extends TraceableImpl<T, MetricsTraceSupplier | LogTraceSupplier> { + static ofLogTraceable<T>(t: ITraceable<T, LogTraceSupplier>) { + const metricsTrace = new MetricsTrace(getEmbeddedMetricConsumer(t.trace)); + return new LogMetricTraceable(t.get(), new LogMetricTrace(t.trace, metricsTrace)); + } - static of<T>(t: T) { - const logTrace = LogTraceable.of(t); - return LogMetricTraceable.ofLogTraceable(logTrace); - } + static of<T>(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<T, Trace>( - trace: string, - ansi?: Array<keyof typeof ANSI> - ): ITraceableMapper<T, ITraceableTuple<T, Trace | Array<Trace>>, Trace> { - if (ansi) { - return (t) => [t.get(), `${ansi.join("")}${trace}${ANSI.RESET}`]; - } - return (t) => [t.get(), trace]; - } + static withTrace<T, Trace>( + trace: string, + ansi?: Array<keyof typeof ANSI>, + ): ITraceableMapper<T, ITraceableTuple<T, Trace | Array<Trace>>, Trace> { + if (ansi) { + return (t) => [t.get(), `${ansi.join('')}${trace}${ANSI.RESET}`]; + } + return (t) => [t.get(), trace]; + } - static withMetricTrace<T, Trace extends MetricsTraceSupplier>( - metric: IMetric, - ): ITraceableMapper<T, ITraceableTuple<T, Trace | Array<Trace>>, Trace> { - return (t) => [t.get(), metric as Trace]; - } + static withMetricTrace<T, Trace extends MetricsTraceSupplier>( + metric: IMetric, + ): ITraceableMapper<T, ITraceableTuple<T, Trace | Array<Trace>>, Trace> { + return (t) => [t.get(), metric as Trace]; + } - static withFunctionTrace<F extends Callable, T, Trace>( - f: F, - ): ITraceableMapper<T, ITraceableTuple<T, Trace | Array<Trace>>, Trace> { - return TraceUtil.withTrace(`fn.${f.name}`); - } + static withFunctionTrace<F extends Callable, T, Trace>( + f: F, + ): ITraceableMapper<T, ITraceableTuple<T, Trace | Array<Trace>>, Trace> { + return TraceUtil.withTrace(`fn.${f.name}`); + } - static withClassTrace<C extends object, T, Trace>( - c: C, - ): ITraceableMapper<T, ITraceableTuple<T, Trace | Array<Trace>>, Trace> { - return TraceUtil.withTrace(`class.${c.constructor.name}`); - } + static withClassTrace<C extends object, T, Trace>( + c: C, + ): ITraceableMapper<T, ITraceableTuple<T, Trace | Array<Trace>>, Trace> { + return TraceUtil.withTrace(`class.${c.constructor.name}`); + } - static promiseify<T, U, Trace>( - mapper: ITraceableMapper<T, U, Trace>, - ): ITraceableMapper<Promise<T>, Promise<U>, Trace> { - return (traceablePromise) => - traceablePromise - .flatMapAsync(async (t) => t.move(await t.get()).map(mapper)) - .get(); - } + static promiseify<T, U, Trace>( + mapper: ITraceableMapper<T, U, Trace>, + ): ITraceableMapper<Promise<T>, Promise<U>, 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'; // -- <job.exectuor> -- const jobTypeMetric = memoize((type: string) => Metric.fromName(`run.${type}`)); export const executeJob = (tJob: ITraceable<Job, LogMetricTraceSupplier>) => - 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(); // -- </job.exectuor> -- // -- <pipeline.executor> -- -const pipelinesMetric = Metric.fromName("pipelines"); +const pipelinesMetric = Metric.fromName('pipelines'); export const executePipeline = ( - tPipeline: ITraceable<Pipeline, LogMetricTraceSupplier>, - baseEnv?: JobArgT, + tPipeline: ITraceable<Pipeline, LogMetricTraceSupplier>, + baseEnv?: JobArgT, ): Promise<IEither<Error, void>> => - tPipeline - .bimap(TraceUtil.withFunctionTrace(executePipeline)) - .bimap(TraceUtil.withMetricTrace(pipelinesMetric)) - .map(async (tJobs): Promise<IEither<Error, void>> => { - 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) => - <Job>{ - ...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<IEither<Error, void>> => { + 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) => + <Job>{ + ...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(); // -- </pipeline.executor> -- 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) => ( - <AnsiblePlaybookJob> { - type: "ansible_playbook.ts", - arguments: baseArgs, - } - )); +const eitherJob = getRequiredEnvVars(['path', 'playbooks']).mapRight( + (baseArgs) => + <AnsiblePlaybookJob>{ + 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<SecureNote>(tEitherJobVault, key, "ssh_key") - ); - const eitherSshKeyFile = await eitherSshKey.mapRight(({ notes }) => notes) - .flatMapAsync(saveToTempFile); - const eitherAnsibleSecrets = await eitherJobVault - .flatMapAsync(({ key, vault }) => - vault.fetchSecret<SecureNote>(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<SecureNote>(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<SecureNote>(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<IEither<Error, string>> => - 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) => ( - <BuildDockerImageJob> { - 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) => + <BuildDockerImageJob>{ + 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<LoginItem>(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<LoginItem>(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) => - <CheckoutCiJob>{ - type: "checkout_ci.ts", - arguments: { - ...baseArgs, - run, - returnPath: Deno.cwd(), - }, - }, +const eitherJob = getRequiredEnvVars(['remote', 'refname', 'rev']).mapRight( + (baseArgs) => + <CheckoutCiJob>{ + 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 = <FetchCodeJob>{ - type: "fetch_code.ts", - arguments: { - remoteUrl: ciJob.arguments.remote, - checkout: ciJob.arguments.rev, - path: getSrcDirectoryForCiJob(ciJob), - }, - }; - return Either.fromFailableAsync<Error, CheckoutCiJob>(() => - 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<Error, string>(() => - Deno.readTextFile( - `${getSrcDirectoryForCiJob(ciJob)}/${CI_WORKFLOW_FILE}`, - ), - ).then((eitherWorkflowJson) => - eitherWorkflowJson - .flatMap((json) => - Either.fromFailable<Error, unknown>(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 = <FetchCodeJob>{ + type: 'fetch_code.ts', + arguments: { + remoteUrl: ciJob.arguments.remote, + checkout: ciJob.arguments.rev, + path: getSrcDirectoryForCiJob(ciJob), + }, + }; + return Either.fromFailableAsync<Error, CheckoutCiJob>(() => + 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<Error, string>(() => + readFile(join(getSrcDirectoryForCiJob(ciJob), CI_WORKFLOW_FILE), 'utf-8'), + ).then((eitherWorkflowJson) => + eitherWorkflowJson + .flatMap((json) => Either.fromFailable<Error, unknown>(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'; // -- <ISecret> -- 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<TClient, TKey, TItemId> { - unlock: (client: TClient) => Promise<IEither<Error, TKey>>; - lock: (client: TClient, key: TKey) => Promise<IEither<Error, TKey>>; + unlock: (client: TClient) => Promise<IEither<Error, TKey>>; + lock: (client: TClient, key: TKey) => Promise<IEither<Error, TKey>>; - fetchSecret: <T extends SecretItem>( - client: TClient, - key: TKey, - item: TItemId, - ) => Promise<IEither<Error, T>>; + fetchSecret: <T extends SecretItem>(client: TClient, key: TKey, item: TItemId) => Promise<IEither<Error, T>>; } // -- </ISecret> -- @@ -39,156 +35,122 @@ type TClient = ITraceable<unknown, LogMetricTraceSupplier>; type TKey = string; type TItemId = string; export class Bitwarden implements IVault<TClient, TKey, TItemId> { - 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<T extends SecretItem>( - client: TClient, - key: string, - item: string, - ): Promise<IEither<Error, T>> { - 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<Error, Array<T & { name: string }>> => - Either.fromFailable(() => JSON.parse(itemsJson)), + public fetchSecret<T extends SecretItem>(client: TClient, key: string, item: string): Promise<IEither<Error, T>> { + 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<Error, Array<T & { name: string }>> => + Either.fromFailable(() => JSON.parse(itemsJson)), + ) + .flatMap((itemsList): IEither<Error, T> => { + 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<Error, T> => { - 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<Error, BitwardenConfig> { - 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<Error, BitwardenConfig> { + 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; } // -- </IVault> -- 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" }] +} |