@runium/plugin-docker 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js ADDED
@@ -0,0 +1 @@
1
+ import{DockerTask as o}from"./tasks/docker-task.js";import{DockerComposeTask as e}from"./tasks/docker-compose-task.js";import{getDockerTaskProjectSchema as r}from"./validation/docker-task-project-schema.js";import{getDockerComposeTaskProjectSchema as c}from"./validation/docker-compose-task-project-schema.js";function p(){return{name:"docker",project:{tasks:{docker:o,"docker-compose":e},validationSchema:{tasks:{Docker_DockerTask:r(),Docker_DockerComposeTask:c()}}}}}export{p as default};
package/package.json CHANGED
@@ -1,38 +1,18 @@
1
1
  {
2
2
  "name": "@runium/plugin-docker",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Runium plugin for Docker support",
5
5
  "author": "TheBeastApp",
6
6
  "license": "MIT",
7
7
  "type": "module",
8
- "main": "dist/index.js",
9
- "scripts": {
10
- "build": "rimraf ./lib && node build.js",
11
- "build:tsc": "rimraf ./lib && tsc",
12
- "lint": "eslint ./src --ext .ts",
13
- "lint:fix": "eslint ./src --ext .ts --fix",
14
- "format": "prettier --write \"src/**/*.ts\""
15
- },
16
8
  "keywords": [
17
9
  "runium",
18
10
  "runium-plugin",
19
11
  "docker"
20
12
  ],
21
- "devDependencies": {
22
- "@runium/types-plugin": "^0.0.6",
23
- "@typescript-eslint/eslint-plugin": "^7.0.0",
24
- "@typescript-eslint/parser": "^7.0.0",
25
- "esbuild": "^0.25.11",
26
- "eslint": "^8.56.0",
27
- "eslint-config-prettier": "^9.1.0",
28
- "eslint-plugin-prettier": "^5.1.3",
29
- "prettier": "^3.2.5",
30
- "rimraf": "^6.1.0",
31
- "ts-node": "^10.9.2",
32
- "typescript": "^5.3.3",
33
- "vite": "^7.1.12"
34
- },
13
+ "module": "./index.js",
14
+ "main": "./index.js",
35
15
  "dependencies": {
36
16
  "yaml": "^2.8.2"
37
17
  }
38
- }
18
+ }
@@ -0,0 +1 @@
1
+ import{spawn as a}from"node:child_process";import{stringify as p}from"yaml";import{DockerTaskBase as c}from"./docker-task-base.js";import{isDockerInstalled as m}from"../utils/is-docker-installed.js";import{isDockerRunning as d}from"../utils/is-docker-running.js";import{pullImage as g}from"../utils/pull-image.js";const{TaskStatus:s}=runium.enum;class C extends c{constructor(r){super(r);this.options=r;this.projectName=`runium-docker-compose-${Date.now()}`,this.composeFile=`docker-compose-${this.projectName}.yml`}projectName;composeFile;version="3.8";correctExitCodes=[130];generateComposeYaml(){const r={version:this.version,services:{}};for(const[o,e]of Object.entries(this.options.services)){const t={image:e.image};e.containerName&&(t.container_name=e.containerName),e.command&&(t.command=e.command),e.ports&&e.ports.length>0&&(t.ports=e.ports),e.volumes&&e.volumes.length>0&&(t.volumes=e.volumes),e.environment&&Object.keys(e.environment).length>0&&(t.environment=e.environment),e.networks&&e.networks.length>0&&(t.networks=e.networks),e.dependsOn&&e.dependsOn.length>0&&(t.depends_on=e.dependsOn),e.restart&&(t.restart=e.restart),e.user&&(t.user=e.user),e.workdir&&(t.working_dir=e.workdir),e.entrypoint&&(t.entrypoint=e.entrypoint),e.privileged&&(t.privileged=e.privileged),e.expose&&e.expose.length>0&&(t.expose=e.expose),e.healthcheck&&(t.healthcheck=e.healthcheck),r.services[o]=t}return this.options.networks&&Object.keys(this.options.networks).length>0&&(r.networks=this.options.networks),this.options.volumes&&Object.keys(this.options.volumes).length>0&&(r.volumes=this.options.volumes),p(r)}async pullAllImages(){const r=Object.values(this.options.services).map(e=>e.image),o=[...new Set(r)];for(const e of o)this.updateState({reason:`pull image: ${e}`}),await g(e,{onStdErr:this.onStdErrData.bind(this)})}prepareDockerComposeArgs(r){const o=["compose"];return o.push("-p",this.projectName),o.push("-f",runium.storage.getPath(["docker",this.composeFile])),o.push(r),o}async start(){if(this.canStart()){if(this.updateState({status:s.STARTING,iteration:this.state.iteration+1,pid:-1,exitCode:void 0,error:void 0,reason:void 0}),!await m()){this.onError(new Error("Docker is not installed"));return}if(!await d()){this.onError(new Error("Docker is not running"));return}try{const r=this.generateComposeYaml();await runium.storage.write(["docker",this.composeFile],r),await this.initLogStreams(),await this.pullAllImages();const o=this.prepareDockerComposeArgs("up");this.process=a("docker",o,{stdio:["ignore","pipe","pipe"]}),this.addProcessListeners(),this.setTTLTimer(),this.updateState({status:s.STARTED,pid:this.process.pid,reason:void 0})}catch(r){this.onError(r)}}}async stop(r=""){if(this.canStop()){this.updateState({status:s.STOPPING,reason:r});try{const o=this.prepareDockerComposeArgs("down");await new Promise((e,t)=>{const n=a("docker",o);n.on("exit",i=>{i===0?e():t(new Error(`Docker compose down exited with code ${i}`))}),n.on("error",t)})}catch(o){this.onError(o)}}}}export{C as DockerComposeTask};
@@ -0,0 +1 @@
1
+ import{createWriteStream as o}from"node:fs";import{mkdir as a}from"node:fs/promises";import{dirname as n,resolve as u}from"node:path";import{setTimeout as l}from"node:timers";const d=-1,m=50,{TaskStatus:s,TaskEvent:i}=runium.enum;class P extends runium.class.RuniumTask{constructor(t){super(t);this.options=t;this.setMaxListeners(m)}correctExitCodes=[];state={status:s.IDLE,timestamp:Date.now(),iteration:0,pid:-1};process=null;stdoutStream=null;stderrStream=null;ttlTimer=null;getState(){return{...this.state}}getOptions(){return{...this.options}}canStart(){const{status:t}=this.state;return t!==s.STARTED&&t!==s.STARTING&&t!==s.STOPPING}canStop(){const{status:t}=this.state;return t===s.STARTED||t===s.STARTING}async restart(){await this.stop("restart"),await this.start()}async initLogStreams(){const{stdout:t=null,stderr:e=null}=this.options.log||{};if(t){const r=u(t);await a(n(r),{recursive:!0}),this.stdoutStream=o(t,{flags:"w"})}if(e){const r=u(e);await a(n(r),{recursive:!0}),this.stderrStream=o(e,{flags:"w"})}}setTTLTimer(){const{ttl:t}=this.options;t&&this.process&&(this.ttlTimer=l(()=>{this.stop("ttl")},t))}addProcessListeners(){this.process&&(this.process.stdout?.on("data",t=>this.onStdOutData(t)),this.process.stderr?.on("data",t=>this.onStdErrData(t)),this.process.on("exit",t=>this.onExit(t)),this.process.on("error",t=>this.onError(t)))}onStdOutData(t){const e=t.toString();this.emit(i.STDOUT,e),this.stdoutStream&&this.stdoutStream.write(e)}onStdErrData(t){const e=t.toString();this.emit(i.STDERR,e),this.stderrStream&&this.stderrStream.write(e)}onExit(t){const e=t!==null&&!this.correctExitCodes.includes(t)?t:d;this.updateState({status:e===0?s.COMPLETED:e===d?s.STOPPED:s.FAILED,exitCode:e}),this.cleanup()}onError(t){const e=t?.code??null;this.updateState({status:e===null?s.STOPPED:s.FAILED,reason:t.message||(e??"")}),this.cleanup()}updateState(t){const e={...this.state,...t,timestamp:Date.now()};this.state=Object.fromEntries(Object.entries(e).filter(([r,c])=>c!==void 0)),this.emit(i.STATE_CHANGE,this.getState()),this.emit(this.state.status,this.getState())}cleanup(){this.ttlTimer&&(clearTimeout(this.ttlTimer),this.ttlTimer=null),this.process&&(this.process.removeAllListeners(),this.process.stdout?.removeAllListeners(),this.process.stderr?.removeAllListeners(),this.process=null),this.stdoutStream&&(this.stdoutStream.end(),this.stdoutStream=null),this.stderrStream&&(this.stderrStream.end(),this.stderrStream=null)}}export{P as DockerTaskBase,d as SILENT_EXIT_CODE};
@@ -0,0 +1 @@
1
+ import{spawn as n}from"node:child_process";import{DockerTaskBase as a}from"./docker-task-base.js";import{isDockerInstalled as p}from"../utils/is-docker-installed.js";import{isDockerRunning as h}from"../utils/is-docker-running.js";import{pullImage as c}from"../utils/pull-image.js";const{TaskStatus:o}=runium.enum;class T extends a{constructor(t){super(t);this.options=t;this.options.containerName??=`runium-${this.options.image.replaceAll("/","-").replaceAll(":","-")}-${Date.now()}`}correctExitCodes=[137];prepareDockerArgs(){const t=["run"];return this.options.autoRemove!==!1&&t.push("--rm"),this.options.containerName&&t.push("--name",this.options.containerName),this.options.ports&&this.options.ports.forEach(s=>{t.push("-p",s)}),this.options.volumes&&this.options.volumes.forEach(s=>{t.push("-v",s)}),this.options.env&&Object.entries(this.options.env).forEach(([s,i])=>{t.push("-e",`${s}=${i}`)}),this.options.network&&t.push("--network",this.options.network),this.options.user&&t.push("--user",this.options.user),this.options.workdir&&t.push("--workdir",this.options.workdir),this.options.entrypoint&&t.push("--entrypoint",this.options.entrypoint),this.options.privileged&&t.push("--privileged"),t.push(this.options.image),this.options.command&&t.push(this.options.command),this.options.arguments&&t.push(...this.options.arguments),t}async start(){if(this.canStart()){if(this.updateState({status:o.STARTING,iteration:this.state.iteration+1,pid:-1,containerName:void 0,exitCode:void 0,error:void 0,reason:void 0}),!await p()){this.onError(new Error("Docker is not installed"));return}if(!await h()){this.onError(new Error("Docker is not running"));return}try{await this.initLogStreams(),this.updateState({reason:`pull image: ${this.options.image}`}),await c(this.options.image,{onStdErr:this.onStdErrData.bind(this)});const t=this.prepareDockerArgs();this.process=n("docker",t,{stdio:["ignore","pipe","pipe"]}),this.addProcessListeners(),this.setTTLTimer(),this.updateState({status:o.STARTED,containerName:this.options.containerName,pid:this.process.pid,reason:void 0})}catch(t){this.onError(t)}}}async stop(t=""){if(this.canStop()){this.updateState({status:o.STOPPING,reason:t});try{await new Promise((s,i)=>{const e=n("docker",["stop",this.options.containerName]);e.on("exit",r=>{r===0?s():i(new Error(`Docker stop exited with code ${r}`))}),e.on("error",i)})}catch(s){this.onError(s)}finally{await new Promise(s=>{this.process?.kill("SIGTERM"),this.process?.on("exit",()=>{s()})})}}}}export{T as DockerTask};
@@ -0,0 +1 @@
1
+ import{exec as o}from"node:child_process";import{promisify as r}from"node:util";const c=r(o);export{c as execAsync};
@@ -0,0 +1 @@
1
+ import{execAsync as e}from"./exec-async.js";async function o(){try{return await e("docker --version"),!0}catch{return!1}}export{o as isDockerInstalled};
@@ -0,0 +1 @@
1
+ import{execAsync as r}from"./exec-async.js";async function n(){try{return await r("docker info"),!0}catch{return!1}}export{n as isDockerRunning};
@@ -0,0 +1 @@
1
+ import{exec as d}from"node:child_process";async function l(n,e={}){return new Promise((u,o)=>{const r=d(`docker pull ${n}`);r.stdout&&e.onStdOut&&r.stdout.on("data",t=>{e.onStdOut(t.toString())}),r.stderr&&e.onStdErr&&r.stderr.on("data",t=>{e.onStdErr(t.toString())}),r.on("exit",t=>{t===0?u():o({code:t,message:t===null?"":`docker pull exited with code ${t}`})}),r.on("error",t=>{o({message:t.message,code:-1})})})}export{l as pullImage};
@@ -0,0 +1 @@
1
+ function r(){return{type:"docker-compose",options:{type:"object",properties:{services:{type:"object",patternProperties:{"^[a-zA-Z0-9_-]+$":{type:"object",properties:{image:{type:"string"},containerName:{type:"string"},command:{type:"string"},ports:{type:"array",items:{type:"string"}},volumes:{type:"array",items:{type:"string"}},environment:{$ref:"#/$defs/Runium_Env"},networks:{type:"array",items:{type:"string"}},dependsOn:{type:"array",items:{type:"string"}},restart:{type:"string",enum:["no","always","on-failure","unless-stopped"]},user:{type:"string"},workdir:{type:"string"},entrypoint:{type:"string"},privileged:{type:"boolean"},expose:{type:"array",items:{type:"string"}},healthcheck:{type:"object",properties:{test:{oneOf:[{type:"string"},{type:"array",items:{type:"string"}}]},interval:{type:"string"},timeout:{type:"string"},retries:{type:"number"},start_period:{type:"string"}},required:["test"],additionalProperties:!1}},required:["image"],additionalProperties:!1}},additionalProperties:!1},networks:{type:"object",patternProperties:{"^[a-zA-Z0-9_-]+$":{type:"object"}},additionalProperties:!1},volumes:{type:"object",patternProperties:{"^[a-zA-Z0-9_-]+$":{type:"object"}},additionalProperties:!1},ttl:{type:"number"},log:{$ref:"#/$defs/Runium_TaskLog"}},required:["services"],additionalProperties:!1}}}export{r as getDockerComposeTaskProjectSchema};
@@ -0,0 +1 @@
1
+ function r(){return{type:"docker",options:{type:"object",properties:{image:{type:"string"},containerName:{type:"string"},command:{type:"string"},arguments:{type:"array",items:{type:"string"}},ports:{type:"array",items:{type:"string"}},volumes:{type:"array",items:{type:"string"}},env:{$ref:"#/$defs/Runium_Env"},network:{type:"string"},autoRemove:{type:"boolean"},user:{type:"string"},workdir:{type:"string"},entrypoint:{type:"string"},privileged:{type:"boolean"},ttl:{type:"number"},log:{$ref:"#/$defs/Runium_TaskLog"}},required:["image"],additionalProperties:!1}}}export{r as getDockerTaskProjectSchema};
package/.eslintrc.json DELETED
@@ -1,19 +0,0 @@
1
- {
2
- "parser": "@typescript-eslint/parser",
3
- "extends": [
4
- "eslint:recommended",
5
- "plugin:@typescript-eslint/recommended",
6
- "plugin:prettier/recommended"
7
- ],
8
- "parserOptions": {
9
- "ecmaVersion": 2022,
10
- "sourceType": "module"
11
- },
12
- "rules": {
13
- "@typescript-eslint/no-explicit-any": "warn",
14
- "@typescript-eslint/explicit-module-boundary-types": "off",
15
- "@typescript-eslint/no-unused-vars": ["error", {
16
- "argsIgnorePattern": "^(_)$"
17
- }]
18
- }
19
- }
package/.prettierrc.json DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "semi": true,
3
- "trailingComma": "es5",
4
- "singleQuote": true,
5
- "printWidth": 80,
6
- "tabWidth": 2,
7
- "arrowParens": "avoid"
8
- }
package/build.js DELETED
@@ -1,95 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import {
4
- chmodSync,
5
- existsSync,
6
- mkdirSync,
7
- readFileSync,
8
- writeFileSync,
9
- } from 'node:fs';
10
- import { resolve } from 'node:path';
11
- import { build } from 'esbuild';
12
-
13
- const buildConfig = {
14
- entryPoints: ['src/**/*.ts'],
15
- bundle: false,
16
- platform: 'node',
17
- target: 'esnext',
18
- format: 'esm',
19
- outdir: 'lib',
20
- sourcemap: false,
21
- minify: true,
22
- treeShaking: false,
23
- plugins: [],
24
- metafile: true,
25
- logLevel: 'info',
26
- };
27
-
28
- async function buildProject() {
29
- try {
30
- console.log('🔨 Building project with esbuild...');
31
-
32
- const libDir = 'lib';
33
- if (!existsSync(libDir)) {
34
- mkdirSync(libDir, { recursive: true });
35
- }
36
-
37
- const result = await build(buildConfig);
38
-
39
- if (process.platform !== 'win32') {
40
- try {
41
- chmodSync('lib/index.js', '755');
42
- console.log('🔐 Made lib/index.js executable');
43
- } catch (error) {
44
- console.warn('⚠️ Could not make file executable:', error.message);
45
- }
46
- }
47
-
48
- console.log('🔨 Processing package.json...');
49
- await processPackageJson();
50
-
51
- console.log('✅ Build completed successfully!');
52
-
53
- // Print build stats if in development mode
54
- if (result.metafile) {
55
- console.log('\n📊 Build statistics:');
56
- const outputs = result.metafile.outputs;
57
- for (const [file, info] of Object.entries(outputs)) {
58
- console.log(` ${file}: ${(info.bytes / 1024).toFixed(2)} KB`);
59
- }
60
- }
61
- } catch (error) {
62
- console.error('❌ Build failed:', error);
63
- process.exit(1);
64
- }
65
- }
66
-
67
- async function processPackageJson() {
68
- let originalPackageJson = readFileSync('./package.json', {
69
- encoding: 'utf-8',
70
- });
71
-
72
- let packageJson = JSON.parse(originalPackageJson);
73
- const { dependencies } = packageJson;
74
-
75
- delete packageJson.dependencies;
76
- delete packageJson.devDependencies;
77
- delete packageJson.scripts;
78
- delete packageJson.module;
79
- delete packageJson.main;
80
-
81
- console.log('✅ Processed package.json', packageJson);
82
-
83
- packageJson = Object.assign(packageJson, {
84
- module: './index.js',
85
- main: './index.js',
86
- dependencies,
87
- });
88
-
89
- writeFileSync(
90
- resolve(import.meta.dirname, 'lib', 'package.json'),
91
- JSON.stringify(packageJson, null, 2)
92
- );
93
- }
94
-
95
- buildProject();
package/src/index.ts DELETED
@@ -1,23 +0,0 @@
1
- import { Plugin } from '@runium/types-plugin';
2
- import { DockerTask } from './tasks/docker-task.js';
3
- import { DockerComposeTask } from './tasks/docker-compose-task.js';
4
- import { getDockerTaskProjectSchema } from './validation/docker-task-project-schema.js';
5
- import { getDockerComposeTaskProjectSchema } from './validation/docker-compose-task-project-schema.js';
6
-
7
- export default function (): Plugin {
8
- return {
9
- name: 'docker',
10
- project: {
11
- tasks: {
12
- docker: DockerTask,
13
- 'docker-compose': DockerComposeTask,
14
- },
15
- validationSchema: {
16
- tasks: {
17
- Docker_DockerTask: getDockerTaskProjectSchema(),
18
- Docker_DockerComposeTask: getDockerComposeTaskProjectSchema(),
19
- },
20
- },
21
- },
22
- } as Plugin;
23
- }
@@ -1,299 +0,0 @@
1
- import { spawn, SpawnOptionsWithoutStdio } from 'node:child_process';
2
- import { stringify } from 'yaml';
3
- import {
4
- DockerTaskBase,
5
- DockerTaskBaseOptions,
6
- DockerTaskBaseState,
7
- } from './docker-task-base.js';
8
- import { isDockerInstalled } from '../utils/is-docker-installed.js';
9
- import { isDockerRunning } from '../utils/is-docker-running.js';
10
- import { pullImage } from '../utils/pull-image.js';
11
-
12
- /**
13
- * Docker service configuration
14
- */
15
- export interface DockerComposeService {
16
- image: string;
17
- containerName?: string;
18
- command?: string;
19
- ports?: string[];
20
- volumes?: string[];
21
- environment?: { [key: string]: string | number | boolean };
22
- networks?: string[];
23
- dependsOn?: string[];
24
- restart?: 'no' | 'always' | 'on-failure' | 'unless-stopped';
25
- user?: string;
26
- workdir?: string;
27
- entrypoint?: string | string[];
28
- privileged?: boolean;
29
- expose?: string[];
30
- healthcheck?: {
31
- test: string | string[];
32
- interval?: string;
33
- timeout?: string;
34
- retries?: number;
35
- start_period?: string;
36
- };
37
- }
38
-
39
- /**
40
- * Docker compose task options
41
- */
42
- export interface DockerComposeTaskOptions extends DockerTaskBaseOptions {
43
- services: { [serviceName: string]: DockerComposeService };
44
- networks?: { [networkName: string]: unknown };
45
- volumes?: { [volumeName: string]: unknown };
46
- }
47
-
48
- /**
49
- * Docker compose task state
50
- */
51
- export type DockerComposeTaskState = DockerTaskBaseState;
52
-
53
- interface DockerComposeYaml {
54
- version: string;
55
- services: Record<string, Record<string, unknown>>;
56
- networks?: Record<string, unknown>;
57
- volumes?: Record<string, unknown>;
58
- }
59
-
60
- const { TaskStatus } = runium.enum;
61
-
62
- /**
63
- * Docker compose task class
64
- */
65
- export class DockerComposeTask extends DockerTaskBase<
66
- DockerComposeTaskOptions,
67
- DockerComposeTaskState
68
- > {
69
- protected readonly projectName: string;
70
- protected readonly composeFile: string;
71
- protected readonly version: string = '3.8';
72
-
73
- // 128 + SIGTERM when stopping docker-compose
74
- protected correctExitCodes: number[] = [130];
75
-
76
- constructor(protected readonly options: DockerComposeTaskOptions) {
77
- super(options);
78
-
79
- // TODO: unique project name
80
- this.projectName = `runium-docker-compose-${Date.now()}`;
81
- this.composeFile = `docker-compose-${this.projectName}.yml`;
82
- }
83
-
84
- /**
85
- * Generate docker-compose YAML content
86
- */
87
- private generateComposeYaml(): string {
88
- const compose: DockerComposeYaml = {
89
- version: this.version,
90
- services: {},
91
- };
92
-
93
- // services
94
- for (const [serviceName, service] of Object.entries(
95
- this.options.services
96
- )) {
97
- const serviceConfig: Record<string, unknown> = {
98
- image: service.image,
99
- };
100
-
101
- if (service.containerName) {
102
- serviceConfig.container_name = service.containerName;
103
- }
104
-
105
- if (service.command) {
106
- serviceConfig.command = service.command;
107
- }
108
-
109
- if (service.ports && service.ports.length > 0) {
110
- serviceConfig.ports = service.ports;
111
- }
112
-
113
- if (service.volumes && service.volumes.length > 0) {
114
- serviceConfig.volumes = service.volumes;
115
- }
116
-
117
- if (service.environment && Object.keys(service.environment).length > 0) {
118
- serviceConfig.environment = service.environment;
119
- }
120
-
121
- if (service.networks && service.networks.length > 0) {
122
- serviceConfig.networks = service.networks;
123
- }
124
-
125
- if (service.dependsOn && service.dependsOn.length > 0) {
126
- serviceConfig.depends_on = service.dependsOn;
127
- }
128
-
129
- if (service.restart) {
130
- serviceConfig.restart = service.restart;
131
- }
132
-
133
- if (service.user) {
134
- serviceConfig.user = service.user;
135
- }
136
-
137
- if (service.workdir) {
138
- serviceConfig.working_dir = service.workdir;
139
- }
140
-
141
- if (service.entrypoint) {
142
- serviceConfig.entrypoint = service.entrypoint;
143
- }
144
-
145
- if (service.privileged) {
146
- serviceConfig.privileged = service.privileged;
147
- }
148
-
149
- if (service.expose && service.expose.length > 0) {
150
- serviceConfig.expose = service.expose;
151
- }
152
-
153
- if (service.healthcheck) {
154
- serviceConfig.healthcheck = service.healthcheck;
155
- }
156
-
157
- compose.services[serviceName] = serviceConfig;
158
- }
159
-
160
- // networks if specified
161
- if (
162
- this.options.networks &&
163
- Object.keys(this.options.networks).length > 0
164
- ) {
165
- compose.networks = this.options.networks;
166
- }
167
-
168
- // volumes if specified
169
- if (this.options.volumes && Object.keys(this.options.volumes).length > 0) {
170
- compose.volumes = this.options.volumes;
171
- }
172
-
173
- // convert to YAML format using yaml package
174
- return stringify(compose);
175
- }
176
-
177
- /**
178
- * Pull all images for services
179
- */
180
- private async pullAllImages(): Promise<void> {
181
- const images = Object.values(this.options.services).map(
182
- service => service.image
183
- );
184
- const uniqueImages = [...new Set(images)];
185
-
186
- for (const image of uniqueImages) {
187
- this.updateState({
188
- reason: `pull image: ${image}`,
189
- });
190
- await pullImage(image, {
191
- onStdErr: this.onStdErrData.bind(this),
192
- });
193
- }
194
- }
195
-
196
- /**
197
- * Prepare docker-compose command arguments
198
- */
199
- private prepareDockerComposeArgs(command: string): string[] {
200
- const args: string[] = ['compose'];
201
-
202
- args.push('-p', this.projectName);
203
- args.push('-f', runium.storage.getPath(['docker', this.composeFile]));
204
- args.push(command);
205
-
206
- return args;
207
- }
208
-
209
- /**
210
- * Start task
211
- */
212
- async start(): Promise<void> {
213
- if (!this.canStart()) {
214
- return;
215
- }
216
-
217
- this.updateState({
218
- status: TaskStatus.STARTING,
219
- iteration: this.state.iteration + 1,
220
- pid: -1,
221
- exitCode: undefined,
222
- error: undefined,
223
- reason: undefined,
224
- });
225
-
226
- if (!(await isDockerInstalled())) {
227
- this.onError(new Error('Docker is not installed'));
228
- return;
229
- }
230
-
231
- if (!(await isDockerRunning())) {
232
- this.onError(new Error('Docker is not running'));
233
- return;
234
- }
235
-
236
- try {
237
- // generate docker-compose yaml file
238
- const yamlContent = this.generateComposeYaml();
239
- await runium.storage.write(['docker', this.composeFile], yamlContent);
240
-
241
- await this.initLogStreams();
242
-
243
- await this.pullAllImages();
244
-
245
- // Start docker-compose
246
- const args = this.prepareDockerComposeArgs('up');
247
-
248
- this.process = spawn('docker', args, {
249
- stdio: ['ignore', 'pipe', 'pipe'],
250
- } as SpawnOptionsWithoutStdio);
251
-
252
- this.addProcessListeners();
253
-
254
- this.setTTLTimer();
255
-
256
- this.updateState({
257
- status: TaskStatus.STARTED,
258
- pid: this.process!.pid,
259
- reason: undefined,
260
- });
261
- } catch (error) {
262
- this.onError(error as Error);
263
- }
264
- }
265
-
266
- /**
267
- * Stop task
268
- * @param reason
269
- */
270
- async stop(reason: string = ''): Promise<void> {
271
- if (!this.canStop()) {
272
- return;
273
- }
274
-
275
- this.updateState({
276
- status: TaskStatus.STOPPING,
277
- reason,
278
- });
279
-
280
- try {
281
- // stop docker-compose services
282
- const args = this.prepareDockerComposeArgs('down');
283
-
284
- await new Promise<void>((resolve, reject) => {
285
- const stopProcess = spawn('docker', args);
286
- stopProcess.on('exit', code => {
287
- if (code === 0) {
288
- resolve();
289
- } else {
290
- reject(new Error(`Docker compose down exited with code ${code}`));
291
- }
292
- });
293
- stopProcess.on('error', reject);
294
- });
295
- } catch (error) {
296
- this.onError(error as Error);
297
- }
298
- }
299
- }
@@ -1,257 +0,0 @@
1
- import { ChildProcessWithoutNullStreams } from 'node:child_process';
2
- import { createWriteStream, WriteStream } from 'node:fs';
3
- import { mkdir } from 'node:fs/promises';
4
- import { dirname, resolve } from 'node:path';
5
- import { setTimeout } from 'node:timers';
6
- import * as Runium from '@runium/types-plugin';
7
-
8
- export const SILENT_EXIT_CODE = -1;
9
-
10
- const MAX_LISTENERS_COUNT = 50;
11
-
12
- const { TaskStatus, TaskEvent } = runium.enum;
13
-
14
- /**
15
- * Docker task base options
16
- */
17
- export interface DockerTaskBaseOptions {
18
- ttl?: number;
19
- log?: {
20
- stdout?: string | null;
21
- stderr?: string | null;
22
- };
23
- }
24
-
25
- /**
26
- * Docker task base state
27
- */
28
- export interface DockerTaskBaseState extends Runium.RuniumTaskState {
29
- pid?: number;
30
- }
31
-
32
- /**
33
- * Docker task base class
34
- */
35
- export abstract class DockerTaskBase<
36
- Options extends DockerTaskBaseOptions = DockerTaskBaseOptions,
37
- State extends DockerTaskBaseState = DockerTaskBaseState,
38
- > extends runium.class.RuniumTask<Options, State> {
39
- protected correctExitCodes: number[] = [];
40
-
41
- protected state: State = {
42
- status: TaskStatus.IDLE,
43
- timestamp: Date.now(),
44
- iteration: 0,
45
- pid: -1,
46
- } as State;
47
-
48
- protected process: ChildProcessWithoutNullStreams | null = null;
49
- protected stdoutStream: WriteStream | null = null;
50
- protected stderrStream: WriteStream | null = null;
51
- protected ttlTimer: NodeJS.Timeout | null = null;
52
-
53
- constructor(protected readonly options: Options) {
54
- super(options);
55
- this.setMaxListeners(MAX_LISTENERS_COUNT);
56
- }
57
-
58
- /**
59
- * Get task state
60
- */
61
- public getState(): State {
62
- return { ...this.state };
63
- }
64
-
65
- /**
66
- * Get task options
67
- */
68
- public getOptions(): Options {
69
- return { ...this.options };
70
- }
71
-
72
- /**
73
- * Check if task can start
74
- */
75
- protected canStart(): boolean {
76
- const { status } = this.state;
77
- return (
78
- status !== TaskStatus.STARTED &&
79
- status !== TaskStatus.STARTING &&
80
- status !== TaskStatus.STOPPING
81
- );
82
- }
83
-
84
- /**
85
- * Check if task can stop
86
- */
87
- protected canStop(): boolean {
88
- const { status } = this.state;
89
- return status === TaskStatus.STARTED || status === TaskStatus.STARTING;
90
- }
91
-
92
- /**
93
- * Start task
94
- */
95
- abstract start(): Promise<void>;
96
-
97
- /**
98
- * Stop task
99
- * @param reason
100
- */
101
- abstract stop(reason: string): Promise<void>;
102
-
103
- /**
104
- * Restart task
105
- */
106
- async restart(): Promise<void> {
107
- await this.stop('restart');
108
- await this.start();
109
- }
110
-
111
- /**
112
- * Initialize log streams
113
- */
114
- protected async initLogStreams(): Promise<void> {
115
- const { stdout = null, stderr = null } = this.options.log || {};
116
- if (stdout) {
117
- const stdOutPath = resolve(stdout);
118
- await mkdir(dirname(stdOutPath), { recursive: true });
119
- this.stdoutStream = createWriteStream(stdout, {
120
- flags: 'w',
121
- });
122
- }
123
- if (stderr) {
124
- const stdErrPath = resolve(stderr);
125
- await mkdir(dirname(stdErrPath), { recursive: true });
126
- this.stderrStream = createWriteStream(stderr, {
127
- flags: 'w',
128
- });
129
- }
130
- }
131
-
132
- /**
133
- * Set TTL timer
134
- */
135
- protected setTTLTimer(): void {
136
- const { ttl } = this.options;
137
- if (ttl && this.process) {
138
- this.ttlTimer = setTimeout(() => {
139
- this.stop('ttl');
140
- }, ttl);
141
- }
142
- }
143
-
144
- /**
145
- * Add process listeners
146
- */
147
- protected addProcessListeners(): void {
148
- if (!this.process) return;
149
-
150
- this.process.stdout?.on('data', (data: Buffer) => this.onStdOutData(data));
151
-
152
- this.process.stderr?.on('data', (data: Buffer) => this.onStdErrData(data));
153
-
154
- this.process.on('exit', (code: number | null) => this.onExit(code));
155
-
156
- this.process.on('error', (error: Error) => this.onError(error));
157
- }
158
-
159
- /**
160
- * On standard output data
161
- */
162
- protected onStdOutData(data: Buffer): void {
163
- const output = data.toString();
164
- this.emit(TaskEvent.STDOUT, output);
165
- if (this.stdoutStream) {
166
- this.stdoutStream!.write(output);
167
- }
168
- }
169
-
170
- /**
171
- * On standard error data
172
- */
173
- protected onStdErrData(data: Buffer): void {
174
- const output = data.toString();
175
- this.emit(TaskEvent.STDERR, output);
176
- if (this.stderrStream) {
177
- this.stderrStream!.write(output);
178
- }
179
- }
180
-
181
- /**
182
- * On exit
183
- * @param code
184
- */
185
- protected onExit(code: number | null): void {
186
- const exitCode =
187
- code !== null && !this.correctExitCodes.includes(code)
188
- ? code
189
- : SILENT_EXIT_CODE;
190
-
191
- this.updateState({
192
- status:
193
- exitCode === 0
194
- ? TaskStatus.COMPLETED
195
- : exitCode === SILENT_EXIT_CODE
196
- ? TaskStatus.STOPPED
197
- : TaskStatus.FAILED,
198
- exitCode,
199
- } as State);
200
-
201
- this.cleanup();
202
- }
203
-
204
- /**
205
- * On error
206
- */
207
- protected onError(error: Error): void {
208
- const code = ((error as { code?: string })?.code ?? null) as number | null;
209
- this.updateState({
210
- status: code === null ? TaskStatus.STOPPED : TaskStatus.FAILED,
211
- reason: error.message || (code ?? ''),
212
- } as State);
213
-
214
- this.cleanup();
215
- }
216
-
217
- /**
218
- * Update task state
219
- */
220
- protected updateState(state: Partial<State>): void {
221
- const newState = { ...this.state, ...state, timestamp: Date.now() };
222
- this.state = Object.fromEntries(
223
- Object.entries(newState).filter(([_, value]) => {
224
- return value !== undefined;
225
- })
226
- ) as unknown as State;
227
- this.emit(TaskEvent.STATE_CHANGE, this.getState());
228
- this.emit(this.state.status, this.getState());
229
- }
230
-
231
- /**
232
- * Cleanup
233
- */
234
- protected cleanup(): void {
235
- if (this.ttlTimer) {
236
- clearTimeout(this.ttlTimer);
237
- this.ttlTimer = null;
238
- }
239
-
240
- if (this.process) {
241
- this.process.removeAllListeners();
242
- this.process.stdout?.removeAllListeners();
243
- this.process.stderr?.removeAllListeners();
244
- this.process = null;
245
- }
246
-
247
- if (this.stdoutStream) {
248
- this.stdoutStream.end();
249
- this.stdoutStream = null;
250
- }
251
-
252
- if (this.stderrStream) {
253
- this.stderrStream.end();
254
- this.stderrStream = null;
255
- }
256
- }
257
- }
@@ -1,221 +0,0 @@
1
- import { spawn, SpawnOptionsWithoutStdio } from 'node:child_process';
2
- import {
3
- DockerTaskBase,
4
- DockerTaskBaseOptions,
5
- DockerTaskBaseState,
6
- } from './docker-task-base.js';
7
- import { isDockerInstalled } from '../utils/is-docker-installed.js';
8
- import { isDockerRunning } from '../utils/is-docker-running.js';
9
- import { pullImage } from '../utils/pull-image.js';
10
-
11
- /**
12
- * Docker task options
13
- */
14
- export interface DockerTaskOptions extends DockerTaskBaseOptions {
15
- // TODO: support more options
16
- // env-file, expose, healthchecking, mount
17
- image: string;
18
- containerName?: string;
19
- command?: string;
20
- arguments?: string[];
21
- ports?: string[];
22
- volumes?: string[];
23
- env?: { [key: string]: string | number | boolean };
24
- network?: string;
25
- autoRemove?: boolean;
26
- user?: string;
27
- workdir?: string;
28
- entrypoint?: string;
29
- privileged?: boolean;
30
- }
31
-
32
- /**
33
- * Docker task state
34
- */
35
- export interface DockerTaskState extends DockerTaskBaseState {
36
- containerName?: string;
37
- }
38
-
39
- const { TaskStatus } = runium.enum;
40
-
41
- /**
42
- * Docker task class
43
- */
44
- export class DockerTask extends DockerTaskBase<
45
- DockerTaskOptions,
46
- DockerTaskState
47
- > {
48
- // 128 + SIGKILL when stopping docker container
49
- protected correctExitCodes: number[] = [137];
50
-
51
- constructor(protected readonly options: DockerTaskOptions) {
52
- super(options);
53
-
54
- this.options.containerName ??= `runium-${this.options.image.replaceAll('/', '-').replaceAll(':', '-')}-${Date.now()}`;
55
- }
56
-
57
- /**
58
- * Prepare docker run command arguments
59
- */
60
- private prepareDockerArgs(): string[] {
61
- const args: string[] = ['run'];
62
-
63
- if (this.options.autoRemove !== false) {
64
- args.push('--rm');
65
- }
66
-
67
- if (this.options.containerName) {
68
- args.push('--name', this.options.containerName);
69
- }
70
-
71
- if (this.options.ports) {
72
- this.options.ports.forEach(port => {
73
- args.push('-p', port);
74
- });
75
- }
76
-
77
- if (this.options.volumes) {
78
- this.options.volumes.forEach(volume => {
79
- args.push('-v', volume);
80
- });
81
- }
82
-
83
- if (this.options.env) {
84
- Object.entries(this.options.env).forEach(([key, value]) => {
85
- args.push('-e', `${key}=${value}`);
86
- });
87
- }
88
-
89
- if (this.options.network) {
90
- args.push('--network', this.options.network);
91
- }
92
-
93
- if (this.options.user) {
94
- args.push('--user', this.options.user);
95
- }
96
-
97
- if (this.options.workdir) {
98
- args.push('--workdir', this.options.workdir);
99
- }
100
-
101
- if (this.options.entrypoint) {
102
- args.push('--entrypoint', this.options.entrypoint);
103
- }
104
-
105
- if (this.options.privileged) {
106
- args.push('--privileged');
107
- }
108
-
109
- args.push(this.options.image);
110
-
111
- if (this.options.command) {
112
- args.push(this.options.command);
113
- }
114
-
115
- if (this.options.arguments) {
116
- args.push(...this.options.arguments);
117
- }
118
-
119
- return args;
120
- }
121
-
122
- /**
123
- * Start task
124
- */
125
- async start(): Promise<void> {
126
- if (!this.canStart()) {
127
- return;
128
- }
129
-
130
- this.updateState({
131
- status: TaskStatus.STARTING,
132
- iteration: this.state.iteration + 1,
133
- pid: -1,
134
- containerName: undefined,
135
- exitCode: undefined,
136
- error: undefined,
137
- reason: undefined,
138
- });
139
-
140
- if (!(await isDockerInstalled())) {
141
- this.onError(new Error('Docker is not installed'));
142
- return;
143
- }
144
-
145
- if (!(await isDockerRunning())) {
146
- this.onError(new Error('Docker is not running'));
147
- return;
148
- }
149
-
150
- try {
151
- await this.initLogStreams();
152
-
153
- this.updateState({
154
- reason: `pull image: ${this.options.image}`,
155
- });
156
- await pullImage(this.options.image, {
157
- onStdErr: this.onStdErrData.bind(this),
158
- });
159
-
160
- const args = this.prepareDockerArgs();
161
-
162
- this.process = spawn('docker', args, {
163
- stdio: ['ignore', 'pipe', 'pipe'],
164
- } as SpawnOptionsWithoutStdio);
165
-
166
- this.addProcessListeners();
167
-
168
- this.setTTLTimer();
169
-
170
- this.updateState({
171
- status: TaskStatus.STARTED,
172
- containerName: this.options.containerName,
173
- pid: this.process!.pid,
174
- reason: undefined,
175
- });
176
- } catch (error) {
177
- this.onError(error as Error);
178
- }
179
- }
180
-
181
- /**
182
- * Stop task
183
- * @param reason
184
- */
185
- async stop(reason: string = ''): Promise<void> {
186
- if (!this.canStop()) {
187
- return;
188
- }
189
-
190
- this.updateState({
191
- status: TaskStatus.STOPPING,
192
- reason,
193
- });
194
-
195
- try {
196
- await new Promise<void>((resolve, reject) => {
197
- const stopProcess = spawn('docker', [
198
- 'stop',
199
- this.options.containerName!,
200
- ]);
201
- stopProcess.on('exit', code => {
202
- if (code === 0) {
203
- resolve();
204
- } else {
205
- reject(new Error(`Docker stop exited with code ${code}`));
206
- }
207
- });
208
- stopProcess.on('error', reject);
209
- });
210
- } catch (error) {
211
- this.onError(error as Error);
212
- } finally {
213
- await new Promise<void>(resolve => {
214
- this.process?.kill('SIGTERM' as NodeJS.Signals);
215
- this.process?.on('exit', () => {
216
- resolve();
217
- });
218
- });
219
- }
220
- }
221
- }
@@ -1,4 +0,0 @@
1
- import { exec } from 'node:child_process';
2
- import { promisify } from 'node:util';
3
-
4
- export const execAsync = promisify(exec);
@@ -1,13 +0,0 @@
1
- import { execAsync } from './exec-async.js';
2
-
3
- /**
4
- * Check if Docker is installed
5
- */
6
- export async function isDockerInstalled(): Promise<boolean> {
7
- try {
8
- await execAsync('docker --version');
9
- return true;
10
- } catch {
11
- return false;
12
- }
13
- }
@@ -1,13 +0,0 @@
1
- import { execAsync } from './exec-async.js';
2
-
3
- /**
4
- * Check if Docker daemon is running
5
- */
6
- export async function isDockerRunning(): Promise<boolean> {
7
- try {
8
- await execAsync('docker info');
9
- return true;
10
- } catch {
11
- return false;
12
- }
13
- }
@@ -1,47 +0,0 @@
1
- import { exec } from 'node:child_process';
2
-
3
- interface PullImageOptions {
4
- onStdOut?: (output: Buffer) => void;
5
- onStdErr?: (output: Buffer) => void;
6
- }
7
-
8
- /**
9
- * Pull a Docker image
10
- * @param imageName
11
- * @param options
12
- */
13
- export async function pullImage(
14
- imageName: string,
15
- options: PullImageOptions = {}
16
- ): Promise<void> {
17
- return new Promise((resolve, reject) => {
18
- const child = exec(`docker pull ${imageName}`);
19
-
20
- if (child.stdout && options.onStdOut) {
21
- child.stdout.on('data', data => {
22
- options.onStdOut!(data.toString());
23
- });
24
- }
25
-
26
- if (child.stderr && options.onStdErr) {
27
- child.stderr.on('data', data => {
28
- options.onStdErr!(data.toString());
29
- });
30
- }
31
-
32
- child.on('exit', (code: number | null) => {
33
- if (code === 0) {
34
- resolve();
35
- } else {
36
- reject({
37
- code,
38
- message: code === null ? '' : `docker pull exited with code ${code}`,
39
- });
40
- }
41
- });
42
-
43
- child.on('error', (error: Error) => {
44
- reject({ message: error.message, code: -1 });
45
- });
46
- });
47
- }
@@ -1,137 +0,0 @@
1
- import { ProjectSchemaExtensionTask } from '@runium/types-plugin';
2
-
3
- export function getDockerComposeTaskProjectSchema(): ProjectSchemaExtensionTask {
4
- return {
5
- type: 'docker-compose',
6
- options: {
7
- type: 'object',
8
- properties: {
9
- services: {
10
- type: 'object',
11
- patternProperties: {
12
- '^[a-zA-Z0-9_-]+$': {
13
- type: 'object',
14
- properties: {
15
- image: {
16
- type: 'string',
17
- },
18
- containerName: {
19
- type: 'string',
20
- },
21
- command: {
22
- type: 'string',
23
- },
24
- ports: {
25
- type: 'array',
26
- items: {
27
- type: 'string',
28
- },
29
- },
30
- volumes: {
31
- type: 'array',
32
- items: {
33
- type: 'string',
34
- },
35
- },
36
- environment: {
37
- $ref: '#/$defs/Runium_Env',
38
- },
39
- networks: {
40
- type: 'array',
41
- items: {
42
- type: 'string',
43
- },
44
- },
45
- dependsOn: {
46
- type: 'array',
47
- items: {
48
- type: 'string',
49
- },
50
- },
51
- restart: {
52
- type: 'string',
53
- enum: ['no', 'always', 'on-failure', 'unless-stopped'],
54
- },
55
- user: {
56
- type: 'string',
57
- },
58
- workdir: {
59
- type: 'string',
60
- },
61
- entrypoint: {
62
- type: 'string',
63
- },
64
- privileged: {
65
- type: 'boolean',
66
- },
67
- expose: {
68
- type: 'array',
69
- items: {
70
- type: 'string',
71
- },
72
- },
73
- healthcheck: {
74
- type: 'object',
75
- properties: {
76
- test: {
77
- oneOf: [
78
- { type: 'string' },
79
- {
80
- type: 'array',
81
- items: { type: 'string' },
82
- },
83
- ],
84
- },
85
- interval: {
86
- type: 'string',
87
- },
88
- timeout: {
89
- type: 'string',
90
- },
91
- retries: {
92
- type: 'number',
93
- },
94
- start_period: {
95
- type: 'string',
96
- },
97
- },
98
- required: ['test'],
99
- additionalProperties: false,
100
- },
101
- },
102
- required: ['image'],
103
- additionalProperties: false,
104
- },
105
- },
106
- additionalProperties: false,
107
- },
108
- networks: {
109
- type: 'object',
110
- patternProperties: {
111
- '^[a-zA-Z0-9_-]+$': {
112
- type: 'object',
113
- },
114
- },
115
- additionalProperties: false,
116
- },
117
- volumes: {
118
- type: 'object',
119
- patternProperties: {
120
- '^[a-zA-Z0-9_-]+$': {
121
- type: 'object',
122
- },
123
- },
124
- additionalProperties: false,
125
- },
126
- ttl: {
127
- type: 'number',
128
- },
129
- log: {
130
- $ref: '#/$defs/Runium_TaskLog',
131
- },
132
- },
133
- required: ['services'],
134
- additionalProperties: false,
135
- },
136
- };
137
- }
@@ -1,68 +0,0 @@
1
- import { ProjectSchemaExtensionTask } from '@runium/types-plugin';
2
-
3
- export function getDockerTaskProjectSchema(): ProjectSchemaExtensionTask {
4
- return {
5
- type: 'docker',
6
- options: {
7
- type: 'object',
8
- properties: {
9
- image: {
10
- type: 'string',
11
- },
12
- containerName: {
13
- type: 'string',
14
- },
15
- command: {
16
- type: 'string',
17
- },
18
- arguments: {
19
- type: 'array',
20
- items: {
21
- type: 'string',
22
- },
23
- },
24
- ports: {
25
- type: 'array',
26
- items: {
27
- type: 'string',
28
- },
29
- },
30
- volumes: {
31
- type: 'array',
32
- items: {
33
- type: 'string',
34
- },
35
- },
36
- env: {
37
- $ref: '#/$defs/Runium_Env',
38
- },
39
- network: {
40
- type: 'string',
41
- },
42
- autoRemove: {
43
- type: 'boolean',
44
- },
45
- user: {
46
- type: 'string',
47
- },
48
- workdir: {
49
- type: 'string',
50
- },
51
- entrypoint: {
52
- type: 'string',
53
- },
54
- privileged: {
55
- type: 'boolean',
56
- },
57
- ttl: {
58
- type: 'number',
59
- },
60
- log: {
61
- $ref: '#/$defs/Runium_TaskLog',
62
- },
63
- },
64
- required: ['image'],
65
- additionalProperties: false,
66
- },
67
- };
68
- }
package/tsconfig.json DELETED
@@ -1,22 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "baseUrl": ".",
4
- "target": "esnext",
5
- "outDir": "./lib",
6
- "rootDir": "src",
7
- "strict": true,
8
- "esModuleInterop": true,
9
- "skipLibCheck": true,
10
- "module": "esnext",
11
- "moduleResolution": "node",
12
- "forceConsistentCasingInFileNames": true,
13
- "declaration": false,
14
- "sourceMap": false,
15
- "strictNullChecks": true,
16
- "removeComments": true,
17
- "allowSyntheticDefaultImports": true,
18
- "types": ["node", "@runium/types-plugin"]
19
- },
20
- "include": ["src/**/*"],
21
- "exclude": ["node_modules", "dist"]
22
- }